Home | History | Annotate | Download | only in music
      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 {
     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