1 /* 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 17 package com.android.music; 18 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.graphics.drawable.NinePatchDrawable; 25 import android.text.TextPaint; 26 import android.util.AttributeSet; 27 import android.util.Log; 28 import android.view.KeyEvent; 29 import android.view.MotionEvent; 30 import android.view.View; 31 32 33 public class VerticalTextSpinner extends View { 34 35 private static final int SELECTOR_ARROW_HEIGHT = 15; 36 37 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; 46 47 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; 50 51 private static final long DEFAULT_SCROLL_INTERVAL_MS = 400; 52 private static final int MIN_ANIMATIONS = 4; 53 54 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; 63 64 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; 73 74 private int mTotalAnimatedDistance; 75 private int mNumberOfAnimations; 76 private long mDelayBetweenAnimations; 77 private int mDistanceOfEachAnimation; 78 79 private String[] mTextList; 80 private int mCurrentSelectedPos; 81 private OnChangedListener mListener; 82 83 private String mText1; 84 private String mText2; 85 private String mText3; 86 private String mText4; 87 private String mText5; 88 89 public interface OnChangedListener { 90 void onChanged( 91 VerticalTextSpinner spinner, int oldPos, int newPos, String[] items); 92 } 93 94 public VerticalTextSpinner(Context context) { 95 this(context, null); 96 } 97 98 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); 105 106 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 117 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 121 mSelectorHeight = mSelectorFocused.getIntrinsicHeight(); 122 mSelectorDefaultY = (mBackgroundFocused.getIntrinsicHeight() - mSelectorHeight) / 2; 123 mSelectorMinY = 0; 124 mSelectorMaxY = mBackgroundFocused.getIntrinsicHeight() - mSelectorHeight; 125 126 mSelector = mSelectorNormal; 127 mSelectorY = mSelectorDefaultY; 128 129 mTextPaintDark = new TextPaint(Paint.ANTI_ALIAS_FLAG); 130 mTextPaintDark.setTextSize(TEXT_SIZE); 131 mTextPaintDark.setColor(context.getResources() 132 .getColor(android.R.color.primary_text_light)); 133 134 mTextPaintLight = new TextPaint(Paint.ANTI_ALIAS_FLAG); 135 mTextPaintLight.setTextSize(TEXT_SIZE); 136 mTextPaintLight.setColor(context.getResources() 137 .getColor(android.R.color.secondary_text_dark)); 138 139 mScrollMode = SCROLL_MODE_NONE; 140 mScrollInterval = DEFAULT_SCROLL_INTERVAL_MS; 141 calculateAnimationValues(); 142 } 143 144 public void setOnChangeListener(OnChangedListener listener) { 145 mListener = listener; 146 } 147 148 public void setItems(String[] textList) { 149 mTextList = textList; 150 calculateTextPositions(); 151 } 152 153 public void setSelectedPos(int selectedPos) { 154 mCurrentSelectedPos = selectedPos; 155 calculateTextPositions(); 156 postInvalidate(); 157 } 158 159 public void setScrollInterval(long interval) { 160 mScrollInterval = interval; 161 calculateAnimationValues(); 162 } 163 164 public void setWrapAround(boolean wrap) { 165 mWrapAround = wrap; 166 } 167 168 @Override 169 public boolean onKeyDown(int keyCode, KeyEvent event) { 170 171 /* 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 } 196 197 @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 } 209 210 @Override 211 public boolean onTouchEvent(MotionEvent event) { 212 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; 219 isDraggingSelector = (y >= mSelectorY) && 220 (y <= (mSelectorY + mSelector.getIntrinsicHeight())); 221 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; 246 247 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 } 257 258 @Override 259 protected void onDraw(Canvas canvas) { 260 261 /* The bounds of the selector */ 262 final int selectorLeft = 0; 263 final int selectorTop = mSelectorY; 264 final int selectorRight = getWidth(); 265 final int selectorBottom = mSelectorY + mSelectorHeight; 266 267 /* Draw the selector */ 268 mSelector.setBounds(selectorLeft, selectorTop, selectorRight, selectorBottom); 269 mSelector.draw(canvas); 270 271 if (mTextList == null) { 272 273 /* We're not setup with values so don't draw anything else */ 274 return; 275 } 276 277 final TextPaint textPaintDark = mTextPaintDark; 278 if (hasFocus()) { 279 280 /* 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; 293 294 /* 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; 327 final int bottomBottom = getMeasuredHeight(); 328 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(); 342 343 } 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; 378 379 /* 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; 385 386 /* 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 } 422 423 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 } 448 449 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 } 469 470 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 } 475 476 public int getCurrentSelectedPos() { 477 return mCurrentSelectedPos; 478 } 479 } 480