View Javadoc

1   /***
2    *     Ambient - A music player for the Android platform
3    Copyright (C) 2007 Martin Vysny
4    
5    This program is free software: you can redistribute it and/or modify
6    it under the terms of the GNU General Public License as published by
7    the Free Software Foundation, either version 3 of the License, or
8    (at your option) any later version.
9    
10   This program is distributed in the hope that it will be useful,
11   but WITHOUT ANY WARRANTY; without even the implied warranty of
12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   GNU General Public License for more details.
14  
15   You should have received a copy of the GNU General Public License
16   along with this program.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  package sk.baka.ambient.views;
19  
20  import java.util.ArrayList;
21  import java.util.Arrays;
22  import java.util.BitSet;
23  import java.util.List;
24  
25  import sk.baka.ambient.AmbientApplication;
26  import sk.baka.ambient.R;
27  import sk.baka.ambient.commons.Interval;
28  import android.content.Context;
29  import android.content.res.TypedArray;
30  import android.graphics.Bitmap;
31  import android.graphics.BitmapFactory;
32  import android.graphics.Canvas;
33  import android.graphics.ColorMatrix;
34  import android.graphics.ColorMatrixColorFilter;
35  import android.graphics.Paint;
36  import android.graphics.Point;
37  import android.graphics.Rect;
38  import android.util.AttributeSet;
39  import android.view.KeyEvent;
40  import android.view.MotionEvent;
41  import android.view.View;
42  import android.widget.AbsoluteLayout;
43  import android.widget.AdapterView.OnItemClickListener;
44  
45  /***
46   * A very simple, naive and inefficient implementation of the
47   * apple-launchbar-like button bar.
48   * 
49   * @author Martin Vysny
50   */
51  public final class ButtonBar extends View {
52  	/***
53  	 * Constructor. This version is only needed if you will be instantiating the
54  	 * object manually (not from a layout XML file).
55  	 * 
56  	 * @param context
57  	 * @param rootId
58  	 *            Denotes root absolute layout id.
59  	 * @param textHintColor
60  	 *            text hint color, default white.
61  	 * @param textHintBgColor
62  	 *            text background color, default 50% transparent black.
63  	 * @param extendDown
64  	 *            If <code>true</code> (the default) then hovered buttons are
65  	 *            extended downwards. If <code>false</code> then buttons are
66  	 *            extended upwards.
67  	 */
68  	public ButtonBar(Context context, final int rootId,
69  			final Integer textHintColor, final Integer textHintBgColor,
70  			final Boolean extendDown) {
71  		super(context);
72  		initView();
73  		this.rootId = rootId;
74  		textPaint.setColor(textHintColor == null ? 0xFFFFFFFF : textHintColor);
75  		textBgPaint.setColor(textHintBgColor == null ? 0x88000000
76  				: textHintBgColor);
77  		this.extendDown = extendDown == null ? true : extendDown;
78  		checkValues();
79  	}
80  
81  	/***
82  	 * If <code>true</code> (the default) then hovered buttons are extended
83  	 * downwards. If <code>false</code> then buttons are extended upwards.
84  	 */
85  	private boolean extendDown;
86  
87  	/***
88  	 * Denotes root absolute layout id.
89  	 */
90  	private int rootId;
91  
92  	/***
93  	 * @param context
94  	 * @param attrs
95  	 * @param defStyle
96  	 */
97  	public ButtonBar(Context context, AttributeSet attrs, int defStyle) {
98  		super(context, attrs, defStyle);
99  		init(attrs);
100 	}
101 
102 	/***
103 	 * @param context
104 	 * @param attrs
105 	 */
106 	public ButtonBar(Context context, AttributeSet attrs) {
107 		super(context, attrs);
108 		init(attrs);
109 	}
110 
111 	private void init(final AttributeSet attrs) {
112 		initView();
113 		final TypedArray a = getContext().obtainStyledAttributes(
114 				attrs, R.styleable.ButtonBar);
115 		rootId = a.getResourceId(R.styleable.ButtonBar_rootId, -1);
116 		textPaint.setColor(a.getColor(R.styleable.ButtonBar_textHintColor,
117 				0xFFFFFFFF));
118 		textBgPaint.setColor(a.getColor(R.styleable.ButtonBar_textHintBgColor,
119 				0x88000000));
120 		extendDown = a.getBoolean(R.styleable.ButtonBar_extendDown, true);
121 		checkValues();
122 	}
123 
124 	private void checkValues() {
125 		if (rootId < 0) {
126 			throw new IllegalArgumentException("The rootId attribute missing");
127 		}
128 	}
129 
130 	private final void initView() {
131 		selectedPaint = new Paint();
132 		selectedPaint.setFilterBitmap(true);
133 		final ColorMatrix cm = new ColorMatrix();
134 		cm.setSaturation(-1f);
135 		selectedPaint.setColorFilter(new ColorMatrixColorFilter(cm));
136 		normalPaint = new Paint();
137 		normalPaint.setFilterBitmap(true);
138 		setFocusable(true);
139 		setFocusableInTouchMode(false);
140 		textPaint.setAntiAlias(true);
141 	}
142 
143 	/***
144 	 * The root layout. Floating view will be created as a child of this layout.
145 	 */
146 	private AbsoluteLayout root = null;
147 
148 	/***
149 	 * A floating image of the button bar. Used to show the bar contents when
150 	 * the bar is selected and widened.
151 	 * 
152 	 * @author Martin Vysny
153 	 */
154 	private class FloatingImage extends View {
155 		private FloatingImage(Context context) {
156 			super(context);
157 		}
158 
159 		@Override
160 		protected void onDraw(Canvas canvas) {
161 			drawOn(canvas);
162 		}
163 
164 		/***
165 		 * @see android.view.View#measure(int, int)
166 		 */
167 		@Override
168 		protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
169 			// we will always be told the exact size
170 			setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
171 					MeasureSpec.getSize(heightMeasureSpec));
172 		}
173 	}
174 
175 	/***
176 	 * A floating image of the button bar. Used to show the bar contents when
177 	 * the bar is selected and widened. If non-<code>null</code> it is being
178 	 * shown on screen.
179 	 */
180 	private FloatingImage floatingGhost = null;
181 
182 	@Override
183 	protected void onAttachedToWindow() {
184 		super.onAttachedToWindow();
185 		root = (AbsoluteLayout) getRootView().findViewById(rootId);
186 		if (root == null) {
187 			throw new IllegalArgumentException("No view with ID " + rootId);
188 		}
189 		checkMark = BitmapFactory.decodeResource(getResources(),
190 				android.R.drawable.checkbox_on_background);
191 		createBitmaps();
192 	}
193 
194 	private Bitmap checkMark = null;
195 	
196 	@Override
197 	protected void onDetachedFromWindow() {
198 		super.onDetachedFromWindow();
199 		destroyGhost();
200 		ViewUtils.recycleBitmaps(bitmaps);
201 		root = null;
202 		if (checkMark != null) {
203 			checkMark.recycle();
204 			checkMark = null;
205 		}
206 	}
207 
208 	private void createGhost(final AbsoluteLayout.LayoutParams lp) {
209 		if (root != null) {
210 			if (floatingGhost != null)
211 				throw new IllegalStateException("Ghost already created");
212 			floatingGhost = new FloatingImage(getContext());
213 			floatingGhost.setLayoutParams(lp != null ? lp
214 					: new AbsoluteLayout.LayoutParams(0, 0, 0, 0));
215 			root.addView(floatingGhost);
216 			floatingGhost.bringToFront();
217 			floatingGhost.setVisibility(View.VISIBLE);
218 		}
219 	}
220 
221 	private void destroyGhost() {
222 		if ((root != null) && (floatingGhost != null)) {
223 			floatingGhost.setVisibility(View.INVISIBLE);
224 			root.removeView(floatingGhost);
225 			floatingGhost = null;
226 		}
227 	}
228 
229 	/***
230 	 * Sets a new set of images to be shown.
231 	 * 
232 	 * @param bitmapResources
233 	 *            the drawables to show
234 	 * @param activityName
235 	 *            the activity captions. If <code>-1</code> then the caption
236 	 *            is not shown for the item. May be <code>null</code> if no
237 	 *            captions are required to be shown.
238 	 * @param bitmapSize
239 	 *            the size of all bitmaps. Here the {@link Point} class is not
240 	 *            used as a point, it denotes dimension instead.
241 	 * @param hoveredBitmapSize
242 	 *            the size of hovered bitmap. Here the {@link Point} class is
243 	 *            not used as a point, it denotes dimension instead.
244 	 */
245 	public void setBitmaps(final int[] bitmapResources,
246 			final int[] activityName, final Point bitmapSize,
247 			final Point hoveredBitmapSize) {
248 		createBitmapsIfNeeded(bitmapResources);
249 		activityNames.clear();
250 		for (int i = 0; i < bitmapResources.length; i++) {
251 			final int stringId = activityName == null ? -1 : activityName[i];
252 			final String name = stringId < 0 ? null : getResources().getString(
253 					stringId);
254 			activityNames.add(name);
255 		}
256 		this.bitmapSize = ViewUtils.clone(bitmapSize);
257 		this.hoveredBitmapSize = ViewUtils.clone(hoveredBitmapSize);
258 		// clear and recompute internal caches
259 		imageXOffsets = null;
260 		imageHeights = null;
261 		computeZoomFunctionValues();
262 		requestLayout();
263 		invalidate();
264 	}
265 
266 	private void createBitmapsIfNeeded(int[] bitmapResources) {
267 		if ((bitmaps.size() == bitmapResources.length)
268 				&& (this.bitmapResources != null)
269 				&& Arrays.equals(bitmapResources, this.bitmapResources)) {
270 			// prevent unnecessary bitmap creation. This should help prevent
271 			// OutOfMemory: bitmap size exceeds VM budget errors
272 			return;
273 		}
274 		this.bitmapResources = bitmapResources.clone();
275 		createBitmaps();
276 	}
277 
278 	private void createBitmaps() {
279 		ViewUtils.recycleBitmaps(bitmaps);
280 		for (int i = 0; i < bitmapResources.length; i++) {
281 			bitmaps.add(BitmapFactory.decodeResource(getResources(),
282 					bitmapResources[i]));
283 		}
284 	}
285 	
286 	/***
287 	 * Images resources shown by the button bar.
288 	 */
289 	private int[] bitmapResources;
290 	
291 	/***
292 	 * Bitmaps loaded by resolving {@link #bitmapResources}.
293 	 */
294 	private final List<Bitmap> bitmaps = new ArrayList<Bitmap>();
295 
296 	private final List<String> activityNames = new ArrayList<String>();
297 
298 	/***
299 	 * All bitmaps will be scaled to this size. Here the {@link Point} class is
300 	 * not used as a point, it denotes dimension instead.
301 	 */
302 	private Point bitmapSize = new Point(32, 32);
303 
304 	/***
305 	 * The hovered (the cursor is over this bitmap) bitmap size. Here the
306 	 * {@link Point} class is not used as a point, it denotes dimension instead.
307 	 */
308 	private Point hoveredBitmapSize = new Point(48, 48);
309 
310 	/***
311 	 * The computed view width.
312 	 * 
313 	 * @return computed view width in pixels.
314 	 */
315 	private int getViewWidth() {
316 		return hoverX >= 0 ? (bitmaps.size() - 1) * bitmapSize.x
317 				+ hoveredBitmapSize.x : bitmaps.size() * bitmapSize.x;
318 	}
319 
320 	/***
321 	 * Computes the view height.
322 	 * 
323 	 * @return the view height, depending on current {@link #hoverX} value.
324 	 */
325 	private int getViewHeight() {
326 		return bitmapSize.y;
327 	}
328 
329 	/***
330 	 * @see android.view.View#measure(int, int)
331 	 */
332 	@Override
333 	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
334 		setMeasuredDimension(measureWidth(widthMeasureSpec),
335 				measureHeight(heightMeasureSpec));
336 	}
337 
338 	/***
339 	 * Determines the width of this view
340 	 * 
341 	 * @param measureSpec
342 	 *            A measureSpec packed into an integer
343 	 * @return The width of the view, honoring constraints from measureSpec
344 	 */
345 	private int measureWidth(int measureSpec) {
346 		int specMode = MeasureSpec.getMode(measureSpec);
347 		int specSize = MeasureSpec.getSize(measureSpec);
348 		if (specMode == MeasureSpec.EXACTLY) {
349 			// We were told how big to be
350 			return specSize;
351 		}
352 		return getViewWidth();
353 	}
354 
355 	/***
356 	 * Determines the height of this view
357 	 * 
358 	 * @param measureSpec
359 	 *            A measureSpec packed into an integer
360 	 * @return The height of the view, honoring constraints from measureSpec
361 	 */
362 	private int measureHeight(int measureSpec) {
363 		int specMode = MeasureSpec.getMode(measureSpec);
364 		int specSize = MeasureSpec.getSize(measureSpec);
365 		if (specMode == MeasureSpec.EXACTLY) {
366 			// We were told how big to be
367 			return specSize;
368 		}
369 		return getViewHeight();
370 	}
371 
372 	/***
373 	 * The X coordinate of the hovering cursor.
374 	 */
375 	private int hoverX = -1;
376 
377 	/***
378 	 * The Y coordinate of the hovering cursor.
379 	 */
380 	private int hoverY = -1;
381 
382 	/***
383 	 * If <code>true</code> then the cursor is hovering over the view.
384 	 */
385 	private boolean isHovering;
386 
387 	/***
388 	 * Briefly flash the selected image with this paint.
389 	 */
390 	private Paint selectedPaint;
391 
392 	/***
393 	 * Paint for regular (non-selected) images.
394 	 */
395 	private Paint normalPaint;
396 
397 	/***
398 	 * The selected image index.
399 	 */
400 	private int selectedIndex = -1;
401 
402 	/***
403 	 * Describes the image zoom function. The value in the middle of the array
404 	 * denotes the size of the fully zoomed image. Here the {@link Point} class
405 	 * is not used as a point, it denotes dimension instead.
406 	 */
407 	private Point[] zoomFunctionValues;
408 
409 	private void computeZoomFunctionValues() {
410 		final int max = hoveredBitmapSize.x;
411 		zoomFunctionValues = new Point[max * 2];
412 		for (int i = 0; i < max * 2; i++) {
413 			final double angle = Math.PI * i / max / 2;
414 			final double sin = Math.sin(angle);
415 			final int sizeDiffX = (int) (sin * (hoveredBitmapSize.x - bitmapSize.x));
416 			final int sizeDiffY = (int) (sin * (hoveredBitmapSize.y - bitmapSize.y));
417 			zoomFunctionValues[i] = new Point(sizeDiffX + bitmapSize.x,
418 					sizeDiffY + bitmapSize.y);
419 		}
420 	}
421 
422 	/***
423 	 * Computes the image size, depending on the destination from the cursor
424 	 * center.
425 	 * 
426 	 * @param delta
427 	 *            the delta from the cursor center.
428 	 * @return the image size. Must not be modified!
429 	 */
430 	private Point getSizes(final int delta) {
431 		final int absDelta = Math.abs(delta);
432 		if (absDelta >= hoveredBitmapSize.x) {
433 			return bitmapSize;
434 		}
435 		final int i = hoveredBitmapSize.x - absDelta;
436 		return zoomFunctionValues[i];
437 	}
438 
439 	/***
440 	 * Offsets of images in pixels from the left corner of the canvas. Used by
441 	 * the drawing algorithm to draw the images correctly.
442 	 */
443 	private int[] imageXOffsets;
444 	/***
445 	 * Height of images in pixels. Used by the drawing algorithm to draw the
446 	 * images correctly.
447 	 */
448 	private int[] imageHeights;
449 
450 	private void computeImageXOffsets() {
451 		final int width = getWidth();
452 		int hoverX = this.hoverX;
453 		if (!isHovering || (hoverY < 0) || (hoverY >= getHeight())) {
454 			hoverX = -1000;
455 		}
456 		if (imageXOffsets == null) {
457 			imageXOffsets = new int[bitmaps.size() + 1];
458 			imageHeights = new int[bitmaps.size() + 1];
459 		}
460 		// two-pass algorithm - first recompute the sizes, then offset them to
461 		// make the images centered.
462 		final int initialX = (width - getViewWidth()) / 2;
463 		// some icons might obscured. Scroll if necessary
464 		int scrollOffset = 0;
465 		if (initialX < 0 && hoverX >= 0 && width > 0) {
466 			scrollOffset = initialX * hoverX * 2 / width - initialX;
467 			hoverX -= scrollOffset;
468 		}
469 		int x = initialX;
470 		final int bitmapCount = bitmaps.size();
471 		final int bitmapSizeDiv2 = bitmapSize.x / 2;
472 		int maxSize = 0;
473 		int maxSizeIndex = -1;
474 		for (int i = 0; i < bitmapCount; i++) {
475 			final int imgCenter = x + bitmapSizeDiv2;
476 			final Point size = getSizes(hoverX - imgCenter);
477 			imageXOffsets[i] = x;
478 			imageHeights[i] = size.y;
479 			x += size.x;
480 			if (maxSize < size.x) {
481 				maxSize = size.x;
482 				maxSizeIndex = i;
483 			}
484 		}
485 		selectedIndex = maxSize == bitmapSize.x ? -1 : maxSizeIndex;
486 		imageXOffsets[bitmaps.size()] = x;
487 		// second pass
488 		final int expectedEnd = (width + x - initialX) / 2;
489 		int delta = expectedEnd - x;
490 		delta += scrollOffset;
491 		if (delta != 0) {
492 			for (int i = 0; i <= bitmapCount; i++) {
493 				imageXOffsets[i] += delta;
494 			}
495 		}
496 	}
497 
498 	/***
499 	 * Used to paint hint text.
500 	 */
501 	private final Paint textPaint = new Paint();
502 	/***
503 	 * Used to paint hint background.
504 	 */
505 	private final Paint textBgPaint = new Paint();
506 
507 	/***
508 	 * The view utility instance.
509 	 */
510 	private final ViewUtils utils = new ViewUtils();
511 
512 	/***
513 	 * A cached rectangle instance used by the {@link #drawOn(Canvas)} method.
514 	 */
515 	private final Rect drawRect = new Rect();
516 
517 	@Override
518 	protected void onDraw(Canvas canvas) {
519 		super.onDraw(canvas);
520 		if (floatingGhost != null) {
521 			// do nothing, the component contents is actually being painted on
522 			// the floating ghost component
523 			return;
524 		}
525 		drawOn(canvas);
526 	}
527 
528 	private final Rect drawCheckedRect = new Rect();
529 	
530 	private void drawOn(final Canvas canvas) {
531 		computeImageXOffsets();
532 		drawRect.top = 0;
533 		drawRect.bottom = hoveredBitmapSize.y;
534 		final int bitmapCount = bitmaps.size();
535 		for (int i = 0; i < bitmapCount; i++) {
536 			final int size = imageXOffsets[i + 1] - imageXOffsets[i];
537 			drawRect.left = imageXOffsets[i];
538 			if (extendDown) {
539 				drawRect.bottom = imageHeights[i];
540 			} else {
541 				drawRect.top = hoveredBitmapSize.y - imageHeights[i];
542 			}
543 			drawRect.right = drawRect.left + size;
544 			if (highlight.contains(i)) {
545 				// highlight the button
546 				canvas.drawRect(drawRect, textBgPaint);
547 			}
548 			canvas.drawBitmap(bitmaps.get(i), null, drawRect,
549 					(i == selectedIndex) ? selectedPaint : normalPaint);
550 			if (checked.get(i)) {
551 				drawCheckedRect.bottom = drawRect.bottom;
552 				drawCheckedRect.right = drawRect.right;
553 				drawCheckedRect.left = drawRect.right - checkMark.getWidth();
554 				drawCheckedRect.top = drawRect.bottom - checkMark.getHeight();
555 				canvas
556 						.drawBitmap(checkMark, null, drawCheckedRect,
557 								normalPaint);
558 			}
559 		}
560 		// draw the activity name on top of the bitmap.
561 		if (selectedIndex >= 0) {
562 			final String text = activityNames.get(selectedIndex);
563 			if (text != null) {
564 				utils.measureTextCache(textPaint, text);
565 				drawRect.top = 2;
566 				drawRect.bottom = drawRect.top + 4 + utils.measuredText.y;
567 				drawRect.left = hoverX - (utils.measuredText.x / 2) - 2;
568 				drawRect.right = drawRect.left + 4 + utils.measuredText.x;
569 				canvas.drawRect(drawRect, textBgPaint);
570 				canvas.drawText(text, drawRect.left + 2, drawRect.top + 2
571 						- textPaint.getFontMetricsInt().top, textPaint);
572 			}
573 		}
574 	}
575 
576 	@Override
577 	protected void onLayout(boolean changed, final int left, final int top,
578 			final int right, final int bottom) {
579 		super.onLayout(changed, left, top, right, bottom);
580 		AmbientApplication.getHandler().post(new Runnable() {
581 			public void run() {
582 				// hide under the floating ghost if needed
583 				if (floatingGhost != null) {
584 					utils.translateCoordinates(new Point(left, top),
585 							(View) getParent(), root);
586 					final AbsoluteLayout.LayoutParams lp = (AbsoluteLayout.LayoutParams) floatingGhost
587 							.getLayoutParams();
588 					lp.width = right - left;
589 					lp.height = hoveredBitmapSize.y;
590 					lp.x = utils.translated.x;
591 					lp.y = utils.translated.y;
592 					floatingGhost.setLayoutParams(lp);
593 				}
594 			}
595 		});
596 	}
597 
598 	@Override
599 	public boolean onTouchEvent(MotionEvent event) {
600 		switch (event.getAction()) {
601 		case MotionEvent.ACTION_DOWN:
602 			doStart((int) event.getX(), (int) event.getY());
603 			break;
604 		case MotionEvent.ACTION_UP:
605 			doButtonPress(false);
606 			break;
607 		case MotionEvent.ACTION_CANCEL:
608 			doCancel();
609 			break;
610 		case MotionEvent.ACTION_MOVE:
611 			doMove((int) event.getX(), (int) event.getY());
612 			break;
613 		}
614 		return true;
615 	}
616 
617 	private class KeyHandler extends KeyEventHandler {
618 		protected KeyHandler() {
619 			super();
620 		}
621 
622 		@Override
623 		protected boolean onConfirm() {
624 			doButtonPress(true);
625 			return true;
626 		}
627 
628 		@Override
629 		protected boolean onDown() {
630 			moveFocus(ButtonBar.this, false);
631 			return true;
632 		}
633 
634 		@Override
635 		protected boolean onLeft() {
636 			move(-bitmapSize.x);
637 			return true;
638 		}
639 
640 		@Override
641 		protected boolean onRight() {
642 			move(bitmapSize.x);
643 			return true;
644 		}
645 
646 		@Override
647 		protected boolean onUp() {
648 			moveFocus(ButtonBar.this, true);
649 			return true;
650 		}
651 		
652 		private void move(int delta) {
653 			int x = hoverX;
654 			if (((delta < 0) && (x > 0)) || ((delta > 0) && (x < getWidth()))) {
655 				x += delta;
656 			}
657 			doMove(x, hoverY);
658 		}
659 	}
660 	
661 	private final KeyEventHandler handler=new KeyHandler();
662 	
663 	@Override
664 	public boolean onKeyDown(int keyCode, KeyEvent event) {
665 		return handler.onKeyDown(keyCode, event);
666 	}
667 
668 	@Override
669 	public boolean onKeyUp(int keyCode, KeyEvent event) {
670 		return handler.onKeyUp(keyCode, event);
671 	}
672 
673 	@Override
674 	public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
675 		return handler.onKeyMultiple(keyCode, repeatCount, event);
676 	}
677 
678 	@Override
679 	protected void onFocusChanged(boolean gainFocus, int direction,
680 			Rect previouslyFocusedRect) {
681 		if (gainFocus) {
682 			int startX = getWidth() / 2;
683 			if (bitmaps.size() % 2 == 0) {
684 				startX += bitmapSize.x / 2;
685 			}
686 			doStart(startX, 0);
687 		} else {
688 			doCancel();
689 		}
690 	}
691 
692 	private void doMove(final int x, final int y) {
693 		if (!isHovering)
694 			return;
695 		hoverX = x;
696 		hoverY = y;
697 		invalidate(false);
698 	}
699 
700 	/***
701 	 * Activates the button bar.
702 	 * 
703 	 * @param x
704 	 * @param y
705 	 */
706 	private void doStart(final int x, final int y) {
707 		hoverX = x;
708 		hoverY = y;
709 		if (!isHovering) {
710 			isHovering = true;
711 			createGhost(null);
712 			requestLayout();
713 		}
714 	}
715 
716 	/***
717 	 * Cancels the button pressing.
718 	 */
719 	private void doCancel() {
720 		if (isHovering) {
721 			destroyGhost();
722 			hoverX = -1000;
723 			isHovering = false;
724 			requestLayout();
725 		}
726 	}
727 
728 	/***
729 	 * Highlighted buttons.
730 	 */
731 	private Interval highlight = Interval.EMPTY;
732 
733 	private final static BitSet EMPTY = new BitSet();
734 	
735 	/***
736 	 * Checked buttons.
737 	 */
738 	private BitSet checked = EMPTY;
739 
740 	/***
741 	 * Highlights given buttons.
742 	 * 
743 	 * @param interval
744 	 *            highlighted buttons. May be <code>null</code> - in this case
745 	 *            the interval is empty.
746 	 * @param checked
747 	 *            a set of checked items.
748 	 */
749 	public void highlight(final Interval interval, final BitSet checked) {
750 		final Interval h = interval == null ? Interval.EMPTY : interval;
751 		final BitSet c = checked == null ? EMPTY : checked;
752 		if (highlight.equals(h) && c.equals(this.checked)) {
753 			return;
754 		}
755 		highlight = h;
756 		this.checked = (BitSet) c.clone();
757 		invalidate(true);
758 	}
759 
760 	/***
761 	 * Returns the highlight interval.
762 	 * 
763 	 * @return the highlight interval, never <code>null</code>
764 	 */
765 	public Interval getHighlight() {
766 		return highlight;
767 	}
768 
769 	/***
770 	 * Presses a button.
771 	 * 
772 	 * @param stayHovered
773 	 *            if <code>true</code> then the component stays in hover mode.
774 	 */
775 	private void doButtonPress(final boolean stayHovered) {
776 		if (!isHovering) {
777 			return;
778 		}
779 		post(new Runnable() {
780 			public void run() {
781 				if ((listener != null) && (selectedIndex >= 0)) {
782 					listener.onItemClick(null, ButtonBar.this, selectedIndex,
783 							selectedIndex);
784 				}
785 				if (!stayHovered) {
786 					destroyGhost();
787 					hoverX = -1000;
788 					isHovering = false;
789 					requestLayout();
790 				}
791 				invalidate(true);
792 			}
793 		});
794 	}
795 
796 	/***
797 	 * The ONCLICK listener. Both the position and the id fields will hold index
798 	 * of image being clicked.
799 	 */
800 	public OnItemClickListener listener;
801 
802 	private void invalidate(boolean both) {
803 		if (floatingGhost != null) {
804 			floatingGhost.invalidate();
805 		}
806 		if ((floatingGhost == null) || both) {
807 			invalidate();
808 		}
809 	}
810 
811 	/***
812 	 * Returns the current check state.
813 	 * 
814 	 * @return checked state of the buttons, never <code>null</code>.
815 	 */
816 	public BitSet getChecked() {
817 		return (BitSet) checked.clone();
818 	}
819 }