--- /dev/null
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.music;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.text.TextPaint;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+
+
+public class VerticalTextSpinner extends View {
+
+ private static final int SELECTOR_ARROW_HEIGHT = 15;
+
+ private static final int TEXT_SPACING = 18;
+ private static final int TEXT_MARGIN_RIGHT = 25;
+ private static final int TEXT_SIZE = 22;
+
+ /* Keep the calculations as this is really a for loop from
+ * -2 to 2 but precalculated so we don't have to do in the onDraw.
+ */
+ private static final int TEXT1_Y = (TEXT_SIZE * (-2 + 2)) + (TEXT_SPACING * (-2 + 1));
+ private static final int TEXT2_Y = (TEXT_SIZE * (-1 + 2)) + (TEXT_SPACING * (-1 + 1));
+ private static final int TEXT3_Y = (TEXT_SIZE * (0 + 2)) + (TEXT_SPACING * (0 + 1));
+ private static final int TEXT4_Y = (TEXT_SIZE * (1 + 2)) + (TEXT_SPACING * (1 + 1));
+ private static final int TEXT5_Y = (TEXT_SIZE * (2 + 2)) + (TEXT_SPACING * (2 + 1));
+
+ private static final int SCROLL_MODE_NONE = 0;
+ private static final int SCROLL_MODE_UP = 1;
+ private static final int SCROLL_MODE_DOWN = 2;
+
+ private static final long DEFAULT_SCROLL_INTERVAL_MS = 400;
+ private static final int SCROLL_DISTANCE = TEXT_SIZE + TEXT_SPACING;
+ private static final int MIN_ANIMATIONS = 4;
+
+ private final Drawable mBackgroundFocused;
+ private final Drawable mSelectorFocused;
+ private final Drawable mSelectorNormal;
+ private final int mSelectorDefaultY;
+ private final int mSelectorMinY;
+ private final int mSelectorMaxY;
+ private final int mSelectorHeight;
+ private final TextPaint mTextPaintDark;
+ private final TextPaint mTextPaintLight;
+
+ private int mSelectorY;
+ private Drawable mSelector;
+ private int mDownY;
+ private boolean isDraggingSelector;
+ private int mScrollMode;
+ private long mScrollInterval;
+ private boolean mIsAnimationRunning;
+ private boolean mStopAnimation;
+ private boolean mWrapAround = true;
+
+ private int mTotalAnimatedDistance;
+ private int mNumberOfAnimations;
+ private long mDelayBetweenAnimations;
+ private int mDistanceOfEachAnimation;
+
+ private String[] mTextList;
+ private int mCurrentSelectedPos;
+ private OnChangedListener mListener;
+
+ private String mText1;
+ private String mText2;
+ private String mText3;
+ private String mText4;
+ private String mText5;
+
+ public interface OnChangedListener {
+ void onChanged(
+ VerticalTextSpinner spinner, int oldPos, int newPos, String[] items);
+ }
+
+ public VerticalTextSpinner(Context context) {
+ this(context, null);
+ }
+
+ public VerticalTextSpinner(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public VerticalTextSpinner(Context context, AttributeSet attrs,
+ int defStyle) {
+ super(context, attrs, defStyle);
+
+ mBackgroundFocused = context.getResources().getDrawable(com.android.internal.R.drawable.pickerbox_background);
+ mSelectorFocused = context.getResources().getDrawable(com.android.internal.R.drawable.pickerbox_selected);
+ mSelectorNormal = context.getResources().getDrawable(com.android.internal.R.drawable.pickerbox_unselected);
+
+ mSelectorHeight = mSelectorFocused.getIntrinsicHeight();
+ mSelectorDefaultY = (mBackgroundFocused.getIntrinsicHeight() - mSelectorHeight) / 2;
+ mSelectorMinY = 0;
+ mSelectorMaxY = mBackgroundFocused.getIntrinsicHeight() - mSelectorHeight;
+
+ mSelector = mSelectorNormal;
+ mSelectorY = mSelectorDefaultY;
+
+ mTextPaintDark = new TextPaint(Paint.ANTI_ALIAS_FLAG);
+ mTextPaintDark.setTextSize(TEXT_SIZE);
+ mTextPaintDark.setColor(context.getResources().getColor(com.android.internal.R.color.primary_text_light));
+
+ mTextPaintLight = new TextPaint(Paint.ANTI_ALIAS_FLAG);
+ mTextPaintLight.setTextSize(TEXT_SIZE);
+ mTextPaintLight.setColor(context.getResources().getColor(com.android.internal.R.color.secondary_text_dark));
+
+ mScrollMode = SCROLL_MODE_NONE;
+ mScrollInterval = DEFAULT_SCROLL_INTERVAL_MS;
+ calculateAnimationValues();
+ }
+
+ public void setOnChangeListener(OnChangedListener listener) {
+ mListener = listener;
+ }
+
+ public void setItems(String[] textList) {
+ mTextList = textList;
+ calculateTextPositions();
+ }
+
+ public void setSelectedPos(int selectedPos) {
+ mCurrentSelectedPos = selectedPos;
+ calculateTextPositions();
+ postInvalidate();
+ }
+
+ public void setScrollInterval(long interval) {
+ mScrollInterval = interval;
+ calculateAnimationValues();
+ }
+
+ public void setWrapAround(boolean wrap) {
+ mWrapAround = wrap;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+
+ /* This is a bit confusing, when we get the key event
+ * DPAD_DOWN we actually roll the spinner up. When the
+ * key event is DPAD_UP we roll the spinner down.
+ */
+ if ((keyCode == KeyEvent.KEYCODE_DPAD_UP) && canScrollDown()) {
+ mScrollMode = SCROLL_MODE_DOWN;
+ scroll();
+ mStopAnimation = true;
+ return true;
+ } else if ((keyCode == KeyEvent.KEYCODE_DPAD_DOWN) && canScrollUp()) {
+ mScrollMode = SCROLL_MODE_UP;
+ scroll();
+ mStopAnimation = true;
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ private boolean canScrollDown() {
+ return (mCurrentSelectedPos > 0) || mWrapAround;
+ }
+
+ private boolean canScrollUp() {
+ return ((mCurrentSelectedPos < (mTextList.length - 1)) || mWrapAround);
+ }
+
+ @Override
+ protected void onFocusChanged(boolean gainFocus, int direction,
+ Rect previouslyFocusedRect) {
+ if (gainFocus) {
+ setBackgroundDrawable(mBackgroundFocused);
+ mSelector = mSelectorFocused;
+ } else {
+ setBackgroundDrawable(null);
+ mSelector = mSelectorNormal;
+ mSelectorY = mSelectorDefaultY;
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ final int action = event.getAction();
+ final int y = (int) event.getY();
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ requestFocus();
+ mDownY = y;
+ isDraggingSelector = (y >= mSelectorY) && (y <= (mSelectorY + mSelector.getIntrinsicHeight()));
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ if (isDraggingSelector) {
+ int top = mSelectorDefaultY + (y - mDownY);
+ if (top <= mSelectorMinY && canScrollDown()) {
+ mSelectorY = mSelectorMinY;
+ mStopAnimation = false;
+ if (mScrollMode != SCROLL_MODE_DOWN) {
+ mScrollMode = SCROLL_MODE_DOWN;
+ scroll();
+ }
+ } else if (top >= mSelectorMaxY && canScrollUp()) {
+ mSelectorY = mSelectorMaxY;
+ mStopAnimation = false;
+ if (mScrollMode != SCROLL_MODE_UP) {
+ mScrollMode = SCROLL_MODE_UP;
+ scroll();
+ }
+ } else {
+ mSelectorY = top;
+ mStopAnimation = true;
+ }
+ }
+ break;
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ default:
+ mSelectorY = mSelectorDefaultY;
+ mStopAnimation = true;
+ invalidate();
+ break;
+ }
+ return true;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+
+ /* The bounds of the selector */
+ final int selectorLeft = 0;
+ final int selectorTop = mSelectorY;
+ final int selectorRight = mMeasuredWidth;
+ final int selectorBottom = mSelectorY + mSelectorHeight;
+
+ /* Draw the selector */
+ mSelector.setBounds(selectorLeft, selectorTop, selectorRight, selectorBottom);
+ mSelector.draw(canvas);
+
+ if (mTextList == null) {
+
+ /* We're not setup with values so don't draw anything else */
+ return;
+ }
+
+ final TextPaint textPaintDark = mTextPaintDark;
+ if (hasFocus()) {
+
+ /* The bounds of the top area where the text should be light */
+ final int topLeft = 0;
+ final int topTop = 0;
+ final int topRight = selectorRight;
+ final int topBottom = selectorTop + SELECTOR_ARROW_HEIGHT;
+
+ /* Assign a bunch of local finals for performance */
+ final String text1 = mText1;
+ final String text2 = mText2;
+ final String text3 = mText3;
+ final String text4 = mText4;
+ final String text5 = mText5;
+ final TextPaint textPaintLight = mTextPaintLight;
+
+ /*
+ * Draw the 1st, 2nd and 3rd item in light only, clip it so it only
+ * draws in the area above the selector
+ */
+ canvas.save();
+ canvas.clipRect(topLeft, topTop, topRight, topBottom);
+ drawText(canvas, text1, TEXT1_Y
+ + mTotalAnimatedDistance, textPaintLight);
+ drawText(canvas, text2, TEXT2_Y
+ + mTotalAnimatedDistance, textPaintLight);
+ drawText(canvas, text3,
+ TEXT3_Y + mTotalAnimatedDistance, textPaintLight);
+ canvas.restore();
+
+ /*
+ * Draw the 2nd, 3rd and 4th clipped to the selector bounds in dark
+ * paint
+ */
+ canvas.save();
+ canvas.clipRect(selectorLeft, selectorTop + SELECTOR_ARROW_HEIGHT,
+ selectorRight, selectorBottom - SELECTOR_ARROW_HEIGHT);
+ drawText(canvas, text2, TEXT2_Y
+ + mTotalAnimatedDistance, textPaintDark);
+ drawText(canvas, text3,
+ TEXT3_Y + mTotalAnimatedDistance, textPaintDark);
+ drawText(canvas, text4,
+ TEXT4_Y + mTotalAnimatedDistance, textPaintDark);
+ canvas.restore();
+
+ /* The bounds of the bottom area where the text should be light */
+ final int bottomLeft = 0;
+ final int bottomTop = selectorBottom - SELECTOR_ARROW_HEIGHT;
+ final int bottomRight = selectorRight;
+ final int bottomBottom = mMeasuredHeight;
+
+ /*
+ * Draw the 3rd, 4th and 5th in white text, clip it so it only draws
+ * in the area below the selector.
+ */
+ canvas.save();
+ canvas.clipRect(bottomLeft, bottomTop, bottomRight, bottomBottom);
+ drawText(canvas, text3,
+ TEXT3_Y + mTotalAnimatedDistance, textPaintLight);
+ drawText(canvas, text4,
+ TEXT4_Y + mTotalAnimatedDistance, textPaintLight);
+ drawText(canvas, text5,
+ TEXT5_Y + mTotalAnimatedDistance, textPaintLight);
+ canvas.restore();
+
+ } else {
+ drawText(canvas, mText3, TEXT3_Y, textPaintDark);
+ }
+ if (mIsAnimationRunning) {
+ if ((Math.abs(mTotalAnimatedDistance) + mDistanceOfEachAnimation) > SCROLL_DISTANCE) {
+ mTotalAnimatedDistance = 0;
+ if (mScrollMode == SCROLL_MODE_UP) {
+ int oldPos = mCurrentSelectedPos;
+ int newPos = getNewIndex(1);
+ if (newPos >= 0) {
+ mCurrentSelectedPos = newPos;
+ if (mListener != null) {
+ mListener.onChanged(this, oldPos, mCurrentSelectedPos, mTextList);
+ }
+ }
+ if (newPos < 0 || ((newPos >= mTextList.length - 1) && !mWrapAround)) {
+ mStopAnimation = true;
+ }
+ calculateTextPositions();
+ } else if (mScrollMode == SCROLL_MODE_DOWN) {
+ int oldPos = mCurrentSelectedPos;
+ int newPos = getNewIndex(-1);
+ if (newPos >= 0) {
+ mCurrentSelectedPos = newPos;
+ if (mListener != null) {
+ mListener.onChanged(this, oldPos, mCurrentSelectedPos, mTextList);
+ }
+ }
+ if (newPos < 0 || (newPos == 0 && !mWrapAround)) {
+ mStopAnimation = true;
+ }
+ calculateTextPositions();
+ }
+ if (mStopAnimation) {
+ final int previousScrollMode = mScrollMode;
+
+ /* No longer scrolling, we wait till the current animation
+ * completes then we stop.
+ */
+ mIsAnimationRunning = false;
+ mStopAnimation = false;
+ mScrollMode = SCROLL_MODE_NONE;
+
+ /* If the current selected item is an empty string
+ * scroll past it.
+ */
+ if ("".equals(mTextList[mCurrentSelectedPos])) {
+ mScrollMode = previousScrollMode;
+ scroll();
+ mStopAnimation = true;
+ }
+ }
+ } else {
+ if (mScrollMode == SCROLL_MODE_UP) {
+ mTotalAnimatedDistance -= mDistanceOfEachAnimation;
+ } else if (mScrollMode == SCROLL_MODE_DOWN) {
+ mTotalAnimatedDistance += mDistanceOfEachAnimation;
+ }
+ }
+ if (mDelayBetweenAnimations > 0) {
+ postInvalidateDelayed(mDelayBetweenAnimations);
+ } else {
+ invalidate();
+ }
+ }
+ }
+
+ /**
+ * Called every time the text items or current position
+ * changes. We calculate store we don't have to calculate
+ * onDraw.
+ */
+ private void calculateTextPositions() {
+ mText1 = getTextToDraw(-2);
+ mText2 = getTextToDraw(-1);
+ mText3 = getTextToDraw(0);
+ mText4 = getTextToDraw(1);
+ mText5 = getTextToDraw(2);
+ }
+
+ private String getTextToDraw(int offset) {
+ int index = getNewIndex(offset);
+ if (index < 0) {
+ return "";
+ }
+ return mTextList[index];
+ }
+
+ private int getNewIndex(int offset) {
+ int index = mCurrentSelectedPos + offset;
+ if (index < 0) {
+ if (mWrapAround) {
+ index += mTextList.length;
+ } else {
+ return -1;
+ }
+ } else if (index >= mTextList.length) {
+ if (mWrapAround) {
+ index -= mTextList.length;
+ } else {
+ return -1;
+ }
+ }
+ return index;
+ }
+
+ private void scroll() {
+ if (mIsAnimationRunning) {
+ return;
+ }
+ mTotalAnimatedDistance = 0;
+ mIsAnimationRunning = true;
+ invalidate();
+ }
+
+ private void calculateAnimationValues() {
+ mNumberOfAnimations = (int) mScrollInterval / SCROLL_DISTANCE;
+ if (mNumberOfAnimations < MIN_ANIMATIONS) {
+ mNumberOfAnimations = MIN_ANIMATIONS;
+ mDistanceOfEachAnimation = SCROLL_DISTANCE / mNumberOfAnimations;
+ mDelayBetweenAnimations = 0;
+ } else {
+ mDistanceOfEachAnimation = SCROLL_DISTANCE / mNumberOfAnimations;
+ mDelayBetweenAnimations = mScrollInterval / mNumberOfAnimations;
+ }
+ }
+
+ private void drawText(Canvas canvas, String text, int y, TextPaint paint) {
+ int width = (int) paint.measureText(text);
+ int x = getMeasuredWidth() - width - TEXT_MARGIN_RIGHT;
+ canvas.drawText(text, x, y, paint);
+ }
+
+ public int getCurrentSelectedPos() {
+ return mCurrentSelectedPos;
+ }
+}