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