blob: 38da81457e71818f47c82326cab8ded90e94cca6 [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;
24import android.text.TextPaint;
25import android.util.AttributeSet;
26import android.view.KeyEvent;
27import android.view.MotionEvent;
28import android.view.View;
29
30
31public class VerticalTextSpinner extends View {
32
33 private static final int SELECTOR_ARROW_HEIGHT = 15;
34
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;
38
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.
41 */
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));
47
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;
51
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;
55
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;
65
66 private int mSelectorY;
67 private Drawable mSelector;
68 private int mDownY;
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;
75
76 private int mTotalAnimatedDistance;
77 private int mNumberOfAnimations;
78 private long mDelayBetweenAnimations;
79 private int mDistanceOfEachAnimation;
80
81 private String[] mTextList;
82 private int mCurrentSelectedPos;
83 private OnChangedListener mListener;
84
85 private String mText1;
86 private String mText2;
87 private String mText3;
88 private String mText4;
89 private String mText5;
90
91 public interface OnChangedListener {
92 void onChanged(
93 VerticalTextSpinner spinner, int oldPos, int newPos, String[] items);
94 }
95
96 public VerticalTextSpinner(Context context) {
97 this(context, null);
98 }
99
100 public VerticalTextSpinner(Context context, AttributeSet attrs) {
101 this(context, attrs, 0);
102 }
103
104 public VerticalTextSpinner(Context context, AttributeSet attrs,
105 int defStyle) {
106 super(context, attrs, defStyle);
107
108 mBackgroundFocused = context.getResources().getDrawable(com.android.internal.R.drawable.pickerbox_background);
109 mSelectorFocused = context.getResources().getDrawable(com.android.internal.R.drawable.pickerbox_selected);
110 mSelectorNormal = context.getResources().getDrawable(com.android.internal.R.drawable.pickerbox_unselected);
111
112 mSelectorHeight = mSelectorFocused.getIntrinsicHeight();
113 mSelectorDefaultY = (mBackgroundFocused.getIntrinsicHeight() - mSelectorHeight) / 2;
114 mSelectorMinY = 0;
115 mSelectorMaxY = mBackgroundFocused.getIntrinsicHeight() - mSelectorHeight;
116
117 mSelector = mSelectorNormal;
118 mSelectorY = mSelectorDefaultY;
119
120 mTextPaintDark = new TextPaint(Paint.ANTI_ALIAS_FLAG);
121 mTextPaintDark.setTextSize(TEXT_SIZE);
122 mTextPaintDark.setColor(context.getResources().getColor(com.android.internal.R.color.primary_text_light));
123
124 mTextPaintLight = new TextPaint(Paint.ANTI_ALIAS_FLAG);
125 mTextPaintLight.setTextSize(TEXT_SIZE);
126 mTextPaintLight.setColor(context.getResources().getColor(com.android.internal.R.color.secondary_text_dark));
127
128 mScrollMode = SCROLL_MODE_NONE;
129 mScrollInterval = DEFAULT_SCROLL_INTERVAL_MS;
130 calculateAnimationValues();
131 }
132
133 public void setOnChangeListener(OnChangedListener listener) {
134 mListener = listener;
135 }
136
137 public void setItems(String[] textList) {
138 mTextList = textList;
139 calculateTextPositions();
140 }
141
142 public void setSelectedPos(int selectedPos) {
143 mCurrentSelectedPos = selectedPos;
144 calculateTextPositions();
145 postInvalidate();
146 }
147
148 public void setScrollInterval(long interval) {
149 mScrollInterval = interval;
150 calculateAnimationValues();
151 }
152
153 public void setWrapAround(boolean wrap) {
154 mWrapAround = wrap;
155 }
156
157 @Override
158 public boolean onKeyDown(int keyCode, KeyEvent event) {
159
160 /* This is a bit confusing, when we get the key event
161 * DPAD_DOWN we actually roll the spinner up. When the
162 * key event is DPAD_UP we roll the spinner down.
163 */
164 if ((keyCode == KeyEvent.KEYCODE_DPAD_UP) && canScrollDown()) {
165 mScrollMode = SCROLL_MODE_DOWN;
166 scroll();
167 mStopAnimation = true;
168 return true;
169 } else if ((keyCode == KeyEvent.KEYCODE_DPAD_DOWN) && canScrollUp()) {
170 mScrollMode = SCROLL_MODE_UP;
171 scroll();
172 mStopAnimation = true;
173 return true;
174 }
175 return super.onKeyDown(keyCode, event);
176 }
177
178 private boolean canScrollDown() {
179 return (mCurrentSelectedPos > 0) || mWrapAround;
180 }
181
182 private boolean canScrollUp() {
183 return ((mCurrentSelectedPos < (mTextList.length - 1)) || mWrapAround);
184 }
185
186 @Override
187 protected void onFocusChanged(boolean gainFocus, int direction,
188 Rect previouslyFocusedRect) {
189 if (gainFocus) {
190 setBackgroundDrawable(mBackgroundFocused);
191 mSelector = mSelectorFocused;
192 } else {
193 setBackgroundDrawable(null);
194 mSelector = mSelectorNormal;
195 mSelectorY = mSelectorDefaultY;
196 }
197 }
198
199 @Override
200 public boolean onTouchEvent(MotionEvent event) {
201 final int action = event.getAction();
202 final int y = (int) event.getY();
203
204 switch (action) {
205 case MotionEvent.ACTION_DOWN:
206 requestFocus();
207 mDownY = y;
208 isDraggingSelector = (y >= mSelectorY) && (y <= (mSelectorY + mSelector.getIntrinsicHeight()));
209 break;
210
211 case MotionEvent.ACTION_MOVE:
212 if (isDraggingSelector) {
213 int top = mSelectorDefaultY + (y - mDownY);
214 if (top <= mSelectorMinY && canScrollDown()) {
215 mSelectorY = mSelectorMinY;
216 mStopAnimation = false;
217 if (mScrollMode != SCROLL_MODE_DOWN) {
218 mScrollMode = SCROLL_MODE_DOWN;
219 scroll();
220 }
221 } else if (top >= mSelectorMaxY && canScrollUp()) {
222 mSelectorY = mSelectorMaxY;
223 mStopAnimation = false;
224 if (mScrollMode != SCROLL_MODE_UP) {
225 mScrollMode = SCROLL_MODE_UP;
226 scroll();
227 }
228 } else {
229 mSelectorY = top;
230 mStopAnimation = true;
231 }
232 }
233 break;
234
235 case MotionEvent.ACTION_UP:
236 case MotionEvent.ACTION_CANCEL:
237 default:
238 mSelectorY = mSelectorDefaultY;
239 mStopAnimation = true;
240 invalidate();
241 break;
242 }
243 return true;
244 }
245
246 @Override
247 protected void onDraw(Canvas canvas) {
248
249 /* The bounds of the selector */
250 final int selectorLeft = 0;
251 final int selectorTop = mSelectorY;
252 final int selectorRight = mMeasuredWidth;
253 final int selectorBottom = mSelectorY + mSelectorHeight;
254
255 /* Draw the selector */
256 mSelector.setBounds(selectorLeft, selectorTop, selectorRight, selectorBottom);
257 mSelector.draw(canvas);
258
259 if (mTextList == null) {
260
261 /* We're not setup with values so don't draw anything else */
262 return;
263 }
264
265 final TextPaint textPaintDark = mTextPaintDark;
266 if (hasFocus()) {
267
268 /* The bounds of the top area where the text should be light */
269 final int topLeft = 0;
270 final int topTop = 0;
271 final int topRight = selectorRight;
272 final int topBottom = selectorTop + SELECTOR_ARROW_HEIGHT;
273
274 /* Assign a bunch of local finals for performance */
275 final String text1 = mText1;
276 final String text2 = mText2;
277 final String text3 = mText3;
278 final String text4 = mText4;
279 final String text5 = mText5;
280 final TextPaint textPaintLight = mTextPaintLight;
281
282 /*
283 * Draw the 1st, 2nd and 3rd item in light only, clip it so it only
284 * draws in the area above the selector
285 */
286 canvas.save();
287 canvas.clipRect(topLeft, topTop, topRight, topBottom);
288 drawText(canvas, text1, TEXT1_Y
289 + mTotalAnimatedDistance, textPaintLight);
290 drawText(canvas, text2, TEXT2_Y
291 + mTotalAnimatedDistance, textPaintLight);
292 drawText(canvas, text3,
293 TEXT3_Y + mTotalAnimatedDistance, textPaintLight);
294 canvas.restore();
295
296 /*
297 * Draw the 2nd, 3rd and 4th clipped to the selector bounds in dark
298 * paint
299 */
300 canvas.save();
301 canvas.clipRect(selectorLeft, selectorTop + SELECTOR_ARROW_HEIGHT,
302 selectorRight, selectorBottom - SELECTOR_ARROW_HEIGHT);
303 drawText(canvas, text2, TEXT2_Y
304 + mTotalAnimatedDistance, textPaintDark);
305 drawText(canvas, text3,
306 TEXT3_Y + mTotalAnimatedDistance, textPaintDark);
307 drawText(canvas, text4,
308 TEXT4_Y + mTotalAnimatedDistance, textPaintDark);
309 canvas.restore();
310
311 /* The bounds of the bottom area where the text should be light */
312 final int bottomLeft = 0;
313 final int bottomTop = selectorBottom - SELECTOR_ARROW_HEIGHT;
314 final int bottomRight = selectorRight;
315 final int bottomBottom = mMeasuredHeight;
316
317 /*
318 * Draw the 3rd, 4th and 5th in white text, clip it so it only draws
319 * in the area below the selector.
320 */
321 canvas.save();
322 canvas.clipRect(bottomLeft, bottomTop, bottomRight, bottomBottom);
323 drawText(canvas, text3,
324 TEXT3_Y + mTotalAnimatedDistance, textPaintLight);
325 drawText(canvas, text4,
326 TEXT4_Y + mTotalAnimatedDistance, textPaintLight);
327 drawText(canvas, text5,
328 TEXT5_Y + mTotalAnimatedDistance, textPaintLight);
329 canvas.restore();
330
331 } else {
332 drawText(canvas, mText3, TEXT3_Y, textPaintDark);
333 }
334 if (mIsAnimationRunning) {
335 if ((Math.abs(mTotalAnimatedDistance) + mDistanceOfEachAnimation) > SCROLL_DISTANCE) {
336 mTotalAnimatedDistance = 0;
337 if (mScrollMode == SCROLL_MODE_UP) {
338 int oldPos = mCurrentSelectedPos;
339 int newPos = getNewIndex(1);
340 if (newPos >= 0) {
341 mCurrentSelectedPos = newPos;
342 if (mListener != null) {
343 mListener.onChanged(this, oldPos, mCurrentSelectedPos, mTextList);
344 }
345 }
346 if (newPos < 0 || ((newPos >= mTextList.length - 1) && !mWrapAround)) {
347 mStopAnimation = true;
348 }
349 calculateTextPositions();
350 } else if (mScrollMode == SCROLL_MODE_DOWN) {
351 int oldPos = mCurrentSelectedPos;
352 int newPos = getNewIndex(-1);
353 if (newPos >= 0) {
354 mCurrentSelectedPos = newPos;
355 if (mListener != null) {
356 mListener.onChanged(this, oldPos, mCurrentSelectedPos, mTextList);
357 }
358 }
359 if (newPos < 0 || (newPos == 0 && !mWrapAround)) {
360 mStopAnimation = true;
361 }
362 calculateTextPositions();
363 }
364 if (mStopAnimation) {
365 final int previousScrollMode = mScrollMode;
366
367 /* No longer scrolling, we wait till the current animation
368 * completes then we stop.
369 */
370 mIsAnimationRunning = false;
371 mStopAnimation = false;
372 mScrollMode = SCROLL_MODE_NONE;
373
374 /* If the current selected item is an empty string
375 * scroll past it.
376 */
377 if ("".equals(mTextList[mCurrentSelectedPos])) {
378 mScrollMode = previousScrollMode;
379 scroll();
380 mStopAnimation = true;
381 }
382 }
383 } else {
384 if (mScrollMode == SCROLL_MODE_UP) {
385 mTotalAnimatedDistance -= mDistanceOfEachAnimation;
386 } else if (mScrollMode == SCROLL_MODE_DOWN) {
387 mTotalAnimatedDistance += mDistanceOfEachAnimation;
388 }
389 }
390 if (mDelayBetweenAnimations > 0) {
391 postInvalidateDelayed(mDelayBetweenAnimations);
392 } else {
393 invalidate();
394 }
395 }
396 }
397
398 /**
399 * Called every time the text items or current position
400 * changes. We calculate store we don't have to calculate
401 * onDraw.
402 */
403 private void calculateTextPositions() {
404 mText1 = getTextToDraw(-2);
405 mText2 = getTextToDraw(-1);
406 mText3 = getTextToDraw(0);
407 mText4 = getTextToDraw(1);
408 mText5 = getTextToDraw(2);
409 }
410
411 private String getTextToDraw(int offset) {
412 int index = getNewIndex(offset);
413 if (index < 0) {
414 return "";
415 }
416 return mTextList[index];
417 }
418
419 private int getNewIndex(int offset) {
420 int index = mCurrentSelectedPos + offset;
421 if (index < 0) {
422 if (mWrapAround) {
423 index += mTextList.length;
424 } else {
425 return -1;
426 }
427 } else if (index >= mTextList.length) {
428 if (mWrapAround) {
429 index -= mTextList.length;
430 } else {
431 return -1;
432 }
433 }
434 return index;
435 }
436
437 private void scroll() {
438 if (mIsAnimationRunning) {
439 return;
440 }
441 mTotalAnimatedDistance = 0;
442 mIsAnimationRunning = true;
443 invalidate();
444 }
445
446 private void calculateAnimationValues() {
447 mNumberOfAnimations = (int) mScrollInterval / SCROLL_DISTANCE;
448 if (mNumberOfAnimations < MIN_ANIMATIONS) {
449 mNumberOfAnimations = MIN_ANIMATIONS;
450 mDistanceOfEachAnimation = SCROLL_DISTANCE / mNumberOfAnimations;
451 mDelayBetweenAnimations = 0;
452 } else {
453 mDistanceOfEachAnimation = SCROLL_DISTANCE / mNumberOfAnimations;
454 mDelayBetweenAnimations = mScrollInterval / mNumberOfAnimations;
455 }
456 }
457
458 private void drawText(Canvas canvas, String text, int y, TextPaint paint) {
459 int width = (int) paint.measureText(text);
460 int x = getMeasuredWidth() - width - TEXT_MARGIN_RIGHT;
461 canvas.drawText(text, x, y, paint);
462 }
463
464 public int getCurrentSelectedPos() {
465 return mCurrentSelectedPos;
466 }
467}