1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.music;
18 
19 import android.content.Context;
20 import android.graphics.Canvas;
21 import android.graphics.Paint;
22 import android.graphics.Rect;
23 import android.graphics.drawable.Drawable;
24 import android.graphics.drawable.NinePatchDrawable;
25 import android.text.TextPaint;
26 import android.util.AttributeSet;
27 import android.util.Log;
28 import android.view.KeyEvent;
29 import android.view.MotionEvent;
30 import android.view.View;
31 
32 
33 public class VerticalTextSpinner extends View {
34 
35     private static final int SELECTOR_ARROW_HEIGHT = 15;
36 
37     private static int TEXT_SPACING;
38     private static int TEXT_MARGIN_RIGHT;
39     private static int TEXT_SIZE;
40     private static int TEXT1_Y;
41     private static int TEXT2_Y;
42     private static int TEXT3_Y;
43     private static int TEXT4_Y;
44     private static int TEXT5_Y;
45     private static int SCROLL_DISTANCE;
46 
47     private static final int SCROLL_MODE_NONE = 0;
48     private static final int SCROLL_MODE_UP = 1;
49     private static final int SCROLL_MODE_DOWN = 2;
50 
51     private static final long DEFAULT_SCROLL_INTERVAL_MS = 400;
52     private static final int MIN_ANIMATIONS = 4;
53 
54     private final Drawable mBackgroundFocused;
55     private final Drawable mSelectorFocused;
56     private final Drawable mSelectorNormal;
57     private final int mSelectorDefaultY;
58     private final int mSelectorMinY;
59     private final int mSelectorMaxY;
60     private final int mSelectorHeight;
61     private final TextPaint mTextPaintDark;
62     private final TextPaint mTextPaintLight;
63 
64     private int mSelectorY;
65     private Drawable mSelector;
66     private int mDownY;
67     private boolean isDraggingSelector;
68     private int mScrollMode;
69     private long mScrollInterval;
70     private boolean mIsAnimationRunning;
71     private boolean mStopAnimation;
72     private boolean mWrapAround = true;
73 
74     private int mTotalAnimatedDistance;
75     private int mNumberOfAnimations;
76     private long mDelayBetweenAnimations;
77     private int mDistanceOfEachAnimation;
78 
79     private String[] mTextList;
80     private int mCurrentSelectedPos;
81     private OnChangedListener mListener;
82 
83     private String mText1;
84     private String mText2;
85     private String mText3;
86     private String mText4;
87     private String mText5;
88 
89     public interface OnChangedListener {
onChanged( VerticalTextSpinner spinner, int oldPos, int newPos, String[] items)90         void onChanged(
91                 VerticalTextSpinner spinner, int oldPos, int newPos, String[] items);
92     }
93 
VerticalTextSpinner(Context context)94     public VerticalTextSpinner(Context context) {
95         this(context, null);
96     }
97 
VerticalTextSpinner(Context context, AttributeSet attrs)98     public VerticalTextSpinner(Context context, AttributeSet attrs) {
99         this(context, attrs, 0);
100     }
101 
VerticalTextSpinner(Context context, AttributeSet attrs, int defStyle)102     public VerticalTextSpinner(Context context, AttributeSet attrs,
103             int defStyle) {
104         super(context, attrs, defStyle);
105 
106         float scale = getResources().getDisplayMetrics().density;
107         TEXT_SPACING = (int)(18 * scale);
108         TEXT_MARGIN_RIGHT = (int)(25 * scale);
109         TEXT_SIZE = (int)(22 * scale);
110         SCROLL_DISTANCE = TEXT_SIZE + TEXT_SPACING;
111         TEXT1_Y = (TEXT_SIZE * (-2 + 2)) + (TEXT_SPACING * (-2 + 1));
112         TEXT2_Y = (TEXT_SIZE * (-1 + 2)) + (TEXT_SPACING * (-1 + 1));
113         TEXT3_Y = (TEXT_SIZE * (0 + 2)) + (TEXT_SPACING * (0 + 1));
114         TEXT4_Y = (TEXT_SIZE * (1 + 2)) + (TEXT_SPACING * (1 + 1));
115         TEXT5_Y = (TEXT_SIZE * (2 + 2)) + (TEXT_SPACING * (2 + 1));
116 
117         mBackgroundFocused = context.getResources().getDrawable(R.drawable.pickerbox_background);
118         mSelectorFocused = context.getResources().getDrawable(R.drawable.pickerbox_selected);
119         mSelectorNormal = context.getResources().getDrawable(R.drawable.pickerbox_unselected);
120 
121         mSelectorHeight = mSelectorFocused.getIntrinsicHeight();
122         mSelectorDefaultY = (mBackgroundFocused.getIntrinsicHeight() - mSelectorHeight) / 2;
123         mSelectorMinY = 0;
124         mSelectorMaxY = mBackgroundFocused.getIntrinsicHeight() - mSelectorHeight;
125 
126         mSelector = mSelectorNormal;
127         mSelectorY = mSelectorDefaultY;
128 
129         mTextPaintDark = new TextPaint(Paint.ANTI_ALIAS_FLAG);
130         mTextPaintDark.setTextSize(TEXT_SIZE);
131         mTextPaintDark.setColor(context.getResources()
132             .getColor(android.R.color.primary_text_light));
133 
134         mTextPaintLight = new TextPaint(Paint.ANTI_ALIAS_FLAG);
135         mTextPaintLight.setTextSize(TEXT_SIZE);
136         mTextPaintLight.setColor(context.getResources()
137             .getColor(android.R.color.secondary_text_dark));
138 
139         mScrollMode = SCROLL_MODE_NONE;
140         mScrollInterval = DEFAULT_SCROLL_INTERVAL_MS;
141         calculateAnimationValues();
142     }
143 
setOnChangeListener(OnChangedListener listener)144     public void setOnChangeListener(OnChangedListener listener) {
145         mListener = listener;
146     }
147 
setItems(String[] textList)148     public void setItems(String[] textList) {
149         mTextList = textList;
150         calculateTextPositions();
151     }
152 
setSelectedPos(int selectedPos)153     public void setSelectedPos(int selectedPos) {
154         mCurrentSelectedPos = selectedPos;
155         calculateTextPositions();
156         postInvalidate();
157     }
158 
setScrollInterval(long interval)159     public void setScrollInterval(long interval) {
160         mScrollInterval = interval;
161         calculateAnimationValues();
162     }
163 
setWrapAround(boolean wrap)164     public void setWrapAround(boolean wrap) {
165         mWrapAround = wrap;
166     }
167 
168     @Override
onKeyDown(int keyCode, KeyEvent event)169     public boolean onKeyDown(int keyCode, KeyEvent event) {
170 
171         /* This is a bit confusing, when we get the key event
172          * DPAD_DOWN we actually roll the spinner up. When the
173          * key event is DPAD_UP we roll the spinner down.
174          */
175         if ((keyCode == KeyEvent.KEYCODE_DPAD_UP) && canScrollDown()) {
176             mScrollMode = SCROLL_MODE_DOWN;
177             scroll();
178             mStopAnimation = true;
179             return true;
180         } else if ((keyCode == KeyEvent.KEYCODE_DPAD_DOWN) && canScrollUp()) {
181             mScrollMode = SCROLL_MODE_UP;
182             scroll();
183             mStopAnimation = true;
184             return true;
185         }
186         return super.onKeyDown(keyCode, event);
187     }
188 
canScrollDown()189     private boolean canScrollDown() {
190         return (mCurrentSelectedPos > 0) || mWrapAround;
191     }
192 
canScrollUp()193     private boolean canScrollUp() {
194         return ((mCurrentSelectedPos < (mTextList.length - 1)) || mWrapAround);
195     }
196 
197     @Override
onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect)198     protected void onFocusChanged(boolean gainFocus, int direction,
199             Rect previouslyFocusedRect) {
200         if (gainFocus) {
201             setBackgroundDrawable(mBackgroundFocused);
202             mSelector = mSelectorFocused;
203         } else {
204             setBackgroundDrawable(null);
205             mSelector = mSelectorNormal;
206             mSelectorY = mSelectorDefaultY;
207         }
208     }
209 
210     @Override
onTouchEvent(MotionEvent event)211     public boolean onTouchEvent(MotionEvent event) {
212         final int action = event.getAction();
213         final int y = (int) event.getY();
214 
215         switch (action) {
216         case MotionEvent.ACTION_DOWN:
217             requestFocus();
218             mDownY = y;
219             isDraggingSelector = (y >= mSelectorY) &&
220                     (y <= (mSelectorY + mSelector.getIntrinsicHeight()));
221             break;
222 
223         case MotionEvent.ACTION_MOVE:
224             if (isDraggingSelector) {
225                 int top = mSelectorDefaultY + (y - mDownY);
226                 if (top <= mSelectorMinY && canScrollDown()) {
227                     mSelectorY = mSelectorMinY;
228                     mStopAnimation = false;
229                     if (mScrollMode != SCROLL_MODE_DOWN) {
230                         mScrollMode = SCROLL_MODE_DOWN;
231                         scroll();
232                     }
233                 } else if (top >= mSelectorMaxY && canScrollUp()) {
234                     mSelectorY = mSelectorMaxY;
235                     mStopAnimation = false;
236                     if (mScrollMode != SCROLL_MODE_UP) {
237                         mScrollMode = SCROLL_MODE_UP;
238                         scroll();
239                     }
240                 } else {
241                     mSelectorY = top;
242                     mStopAnimation = true;
243                 }
244             }
245             break;
246 
247         case MotionEvent.ACTION_UP:
248         case MotionEvent.ACTION_CANCEL:
249         default:
250             mSelectorY = mSelectorDefaultY;
251             mStopAnimation = true;
252             invalidate();
253             break;
254         }
255         return true;
256     }
257 
258     @Override
onDraw(Canvas canvas)259     protected void onDraw(Canvas canvas) {
260 
261         /* The bounds of the selector */
262         final int selectorLeft = 0;
263         final int selectorTop = mSelectorY;
264         final int selectorRight = getWidth();
265         final int selectorBottom = mSelectorY + mSelectorHeight;
266 
267         /* Draw the selector */
268         mSelector.setBounds(selectorLeft, selectorTop, selectorRight, selectorBottom);
269         mSelector.draw(canvas);
270 
271         if (mTextList == null) {
272 
273             /* We're not setup with values so don't draw anything else */
274             return;
275         }
276 
277         final TextPaint textPaintDark = mTextPaintDark;
278         if (hasFocus()) {
279 
280             /* The bounds of the top area where the text should be light */
281             final int topLeft = 0;
282             final int topTop = 0;
283             final int topRight = selectorRight;
284             final int topBottom = selectorTop + SELECTOR_ARROW_HEIGHT;
285 
286             /* Assign a bunch of local finals for performance */
287             final String text1 = mText1;
288             final String text2 = mText2;
289             final String text3 = mText3;
290             final String text4 = mText4;
291             final String text5 = mText5;
292             final TextPaint textPaintLight = mTextPaintLight;
293 
294             /*
295              * Draw the 1st, 2nd and 3rd item in light only, clip it so it only
296              * draws in the area above the selector
297              */
298             canvas.save();
299             canvas.clipRect(topLeft, topTop, topRight, topBottom);
300             drawText(canvas, text1, TEXT1_Y
301                     + mTotalAnimatedDistance, textPaintLight);
302             drawText(canvas, text2, TEXT2_Y
303                     + mTotalAnimatedDistance, textPaintLight);
304             drawText(canvas, text3,
305                     TEXT3_Y + mTotalAnimatedDistance, textPaintLight);
306             canvas.restore();
307 
308             /*
309              * Draw the 2nd, 3rd and 4th clipped to the selector bounds in dark
310              * paint
311              */
312             canvas.save();
313             canvas.clipRect(selectorLeft, selectorTop + SELECTOR_ARROW_HEIGHT,
314                     selectorRight, selectorBottom - SELECTOR_ARROW_HEIGHT);
315             drawText(canvas, text2, TEXT2_Y
316                     + mTotalAnimatedDistance, textPaintDark);
317             drawText(canvas, text3,
318                     TEXT3_Y + mTotalAnimatedDistance, textPaintDark);
319             drawText(canvas, text4,
320                     TEXT4_Y + mTotalAnimatedDistance, textPaintDark);
321             canvas.restore();
322 
323             /* The bounds of the bottom area where the text should be light */
324             final int bottomLeft = 0;
325             final int bottomTop = selectorBottom - SELECTOR_ARROW_HEIGHT;
326             final int bottomRight = selectorRight;
327             final int bottomBottom = getMeasuredHeight();
328 
329             /*
330              * Draw the 3rd, 4th and 5th in white text, clip it so it only draws
331              * in the area below the selector.
332              */
333             canvas.save();
334             canvas.clipRect(bottomLeft, bottomTop, bottomRight, bottomBottom);
335             drawText(canvas, text3,
336                     TEXT3_Y + mTotalAnimatedDistance, textPaintLight);
337             drawText(canvas, text4,
338                     TEXT4_Y + mTotalAnimatedDistance, textPaintLight);
339             drawText(canvas, text5,
340                     TEXT5_Y + mTotalAnimatedDistance, textPaintLight);
341             canvas.restore();
342 
343         } else {
344             drawText(canvas, mText3, TEXT3_Y, textPaintDark);
345         }
346         if (mIsAnimationRunning) {
347             if ((Math.abs(mTotalAnimatedDistance) + mDistanceOfEachAnimation) > SCROLL_DISTANCE) {
348                 mTotalAnimatedDistance = 0;
349                 if (mScrollMode == SCROLL_MODE_UP) {
350                     int oldPos = mCurrentSelectedPos;
351                     int newPos = getNewIndex(1);
352                     if (newPos >= 0) {
353                         mCurrentSelectedPos = newPos;
354                         if (mListener != null) {
355                             mListener.onChanged(this, oldPos, mCurrentSelectedPos, mTextList);
356                         }
357                     }
358                     if (newPos < 0 || ((newPos >= mTextList.length - 1) && !mWrapAround)) {
359                         mStopAnimation = true;
360                     }
361                     calculateTextPositions();
362                 } else if (mScrollMode == SCROLL_MODE_DOWN) {
363                     int oldPos = mCurrentSelectedPos;
364                     int newPos = getNewIndex(-1);
365                     if (newPos >= 0) {
366                         mCurrentSelectedPos = newPos;
367                         if (mListener != null) {
368                             mListener.onChanged(this, oldPos, mCurrentSelectedPos, mTextList);
369                         }
370                     }
371                     if (newPos < 0 || (newPos == 0 && !mWrapAround)) {
372                         mStopAnimation = true;
373                     }
374                     calculateTextPositions();
375                 }
376                 if (mStopAnimation) {
377                     final int previousScrollMode = mScrollMode;
378 
379                     /* No longer scrolling, we wait till the current animation
380                      * completes then we stop.
381                      */
382                     mIsAnimationRunning = false;
383                     mStopAnimation = false;
384                     mScrollMode = SCROLL_MODE_NONE;
385 
386                     /* If the current selected item is an empty string
387                      * scroll past it.
388                      */
389                     if ("".equals(mTextList[mCurrentSelectedPos])) {
390                        mScrollMode = previousScrollMode;
391                        scroll();
392                        mStopAnimation = true;
393                     }
394                 }
395             } else {
396                 if (mScrollMode == SCROLL_MODE_UP) {
397                     mTotalAnimatedDistance -= mDistanceOfEachAnimation;
398                 } else if (mScrollMode == SCROLL_MODE_DOWN) {
399                     mTotalAnimatedDistance += mDistanceOfEachAnimation;
400                 }
401             }
402             if (mDelayBetweenAnimations > 0) {
403                 postInvalidateDelayed(mDelayBetweenAnimations);
404             } else {
405                 invalidate();
406             }
407         }
408     }
409 
410     /**
411      * Called every time the text items or current position
412      * changes. We calculate store we don't have to calculate
413      * onDraw.
414      */
calculateTextPositions()415     private void calculateTextPositions() {
416         mText1 = getTextToDraw(-2);
417         mText2 = getTextToDraw(-1);
418         mText3 = getTextToDraw(0);
419         mText4 = getTextToDraw(1);
420         mText5 = getTextToDraw(2);
421     }
422 
getTextToDraw(int offset)423     private String getTextToDraw(int offset) {
424         int index = getNewIndex(offset);
425         if (index < 0) {
426             return "";
427         }
428         return mTextList[index];
429     }
430 
getNewIndex(int offset)431     private int getNewIndex(int offset) {
432         int index = mCurrentSelectedPos + offset;
433         if (index < 0) {
434             if (mWrapAround) {
435                 index += mTextList.length;
436             } else {
437                 return -1;
438             }
439         } else if (index >= mTextList.length) {
440             if (mWrapAround) {
441                 index -= mTextList.length;
442             } else {
443                 return -1;
444             }
445         }
446         return index;
447     }
448 
scroll()449     private void scroll() {
450         if (mIsAnimationRunning) {
451             return;
452         }
453         mTotalAnimatedDistance = 0;
454         mIsAnimationRunning = true;
455         invalidate();
456     }
457 
calculateAnimationValues()458     private void calculateAnimationValues() {
459         mNumberOfAnimations = (int) mScrollInterval / SCROLL_DISTANCE;
460         if (mNumberOfAnimations < MIN_ANIMATIONS) {
461             mNumberOfAnimations = MIN_ANIMATIONS;
462             mDistanceOfEachAnimation = SCROLL_DISTANCE / mNumberOfAnimations;
463             mDelayBetweenAnimations = 0;
464         } else {
465             mDistanceOfEachAnimation = SCROLL_DISTANCE / mNumberOfAnimations;
466             mDelayBetweenAnimations = mScrollInterval / mNumberOfAnimations;
467         }
468     }
469 
drawText(Canvas canvas, String text, int y, TextPaint paint)470     private void drawText(Canvas canvas, String text, int y, TextPaint paint) {
471         int width = (int) paint.measureText(text);
472         int x = getMeasuredWidth() - width - TEXT_MARGIN_RIGHT;
473         canvas.drawText(text, x, y, paint);
474     }
475 
getCurrentSelectedPos()476     public int getCurrentSelectedPos() {
477         return mCurrentSelectedPos;
478     }
479 }
480