2 * Copyright (C) 2008 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.music;
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.text.TextPaint;
25 import android.util.AttributeSet;
26 import android.view.KeyEvent;
27 import android.view.MotionEvent;
28 import android.view.View;
31 public class VerticalTextSpinner extends View {
33 private static final int SELECTOR_ARROW_HEIGHT = 15;
35 private static final int TEXT_SPACING = 18;
36 private static final int TEXT_MARGIN_RIGHT = 25;
37 private static final int TEXT_SIZE = 22;
39 /* Keep the calculations as this is really a for loop from
40 * -2 to 2 but precalculated so we don't have to do in the onDraw.
42 private static final int TEXT1_Y = (TEXT_SIZE * (-2 + 2)) + (TEXT_SPACING * (-2 + 1));
43 private static final int TEXT2_Y = (TEXT_SIZE * (-1 + 2)) + (TEXT_SPACING * (-1 + 1));
44 private static final int TEXT3_Y = (TEXT_SIZE * (0 + 2)) + (TEXT_SPACING * (0 + 1));
45 private static final int TEXT4_Y = (TEXT_SIZE * (1 + 2)) + (TEXT_SPACING * (1 + 1));
46 private static final int TEXT5_Y = (TEXT_SIZE * (2 + 2)) + (TEXT_SPACING * (2 + 1));
48 private static final int SCROLL_MODE_NONE = 0;
49 private static final int SCROLL_MODE_UP = 1;
50 private static final int SCROLL_MODE_DOWN = 2;
52 private static final long DEFAULT_SCROLL_INTERVAL_MS = 400;
53 private static final int SCROLL_DISTANCE = TEXT_SIZE + TEXT_SPACING;
54 private static final int MIN_ANIMATIONS = 4;
56 private final Drawable mBackgroundFocused;
57 private final Drawable mSelectorFocused;
58 private final Drawable mSelectorNormal;
59 private final int mSelectorDefaultY;
60 private final int mSelectorMinY;
61 private final int mSelectorMaxY;
62 private final int mSelectorHeight;
63 private final TextPaint mTextPaintDark;
64 private final TextPaint mTextPaintLight;
66 private int mSelectorY;
67 private Drawable mSelector;
69 private boolean isDraggingSelector;
70 private int mScrollMode;
71 private long mScrollInterval;
72 private boolean mIsAnimationRunning;
73 private boolean mStopAnimation;
74 private boolean mWrapAround = true;
76 private int mTotalAnimatedDistance;
77 private int mNumberOfAnimations;
78 private long mDelayBetweenAnimations;
79 private int mDistanceOfEachAnimation;
81 private String[] mTextList;
82 private int mCurrentSelectedPos;
83 private OnChangedListener mListener;
85 private String mText1;
86 private String mText2;
87 private String mText3;
88 private String mText4;
89 private String mText5;
91 public interface OnChangedListener {
93 VerticalTextSpinner spinner, int oldPos, int newPos, String[] items);
96 public VerticalTextSpinner(Context context) {
100 public VerticalTextSpinner(Context context, AttributeSet attrs) {
101 this(context, attrs, 0);
104 public VerticalTextSpinner(Context context, AttributeSet attrs,
106 super(context, attrs, defStyle);
108 mBackgroundFocused = context.getResources().getDrawable(R.drawable.pickerbox_background);
109 mSelectorFocused = context.getResources().getDrawable(R.drawable.pickerbox_selected);
110 mSelectorNormal = context.getResources().getDrawable(R.drawable.pickerbox_unselected);
112 mSelectorHeight = mSelectorFocused.getIntrinsicHeight();
113 mSelectorDefaultY = (mBackgroundFocused.getIntrinsicHeight() - mSelectorHeight) / 2;
115 mSelectorMaxY = mBackgroundFocused.getIntrinsicHeight() - mSelectorHeight;
117 mSelector = mSelectorNormal;
118 mSelectorY = mSelectorDefaultY;
120 mTextPaintDark = new TextPaint(Paint.ANTI_ALIAS_FLAG);
121 mTextPaintDark.setTextSize(TEXT_SIZE);
122 mTextPaintDark.setColor(context.getResources()
123 .getColor(android.R.color.primary_text_light));
125 mTextPaintLight = new TextPaint(Paint.ANTI_ALIAS_FLAG);
126 mTextPaintLight.setTextSize(TEXT_SIZE);
127 mTextPaintLight.setColor(context.getResources()
128 .getColor(android.R.color.secondary_text_dark));
130 mScrollMode = SCROLL_MODE_NONE;
131 mScrollInterval = DEFAULT_SCROLL_INTERVAL_MS;
132 calculateAnimationValues();
135 public void setOnChangeListener(OnChangedListener listener) {
136 mListener = listener;
139 public void setItems(String[] textList) {
140 mTextList = textList;
141 calculateTextPositions();
144 public void setSelectedPos(int selectedPos) {
145 mCurrentSelectedPos = selectedPos;
146 calculateTextPositions();
150 public void setScrollInterval(long interval) {
151 mScrollInterval = interval;
152 calculateAnimationValues();
155 public void setWrapAround(boolean wrap) {
160 public boolean onKeyDown(int keyCode, KeyEvent event) {
162 /* This is a bit confusing, when we get the key event
163 * DPAD_DOWN we actually roll the spinner up. When the
164 * key event is DPAD_UP we roll the spinner down.
166 if ((keyCode == KeyEvent.KEYCODE_DPAD_UP) && canScrollDown()) {
167 mScrollMode = SCROLL_MODE_DOWN;
169 mStopAnimation = true;
171 } else if ((keyCode == KeyEvent.KEYCODE_DPAD_DOWN) && canScrollUp()) {
172 mScrollMode = SCROLL_MODE_UP;
174 mStopAnimation = true;
177 return super.onKeyDown(keyCode, event);
180 private boolean canScrollDown() {
181 return (mCurrentSelectedPos > 0) || mWrapAround;
184 private boolean canScrollUp() {
185 return ((mCurrentSelectedPos < (mTextList.length - 1)) || mWrapAround);
189 protected void onFocusChanged(boolean gainFocus, int direction,
190 Rect previouslyFocusedRect) {
192 setBackgroundDrawable(mBackgroundFocused);
193 mSelector = mSelectorFocused;
195 setBackgroundDrawable(null);
196 mSelector = mSelectorNormal;
197 mSelectorY = mSelectorDefaultY;
202 public boolean onTouchEvent(MotionEvent event) {
203 final int action = event.getAction();
204 final int y = (int) event.getY();
207 case MotionEvent.ACTION_DOWN:
210 isDraggingSelector = (y >= mSelectorY) &&
211 (y <= (mSelectorY + mSelector.getIntrinsicHeight()));
214 case MotionEvent.ACTION_MOVE:
215 if (isDraggingSelector) {
216 int top = mSelectorDefaultY + (y - mDownY);
217 if (top <= mSelectorMinY && canScrollDown()) {
218 mSelectorY = mSelectorMinY;
219 mStopAnimation = false;
220 if (mScrollMode != SCROLL_MODE_DOWN) {
221 mScrollMode = SCROLL_MODE_DOWN;
224 } else if (top >= mSelectorMaxY && canScrollUp()) {
225 mSelectorY = mSelectorMaxY;
226 mStopAnimation = false;
227 if (mScrollMode != SCROLL_MODE_UP) {
228 mScrollMode = SCROLL_MODE_UP;
233 mStopAnimation = true;
238 case MotionEvent.ACTION_UP:
239 case MotionEvent.ACTION_CANCEL:
241 mSelectorY = mSelectorDefaultY;
242 mStopAnimation = true;
250 protected void onDraw(Canvas canvas) {
252 /* The bounds of the selector */
253 final int selectorLeft = 0;
254 final int selectorTop = mSelectorY;
255 final int selectorRight = getMeasuredWidth();
256 final int selectorBottom = mSelectorY + mSelectorHeight;
258 /* Draw the selector */
259 mSelector.setBounds(selectorLeft, selectorTop, selectorRight, selectorBottom);
260 mSelector.draw(canvas);
262 if (mTextList == null) {
264 /* We're not setup with values so don't draw anything else */
268 final TextPaint textPaintDark = mTextPaintDark;
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;
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;
286 * Draw the 1st, 2nd and 3rd item in light only, clip it so it only
287 * draws in the area above the selector
290 canvas.clipRect(topLeft, topTop, topRight, topBottom);
291 drawText(canvas, text1, TEXT1_Y
292 + mTotalAnimatedDistance, textPaintLight);
293 drawText(canvas, text2, TEXT2_Y
294 + mTotalAnimatedDistance, textPaintLight);
295 drawText(canvas, text3,
296 TEXT3_Y + mTotalAnimatedDistance, textPaintLight);
300 * Draw the 2nd, 3rd and 4th clipped to the selector bounds in dark
304 canvas.clipRect(selectorLeft, selectorTop + SELECTOR_ARROW_HEIGHT,
305 selectorRight, selectorBottom - SELECTOR_ARROW_HEIGHT);
306 drawText(canvas, text2, TEXT2_Y
307 + mTotalAnimatedDistance, textPaintDark);
308 drawText(canvas, text3,
309 TEXT3_Y + mTotalAnimatedDistance, textPaintDark);
310 drawText(canvas, text4,
311 TEXT4_Y + mTotalAnimatedDistance, textPaintDark);
314 /* The bounds of the bottom area where the text should be light */
315 final int bottomLeft = 0;
316 final int bottomTop = selectorBottom - SELECTOR_ARROW_HEIGHT;
317 final int bottomRight = selectorRight;
318 final int bottomBottom = getMeasuredHeight();
321 * Draw the 3rd, 4th and 5th in white text, clip it so it only draws
322 * in the area below the selector.
325 canvas.clipRect(bottomLeft, bottomTop, bottomRight, bottomBottom);
326 drawText(canvas, text3,
327 TEXT3_Y + mTotalAnimatedDistance, textPaintLight);
328 drawText(canvas, text4,
329 TEXT4_Y + mTotalAnimatedDistance, textPaintLight);
330 drawText(canvas, text5,
331 TEXT5_Y + mTotalAnimatedDistance, textPaintLight);
335 drawText(canvas, mText3, TEXT3_Y, textPaintDark);
337 if (mIsAnimationRunning) {
338 if ((Math.abs(mTotalAnimatedDistance) + mDistanceOfEachAnimation) > SCROLL_DISTANCE) {
339 mTotalAnimatedDistance = 0;
340 if (mScrollMode == SCROLL_MODE_UP) {
341 int oldPos = mCurrentSelectedPos;
342 int newPos = getNewIndex(1);
344 mCurrentSelectedPos = newPos;
345 if (mListener != null) {
346 mListener.onChanged(this, oldPos, mCurrentSelectedPos, mTextList);
349 if (newPos < 0 || ((newPos >= mTextList.length - 1) && !mWrapAround)) {
350 mStopAnimation = true;
352 calculateTextPositions();
353 } else if (mScrollMode == SCROLL_MODE_DOWN) {
354 int oldPos = mCurrentSelectedPos;
355 int newPos = getNewIndex(-1);
357 mCurrentSelectedPos = newPos;
358 if (mListener != null) {
359 mListener.onChanged(this, oldPos, mCurrentSelectedPos, mTextList);
362 if (newPos < 0 || (newPos == 0 && !mWrapAround)) {
363 mStopAnimation = true;
365 calculateTextPositions();
367 if (mStopAnimation) {
368 final int previousScrollMode = mScrollMode;
370 /* No longer scrolling, we wait till the current animation
371 * completes then we stop.
373 mIsAnimationRunning = false;
374 mStopAnimation = false;
375 mScrollMode = SCROLL_MODE_NONE;
377 /* If the current selected item is an empty string
380 if ("".equals(mTextList[mCurrentSelectedPos])) {
381 mScrollMode = previousScrollMode;
383 mStopAnimation = true;
387 if (mScrollMode == SCROLL_MODE_UP) {
388 mTotalAnimatedDistance -= mDistanceOfEachAnimation;
389 } else if (mScrollMode == SCROLL_MODE_DOWN) {
390 mTotalAnimatedDistance += mDistanceOfEachAnimation;
393 if (mDelayBetweenAnimations > 0) {
394 postInvalidateDelayed(mDelayBetweenAnimations);
402 * Called every time the text items or current position
403 * changes. We calculate store we don't have to calculate
406 private void calculateTextPositions() {
407 mText1 = getTextToDraw(-2);
408 mText2 = getTextToDraw(-1);
409 mText3 = getTextToDraw(0);
410 mText4 = getTextToDraw(1);
411 mText5 = getTextToDraw(2);
414 private String getTextToDraw(int offset) {
415 int index = getNewIndex(offset);
419 return mTextList[index];
422 private int getNewIndex(int offset) {
423 int index = mCurrentSelectedPos + offset;
426 index += mTextList.length;
430 } else if (index >= mTextList.length) {
432 index -= mTextList.length;
440 private void scroll() {
441 if (mIsAnimationRunning) {
444 mTotalAnimatedDistance = 0;
445 mIsAnimationRunning = true;
449 private void calculateAnimationValues() {
450 mNumberOfAnimations = (int) mScrollInterval / SCROLL_DISTANCE;
451 if (mNumberOfAnimations < MIN_ANIMATIONS) {
452 mNumberOfAnimations = MIN_ANIMATIONS;
453 mDistanceOfEachAnimation = SCROLL_DISTANCE / mNumberOfAnimations;
454 mDelayBetweenAnimations = 0;
456 mDistanceOfEachAnimation = SCROLL_DISTANCE / mNumberOfAnimations;
457 mDelayBetweenAnimations = mScrollInterval / mNumberOfAnimations;
461 private void drawText(Canvas canvas, String text, int y, TextPaint paint) {
462 int width = (int) paint.measureText(text);
463 int x = getMeasuredWidth() - width - TEXT_MARGIN_RIGHT;
464 canvas.drawText(text, x, y, paint);
467 public int getCurrentSelectedPos() {
468 return mCurrentSelectedPos;