/* * 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.internal.widget; 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; } }