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