blob: e599eebe018b0134cad6ff38f3b934c0e8cb2d26 [file] [log] [blame]
Marco Nelissen51f94bf2010-01-05 14:13:21 -08001/*
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
17package com.android.music;
18
19import android.content.Context;
20import android.graphics.Canvas;
21import android.graphics.Paint;
22import android.graphics.Rect;
23import android.graphics.drawable.Drawable;
Marco Nelissen660b8fc2010-01-12 13:58:12 -080024import android.graphics.drawable.NinePatchDrawable;
Marco Nelissen51f94bf2010-01-05 14:13:21 -080025import android.text.TextPaint;
26import android.util.AttributeSet;
Marco Nelissen660b8fc2010-01-12 13:58:12 -080027import android.util.Log;
Marco Nelissen51f94bf2010-01-05 14:13:21 -080028import android.view.KeyEvent;
29import android.view.MotionEvent;
30import android.view.View;
31
32
33public class VerticalTextSpinner extends View {
34
35 private static final int SELECTOR_ARROW_HEIGHT = 15;
Marco Nelissend16985c2010-01-05 16:39:21 -080036
Marco Nelissen660b8fc2010-01-12 13:58:12 -080037 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;
Marco Nelissend16985c2010-01-05 16:39:21 -080046
Marco Nelissen51f94bf2010-01-05 14:13:21 -080047 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;
Marco Nelissend16985c2010-01-05 16:39:21 -080050
Marco Nelissen51f94bf2010-01-05 14:13:21 -080051 private static final long DEFAULT_SCROLL_INTERVAL_MS = 400;
Marco Nelissen51f94bf2010-01-05 14:13:21 -080052 private static final int MIN_ANIMATIONS = 4;
Marco Nelissend16985c2010-01-05 16:39:21 -080053
Marco Nelissen51f94bf2010-01-05 14:13:21 -080054 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;
Marco Nelissend16985c2010-01-05 16:39:21 -080063
Marco Nelissen51f94bf2010-01-05 14:13:21 -080064 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;
Marco Nelissend16985c2010-01-05 16:39:21 -080073
Marco Nelissen51f94bf2010-01-05 14:13:21 -080074 private int mTotalAnimatedDistance;
75 private int mNumberOfAnimations;
76 private long mDelayBetweenAnimations;
77 private int mDistanceOfEachAnimation;
Marco Nelissend16985c2010-01-05 16:39:21 -080078
Marco Nelissen51f94bf2010-01-05 14:13:21 -080079 private String[] mTextList;
80 private int mCurrentSelectedPos;
81 private OnChangedListener mListener;
Marco Nelissend16985c2010-01-05 16:39:21 -080082
Marco Nelissen51f94bf2010-01-05 14:13:21 -080083 private String mText1;
84 private String mText2;
85 private String mText3;
86 private String mText4;
87 private String mText5;
Marco Nelissend16985c2010-01-05 16:39:21 -080088
Marco Nelissen51f94bf2010-01-05 14:13:21 -080089 public interface OnChangedListener {
90 void onChanged(
91 VerticalTextSpinner spinner, int oldPos, int newPos, String[] items);
92 }
Marco Nelissend16985c2010-01-05 16:39:21 -080093
Marco Nelissen51f94bf2010-01-05 14:13:21 -080094 public VerticalTextSpinner(Context context) {
95 this(context, null);
96 }
Marco Nelissend16985c2010-01-05 16:39:21 -080097
Marco Nelissen51f94bf2010-01-05 14:13:21 -080098 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);
Marco Nelissend16985c2010-01-05 16:39:21 -0800105
Marco Nelissen660b8fc2010-01-12 13:58:12 -0800106 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
Marco Nelissend16985c2010-01-05 16:39:21 -0800117 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
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800121 mSelectorHeight = mSelectorFocused.getIntrinsicHeight();
122 mSelectorDefaultY = (mBackgroundFocused.getIntrinsicHeight() - mSelectorHeight) / 2;
123 mSelectorMinY = 0;
124 mSelectorMaxY = mBackgroundFocused.getIntrinsicHeight() - mSelectorHeight;
Marco Nelissend16985c2010-01-05 16:39:21 -0800125
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800126 mSelector = mSelectorNormal;
127 mSelectorY = mSelectorDefaultY;
Marco Nelissend16985c2010-01-05 16:39:21 -0800128
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800129 mTextPaintDark = new TextPaint(Paint.ANTI_ALIAS_FLAG);
130 mTextPaintDark.setTextSize(TEXT_SIZE);
Marco Nelissend16985c2010-01-05 16:39:21 -0800131 mTextPaintDark.setColor(context.getResources()
132 .getColor(android.R.color.primary_text_light));
133
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800134 mTextPaintLight = new TextPaint(Paint.ANTI_ALIAS_FLAG);
135 mTextPaintLight.setTextSize(TEXT_SIZE);
Marco Nelissend16985c2010-01-05 16:39:21 -0800136 mTextPaintLight.setColor(context.getResources()
137 .getColor(android.R.color.secondary_text_dark));
138
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800139 mScrollMode = SCROLL_MODE_NONE;
140 mScrollInterval = DEFAULT_SCROLL_INTERVAL_MS;
141 calculateAnimationValues();
142 }
Marco Nelissend16985c2010-01-05 16:39:21 -0800143
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800144 public void setOnChangeListener(OnChangedListener listener) {
145 mListener = listener;
146 }
Marco Nelissend16985c2010-01-05 16:39:21 -0800147
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800148 public void setItems(String[] textList) {
149 mTextList = textList;
150 calculateTextPositions();
151 }
Marco Nelissend16985c2010-01-05 16:39:21 -0800152
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800153 public void setSelectedPos(int selectedPos) {
154 mCurrentSelectedPos = selectedPos;
155 calculateTextPositions();
156 postInvalidate();
157 }
Marco Nelissend16985c2010-01-05 16:39:21 -0800158
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800159 public void setScrollInterval(long interval) {
160 mScrollInterval = interval;
161 calculateAnimationValues();
162 }
Marco Nelissend16985c2010-01-05 16:39:21 -0800163
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800164 public void setWrapAround(boolean wrap) {
165 mWrapAround = wrap;
166 }
Marco Nelissend16985c2010-01-05 16:39:21 -0800167
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800168 @Override
169 public boolean onKeyDown(int keyCode, KeyEvent event) {
Marco Nelissend16985c2010-01-05 16:39:21 -0800170
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800171 /* 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 }
Marco Nelissend16985c2010-01-05 16:39:21 -0800196
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800197 @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 }
Marco Nelissend16985c2010-01-05 16:39:21 -0800209
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800210 @Override
Marco Nelissend16985c2010-01-05 16:39:21 -0800211 public boolean onTouchEvent(MotionEvent event) {
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800212 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;
Marco Nelissend16985c2010-01-05 16:39:21 -0800219 isDraggingSelector = (y >= mSelectorY) &&
220 (y <= (mSelectorY + mSelector.getIntrinsicHeight()));
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800221 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;
Marco Nelissend16985c2010-01-05 16:39:21 -0800246
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800247 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 }
Marco Nelissend16985c2010-01-05 16:39:21 -0800257
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800258 @Override
259 protected void onDraw(Canvas canvas) {
Marco Nelissend16985c2010-01-05 16:39:21 -0800260
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800261 /* The bounds of the selector */
262 final int selectorLeft = 0;
263 final int selectorTop = mSelectorY;
Marco Nelissen660b8fc2010-01-12 13:58:12 -0800264 final int selectorRight = getWidth();
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800265 final int selectorBottom = mSelectorY + mSelectorHeight;
Marco Nelissend16985c2010-01-05 16:39:21 -0800266
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800267 /* Draw the selector */
268 mSelector.setBounds(selectorLeft, selectorTop, selectorRight, selectorBottom);
269 mSelector.draw(canvas);
Marco Nelissend16985c2010-01-05 16:39:21 -0800270
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800271 if (mTextList == null) {
Marco Nelissend16985c2010-01-05 16:39:21 -0800272
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800273 /* We're not setup with values so don't draw anything else */
274 return;
275 }
Marco Nelissend16985c2010-01-05 16:39:21 -0800276
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800277 final TextPaint textPaintDark = mTextPaintDark;
278 if (hasFocus()) {
Marco Nelissend16985c2010-01-05 16:39:21 -0800279
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800280 /* 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;
Marco Nelissend16985c2010-01-05 16:39:21 -0800293
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800294 /*
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;
Marco Nelissend16985c2010-01-05 16:39:21 -0800327 final int bottomBottom = getMeasuredHeight();
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800328
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();
Marco Nelissend16985c2010-01-05 16:39:21 -0800342
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800343 } 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;
Marco Nelissend16985c2010-01-05 16:39:21 -0800378
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800379 /* 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;
Marco Nelissend16985c2010-01-05 16:39:21 -0800385
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800386 /* 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 }
Marco Nelissend16985c2010-01-05 16:39:21 -0800422
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800423 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 }
Marco Nelissend16985c2010-01-05 16:39:21 -0800448
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800449 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 }
Marco Nelissend16985c2010-01-05 16:39:21 -0800469
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800470 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 }
Marco Nelissend16985c2010-01-05 16:39:21 -0800475
Marco Nelissen51f94bf2010-01-05 14:13:21 -0800476 public int getCurrentSelectedPos() {
477 return mCurrentSelectedPos;
478 }
479}