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