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
17package com.android.music;
18
19import android.content.Context;
20import android.graphics.Canvas;
21import android.graphics.Paint;
22import android.graphics.Rect;
23import android.graphics.drawable.Drawable;
24import android.graphics.drawable.NinePatchDrawable;
25import android.text.TextPaint;
26import android.util.AttributeSet;
27import android.util.Log;
28import android.view.KeyEvent;
29import android.view.MotionEvent;
30import android.view.View;
31
32public 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 {
88        void onChanged(VerticalTextSpinner spinner, int oldPos, int newPos, String[] items);
89    }
90
91    public VerticalTextSpinner(Context context) {
92        this(context, null);
93    }
94
95    public VerticalTextSpinner(Context context, AttributeSet attrs) {
96        this(context, attrs, 0);
97    }
98
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
140    public void setOnChangeListener(OnChangedListener listener) {
141        mListener = listener;
142    }
143
144    public void setItems(String[] textList) {
145        mTextList = textList;
146        calculateTextPositions();
147    }
148
149    public void setSelectedPos(int selectedPos) {
150        mCurrentSelectedPos = selectedPos;
151        calculateTextPositions();
152        postInvalidate();
153    }
154
155    public void setScrollInterval(long interval) {
156        mScrollInterval = interval;
157        calculateAnimationValues();
158    }
159
160    public void setWrapAround(boolean wrap) {
161        mWrapAround = wrap;
162    }
163
164    @Override
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
184    private boolean canScrollDown() {
185        return (mCurrentSelectedPos > 0) || mWrapAround;
186    }
187
188    private boolean canScrollUp() {
189        return ((mCurrentSelectedPos < (mTextList.length - 1)) || mWrapAround);
190    }
191
192    @Override
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
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
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     */
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
405    private String getTextToDraw(int offset) {
406        int index = getNewIndex(offset);
407        if (index < 0) {
408            return "";
409        }
410        return mTextList[index];
411    }
412
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
431    private void scroll() {
432        if (mIsAnimationRunning) {
433            return;
434        }
435        mTotalAnimatedDistance = 0;
436        mIsAnimationRunning = true;
437        invalidate();
438    }
439
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
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
458    public int getCurrentSelectedPos() {
459        return mCurrentSelectedPos;
460    }
461}
462