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
131
132
133
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
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
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
266
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
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
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
456
457 final int initialX = (width - getViewWidth()) / 2;
458
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
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
517
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
539 canvas.drawRect(drawRect, textBgPaint);
540 }
541 canvas.drawBitmap(bitmaps.get(i), null, drawRect,
542 (i == selectedIndex) ? selectedPaint : normalPaint);
543 }
544
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
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
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 }