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 
     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 {
     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