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
32
33public 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 {
90        void onChanged(
91                VerticalTextSpinner spinner, int oldPos, int newPos, String[] items);
92    }
93
94    public VerticalTextSpinner(Context context) {
95        this(context, null);
96    }
97
98    public VerticalTextSpinner(Context context, AttributeSet attrs) {
99        this(context, attrs, 0);
100    }
101
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
144    public void setOnChangeListener(OnChangedListener listener) {
145        mListener = listener;
146    }
147
148    public void setItems(String[] textList) {
149        mTextList = textList;
150        calculateTextPositions();
151    }
152
153    public void setSelectedPos(int selectedPos) {
154        mCurrentSelectedPos = selectedPos;
155        calculateTextPositions();
156        postInvalidate();
157    }
158
159    public void setScrollInterval(long interval) {
160        mScrollInterval = interval;
161        calculateAnimationValues();
162    }
163
164    public void setWrapAround(boolean wrap) {
165        mWrapAround = wrap;
166    }
167
168    @Override
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
189    private boolean canScrollDown() {
190        return (mCurrentSelectedPos > 0) || mWrapAround;
191    }
192
193    private boolean canScrollUp() {
194        return ((mCurrentSelectedPos < (mTextList.length - 1)) || mWrapAround);
195    }
196
197    @Override
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
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
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     */
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
423    private String getTextToDraw(int offset) {
424        int index = getNewIndex(offset);
425        if (index < 0) {
426            return "";
427        }
428        return mTextList[index];
429    }
430
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
449    private void scroll() {
450        if (mIsAnimationRunning) {
451            return;
452        }
453        mTotalAnimatedDistance = 0;
454        mIsAnimationRunning = true;
455        invalidate();
456    }
457
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
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
476    public int getCurrentSelectedPos() {
477        return mCurrentSelectedPos;
478    }
479}
480