RadialTimePickerView.java revision 5efe0d199e9a6133af355ace6e4b52b82a65af6e
1/* 2 * Copyright (C) 2013 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 android.widget; 18 19import android.animation.Animator; 20import android.animation.AnimatorSet; 21import android.animation.Keyframe; 22import android.animation.ObjectAnimator; 23import android.animation.PropertyValuesHolder; 24import android.animation.ValueAnimator; 25import android.content.Context; 26import android.content.res.Resources; 27import android.content.res.TypedArray; 28import android.graphics.Canvas; 29import android.graphics.Color; 30import android.graphics.Paint; 31import android.graphics.Rect; 32import android.graphics.Typeface; 33import android.os.Bundle; 34import android.util.AttributeSet; 35import android.util.IntArray; 36import android.util.Log; 37import android.util.MathUtils; 38import android.util.TypedValue; 39import android.view.HapticFeedbackConstants; 40import android.view.MotionEvent; 41import android.view.View; 42import android.view.ViewGroup; 43import android.view.accessibility.AccessibilityEvent; 44import android.view.accessibility.AccessibilityNodeInfo; 45import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; 46 47import com.android.internal.R; 48import com.android.internal.widget.ExploreByTouchHelper; 49 50import java.util.ArrayList; 51import java.util.Calendar; 52import java.util.Locale; 53 54/** 55 * View to show a clock circle picker (with one or two picking circles) 56 * 57 * @hide 58 */ 59public class RadialTimePickerView extends View implements View.OnTouchListener { 60 private static final String TAG = "ClockView"; 61 62 private static final boolean DEBUG = false; 63 64 private static final int DEBUG_COLOR = 0x20FF0000; 65 private static final int DEBUG_TEXT_COLOR = 0x60FF0000; 66 private static final int DEBUG_STROKE_WIDTH = 2; 67 68 private static final int HOURS = 0; 69 private static final int MINUTES = 1; 70 private static final int HOURS_INNER = 2; 71 72 private static final int SELECTOR_CIRCLE = 0; 73 private static final int SELECTOR_DOT = 1; 74 private static final int SELECTOR_LINE = 2; 75 76 private static final int AM = 0; 77 private static final int PM = 1; 78 79 // Opaque alpha level 80 private static final int ALPHA_OPAQUE = 255; 81 82 // Transparent alpha level 83 private static final int ALPHA_TRANSPARENT = 0; 84 85 // Alpha level of color for selector. 86 private static final int ALPHA_SELECTOR = 60; // was 51 87 88 private static final float COSINE_30_DEGREES = ((float) Math.sqrt(3)) * 0.5f; 89 private static final float SINE_30_DEGREES = 0.5f; 90 91 private static final int DEGREES_FOR_ONE_HOUR = 30; 92 private static final int DEGREES_FOR_ONE_MINUTE = 6; 93 94 private static final int[] HOURS_NUMBERS = {12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; 95 private static final int[] HOURS_NUMBERS_24 = {0, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}; 96 private static final int[] MINUTES_NUMBERS = {0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55}; 97 98 private static final int CENTER_RADIUS = 2; 99 100 private static int[] sSnapPrefer30sMap = new int[361]; 101 102 private final InvalidateUpdateListener mInvalidateUpdateListener = 103 new InvalidateUpdateListener(); 104 105 private final String[] mHours12Texts = new String[12]; 106 private final String[] mOuterHours24Texts = new String[12]; 107 private final String[] mInnerHours24Texts = new String[12]; 108 private final String[] mMinutesTexts = new String[12]; 109 110 private final Paint[] mPaint = new Paint[2]; 111 private final int[] mColor = new int[2]; 112 private final IntHolder[] mAlpha = new IntHolder[2]; 113 114 private final Paint mPaintCenter = new Paint(); 115 116 private final Paint[][] mPaintSelector = new Paint[2][3]; 117 private final int[][] mColorSelector = new int[2][3]; 118 private final IntHolder[][] mAlphaSelector = new IntHolder[2][3]; 119 120 private final Paint mPaintBackground = new Paint(); 121 private final Paint mPaintDebug = new Paint(); 122 123 private final Typeface mTypeface; 124 125 private final float[] mCircleRadius = new float[3]; 126 127 private final float[] mTextSize = new float[2]; 128 129 private final float[][] mTextGridHeights = new float[2][7]; 130 private final float[][] mTextGridWidths = new float[2][7]; 131 132 private final float[] mInnerTextGridHeights = new float[7]; 133 private final float[] mInnerTextGridWidths = new float[7]; 134 135 private final float[] mCircleRadiusMultiplier = new float[2]; 136 private final float[] mNumbersRadiusMultiplier = new float[3]; 137 138 private final float[] mTextSizeMultiplier = new float[3]; 139 140 private final float[] mAnimationRadiusMultiplier = new float[3]; 141 142 private final float mTransitionMidRadiusMultiplier; 143 private final float mTransitionEndRadiusMultiplier; 144 145 private final int[] mLineLength = new int[3]; 146 private final int[] mSelectionRadius = new int[3]; 147 private final float mSelectionRadiusMultiplier; 148 private final int[] mSelectionDegrees = new int[3]; 149 150 private final ArrayList<Animator> mHoursToMinutesAnims = new ArrayList<Animator>(); 151 private final ArrayList<Animator> mMinuteToHoursAnims = new ArrayList<Animator>(); 152 153 private final RadialPickerTouchHelper mTouchHelper; 154 155 private float mInnerTextSize; 156 157 private boolean mIs24HourMode; 158 private boolean mShowHours; 159 160 /** 161 * When in 24-hour mode, indicates that the current hour is between 162 * 1 and 12 (inclusive). 163 */ 164 private boolean mIsOnInnerCircle; 165 166 private int mXCenter; 167 private int mYCenter; 168 169 private int mMinHypotenuseForInnerNumber; 170 private int mMaxHypotenuseForOuterNumber; 171 private int mHalfwayHypotenusePoint; 172 173 private String[] mOuterTextHours; 174 private String[] mInnerTextHours; 175 private String[] mOuterTextMinutes; 176 private AnimatorSet mTransition; 177 178 private int mAmOrPm; 179 private int mDisabledAlpha; 180 181 private OnValueSelectedListener mListener; 182 183 private boolean mInputEnabled = true; 184 185 public interface OnValueSelectedListener { 186 void onValueSelected(int pickerIndex, int newValue, boolean autoAdvance); 187 } 188 189 static { 190 // Prepare mapping to snap touchable degrees to selectable degrees. 191 preparePrefer30sMap(); 192 } 193 194 /** 195 * Split up the 360 degrees of the circle among the 60 selectable values. Assigns a larger 196 * selectable area to each of the 12 visible values, such that the ratio of space apportioned 197 * to a visible value : space apportioned to a non-visible value will be 14 : 4. 198 * E.g. the output of 30 degrees should have a higher range of input associated with it than 199 * the output of 24 degrees, because 30 degrees corresponds to a visible number on the clock 200 * circle (5 on the minutes, 1 or 13 on the hours). 201 */ 202 private static void preparePrefer30sMap() { 203 // We'll split up the visible output and the non-visible output such that each visible 204 // output will correspond to a range of 14 associated input degrees, and each non-visible 205 // output will correspond to a range of 4 associate input degrees, so visible numbers 206 // are more than 3 times easier to get than non-visible numbers: 207 // {354-359,0-7}:0, {8-11}:6, {12-15}:12, {16-19}:18, {20-23}:24, {24-37}:30, etc. 208 // 209 // If an output of 30 degrees should correspond to a range of 14 associated degrees, then 210 // we'll need any input between 24 - 37 to snap to 30. Working out from there, 20-23 should 211 // snap to 24, while 38-41 should snap to 36. This is somewhat counter-intuitive, that you 212 // can be touching 36 degrees but have the selection snapped to 30 degrees; however, this 213 // inconsistency isn't noticeable at such fine-grained degrees, and it affords us the 214 // ability to aggressively prefer the visible values by a factor of more than 3:1, which 215 // greatly contributes to the selectability of these values. 216 217 // The first output is 0, and each following output will increment by 6 {0, 6, 12, ...}. 218 int snappedOutputDegrees = 0; 219 // Count of how many inputs we've designated to the specified output. 220 int count = 1; 221 // How many input we expect for a specified output. This will be 14 for output divisible 222 // by 30, and 4 for the remaining output. We'll special case the outputs of 0 and 360, so 223 // the caller can decide which they need. 224 int expectedCount = 8; 225 // Iterate through the input. 226 for (int degrees = 0; degrees < 361; degrees++) { 227 // Save the input-output mapping. 228 sSnapPrefer30sMap[degrees] = snappedOutputDegrees; 229 // If this is the last input for the specified output, calculate the next output and 230 // the next expected count. 231 if (count == expectedCount) { 232 snappedOutputDegrees += 6; 233 if (snappedOutputDegrees == 360) { 234 expectedCount = 7; 235 } else if (snappedOutputDegrees % 30 == 0) { 236 expectedCount = 14; 237 } else { 238 expectedCount = 4; 239 } 240 count = 1; 241 } else { 242 count++; 243 } 244 } 245 } 246 247 /** 248 * Returns mapping of any input degrees (0 to 360) to one of 60 selectable output degrees, 249 * where the degrees corresponding to visible numbers (i.e. those divisible by 30) will be 250 * weighted heavier than the degrees corresponding to non-visible numbers. 251 * See {@link #preparePrefer30sMap()} documentation for the rationale and generation of the 252 * mapping. 253 */ 254 private static int snapPrefer30s(int degrees) { 255 if (sSnapPrefer30sMap == null) { 256 return -1; 257 } 258 return sSnapPrefer30sMap[degrees]; 259 } 260 261 /** 262 * Returns mapping of any input degrees (0 to 360) to one of 12 visible output degrees (all 263 * multiples of 30), where the input will be "snapped" to the closest visible degrees. 264 * @param degrees The input degrees 265 * @param forceHigherOrLower The output may be forced to either the higher or lower step, or may 266 * be allowed to snap to whichever is closer. Use 1 to force strictly higher, -1 to force 267 * strictly lower, and 0 to snap to the closer one. 268 * @return output degrees, will be a multiple of 30 269 */ 270 private static int snapOnly30s(int degrees, int forceHigherOrLower) { 271 final int stepSize = DEGREES_FOR_ONE_HOUR; 272 int floor = (degrees / stepSize) * stepSize; 273 final int ceiling = floor + stepSize; 274 if (forceHigherOrLower == 1) { 275 degrees = ceiling; 276 } else if (forceHigherOrLower == -1) { 277 if (degrees == floor) { 278 floor -= stepSize; 279 } 280 degrees = floor; 281 } else { 282 if ((degrees - floor) < (ceiling - degrees)) { 283 degrees = floor; 284 } else { 285 degrees = ceiling; 286 } 287 } 288 return degrees; 289 } 290 291 @SuppressWarnings("unused") 292 public RadialTimePickerView(Context context) { 293 this(context, null); 294 } 295 296 public RadialTimePickerView(Context context, AttributeSet attrs) { 297 this(context, attrs, R.attr.timePickerStyle); 298 } 299 300 public RadialTimePickerView(Context context, AttributeSet attrs, int defStyleAttr) { 301 this(context, attrs, defStyleAttr, 0); 302 } 303 304 public RadialTimePickerView( 305 Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 306 super(context, attrs); 307 308 // Pull disabled alpha from theme. 309 final TypedValue outValue = new TypedValue(); 310 context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, outValue, true); 311 mDisabledAlpha = (int) (outValue.getFloat() * 255 + 0.5f); 312 313 // process style attributes 314 final Resources res = getResources(); 315 final TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.TimePicker, 316 defStyleAttr, defStyleRes); 317 318 mTypeface = Typeface.create("sans-serif", Typeface.NORMAL); 319 320 // Initialize all alpha values to opaque. 321 for (int i = 0; i < mAlpha.length; i++) { 322 mAlpha[i] = new IntHolder(ALPHA_OPAQUE); 323 } 324 for (int i = 0; i < mAlphaSelector.length; i++) { 325 for (int j = 0; j < mAlphaSelector[i].length; j++) { 326 mAlphaSelector[i][j] = new IntHolder(ALPHA_OPAQUE); 327 } 328 } 329 330 final int numbersTextColor = a.getColor(R.styleable.TimePicker_numbersTextColor, 331 res.getColor(R.color.timepicker_default_text_color_material)); 332 333 mPaint[HOURS] = new Paint(); 334 mPaint[HOURS].setAntiAlias(true); 335 mPaint[HOURS].setTextAlign(Paint.Align.CENTER); 336 mColor[HOURS] = numbersTextColor; 337 338 mPaint[MINUTES] = new Paint(); 339 mPaint[MINUTES].setAntiAlias(true); 340 mPaint[MINUTES].setTextAlign(Paint.Align.CENTER); 341 mColor[MINUTES] = numbersTextColor; 342 343 mPaintCenter.setColor(numbersTextColor); 344 mPaintCenter.setAntiAlias(true); 345 mPaintCenter.setTextAlign(Paint.Align.CENTER); 346 347 mPaintSelector[HOURS][SELECTOR_CIRCLE] = new Paint(); 348 mPaintSelector[HOURS][SELECTOR_CIRCLE].setAntiAlias(true); 349 mColorSelector[HOURS][SELECTOR_CIRCLE] = a.getColor( 350 R.styleable.TimePicker_numbersSelectorColor, 351 R.color.timepicker_default_selector_color_material); 352 353 mPaintSelector[HOURS][SELECTOR_DOT] = new Paint(); 354 mPaintSelector[HOURS][SELECTOR_DOT].setAntiAlias(true); 355 mColorSelector[HOURS][SELECTOR_DOT] = a.getColor( 356 R.styleable.TimePicker_numbersSelectorColor, 357 R.color.timepicker_default_selector_color_material); 358 359 mPaintSelector[HOURS][SELECTOR_LINE] = new Paint(); 360 mPaintSelector[HOURS][SELECTOR_LINE].setAntiAlias(true); 361 mPaintSelector[HOURS][SELECTOR_LINE].setStrokeWidth(2); 362 mColorSelector[HOURS][SELECTOR_LINE] = a.getColor( 363 R.styleable.TimePicker_numbersSelectorColor, 364 R.color.timepicker_default_selector_color_material); 365 366 mPaintSelector[MINUTES][SELECTOR_CIRCLE] = new Paint(); 367 mPaintSelector[MINUTES][SELECTOR_CIRCLE].setAntiAlias(true); 368 mColorSelector[MINUTES][SELECTOR_CIRCLE] = a.getColor( 369 R.styleable.TimePicker_numbersSelectorColor, 370 R.color.timepicker_default_selector_color_material); 371 372 mPaintSelector[MINUTES][SELECTOR_DOT] = new Paint(); 373 mPaintSelector[MINUTES][SELECTOR_DOT].setAntiAlias(true); 374 mColorSelector[MINUTES][SELECTOR_DOT] = a.getColor( 375 R.styleable.TimePicker_numbersSelectorColor, 376 R.color.timepicker_default_selector_color_material); 377 378 mPaintSelector[MINUTES][SELECTOR_LINE] = new Paint(); 379 mPaintSelector[MINUTES][SELECTOR_LINE].setAntiAlias(true); 380 mPaintSelector[MINUTES][SELECTOR_LINE].setStrokeWidth(2); 381 mColorSelector[MINUTES][SELECTOR_LINE] = a.getColor( 382 R.styleable.TimePicker_numbersSelectorColor, 383 R.color.timepicker_default_selector_color_material); 384 385 mPaintBackground.setColor(a.getColor(R.styleable.TimePicker_numbersBackgroundColor, 386 res.getColor(R.color.timepicker_default_numbers_background_color_material))); 387 mPaintBackground.setAntiAlias(true); 388 389 if (DEBUG) { 390 mPaintDebug.setColor(DEBUG_COLOR); 391 mPaintDebug.setAntiAlias(true); 392 mPaintDebug.setStrokeWidth(DEBUG_STROKE_WIDTH); 393 mPaintDebug.setStyle(Paint.Style.STROKE); 394 mPaintDebug.setTextAlign(Paint.Align.CENTER); 395 } 396 397 mShowHours = true; 398 mIs24HourMode = false; 399 mAmOrPm = AM; 400 401 // Set up accessibility components. 402 mTouchHelper = new RadialPickerTouchHelper(); 403 setAccessibilityDelegate(mTouchHelper); 404 405 if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { 406 setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); 407 } 408 409 initHoursAndMinutesText(); 410 initData(); 411 412 mTransitionMidRadiusMultiplier = Float.parseFloat( 413 res.getString(R.string.timepicker_transition_mid_radius_multiplier)); 414 mTransitionEndRadiusMultiplier = Float.parseFloat( 415 res.getString(R.string.timepicker_transition_end_radius_multiplier)); 416 417 mTextGridHeights[HOURS] = new float[7]; 418 mTextGridHeights[MINUTES] = new float[7]; 419 420 mSelectionRadiusMultiplier = Float.parseFloat( 421 res.getString(R.string.timepicker_selection_radius_multiplier)); 422 423 a.recycle(); 424 425 setOnTouchListener(this); 426 setClickable(true); 427 428 // Initial values 429 final Calendar calendar = Calendar.getInstance(Locale.getDefault()); 430 final int currentHour = calendar.get(Calendar.HOUR_OF_DAY); 431 final int currentMinute = calendar.get(Calendar.MINUTE); 432 433 setCurrentHourInternal(currentHour, false, false); 434 setCurrentMinuteInternal(currentMinute, false); 435 436 setHapticFeedbackEnabled(true); 437 } 438 439 /** 440 * Measure the view to end up as a square, based on the minimum of the height and width. 441 */ 442 @Override 443 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 444 int measuredWidth = MeasureSpec.getSize(widthMeasureSpec); 445 int widthMode = MeasureSpec.getMode(widthMeasureSpec); 446 int measuredHeight = MeasureSpec.getSize(heightMeasureSpec); 447 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 448 int minDimension = Math.min(measuredWidth, measuredHeight); 449 450 super.onMeasure(MeasureSpec.makeMeasureSpec(minDimension, widthMode), 451 MeasureSpec.makeMeasureSpec(minDimension, heightMode)); 452 } 453 454 public void initialize(int hour, int minute, boolean is24HourMode) { 455 if (mIs24HourMode != is24HourMode) { 456 mIs24HourMode = is24HourMode; 457 initData(); 458 } 459 460 setCurrentHourInternal(hour, false, false); 461 setCurrentMinuteInternal(minute, false); 462 } 463 464 public void setCurrentItemShowing(int item, boolean animate) { 465 switch (item){ 466 case HOURS: 467 showHours(animate); 468 break; 469 case MINUTES: 470 showMinutes(animate); 471 break; 472 default: 473 Log.e(TAG, "ClockView does not support showing item " + item); 474 } 475 } 476 477 public int getCurrentItemShowing() { 478 return mShowHours ? HOURS : MINUTES; 479 } 480 481 public void setOnValueSelectedListener(OnValueSelectedListener listener) { 482 mListener = listener; 483 } 484 485 /** 486 * Sets the current hour in 24-hour time. 487 * 488 * @param hour the current hour between 0 and 23 (inclusive) 489 */ 490 public void setCurrentHour(int hour) { 491 setCurrentHourInternal(hour, true, false); 492 } 493 494 /** 495 * Sets the current hour. 496 * 497 * @param hour The current hour 498 * @param callback Whether the value listener should be invoked 499 * @param autoAdvance Whether the listener should auto-advance to the next 500 * selection mode, e.g. hour to minutes 501 */ 502 private void setCurrentHourInternal(int hour, boolean callback, boolean autoAdvance) { 503 final int degrees = (hour % 12) * DEGREES_FOR_ONE_HOUR; 504 mSelectionDegrees[HOURS] = degrees; 505 mSelectionDegrees[HOURS_INNER] = degrees; 506 507 // 0 is 12 AM (midnight) and 12 is 12 PM (noon). 508 final int amOrPm = (hour == 0 || (hour % 24) < 12) ? AM : PM; 509 final boolean isOnInnerCircle = mIs24HourMode && hour >= 1 && hour <= 12; 510 if (mAmOrPm != amOrPm || mIsOnInnerCircle != isOnInnerCircle) { 511 mAmOrPm = amOrPm; 512 mIsOnInnerCircle = isOnInnerCircle; 513 514 initData(); 515 updateLayoutData(); 516 mTouchHelper.invalidateRoot(); 517 } 518 519 invalidate(); 520 521 if (callback && mListener != null) { 522 mListener.onValueSelected(HOURS, hour, autoAdvance); 523 } 524 } 525 526 /** 527 * Returns the current hour in 24-hour time. 528 * 529 * @return the current hour between 0 and 23 (inclusive) 530 */ 531 public int getCurrentHour() { 532 return getHourForDegrees( 533 mSelectionDegrees[mIsOnInnerCircle ? HOURS_INNER : HOURS], mIsOnInnerCircle); 534 } 535 536 private int getHourForDegrees(int degrees, boolean innerCircle) { 537 int hour = (degrees / DEGREES_FOR_ONE_HOUR) % 12; 538 if (mIs24HourMode) { 539 // Convert the 12-hour value into 24-hour time based on where the 540 // selector is positioned. 541 if (innerCircle && hour == 0) { 542 // Inner circle is 1 through 12. 543 hour = 12; 544 } else if (!innerCircle && hour != 0) { 545 // Outer circle is 13 through 23 and 0. 546 hour += 12; 547 } 548 } else if (mAmOrPm == PM) { 549 hour += 12; 550 } 551 return hour; 552 } 553 554 private int getDegreesForHour(int hour) { 555 // Convert to be 0-11. 556 if (mIs24HourMode) { 557 if (hour >= 12) { 558 hour -= 12; 559 } 560 } else if (hour == 12) { 561 hour = 0; 562 } 563 return hour * DEGREES_FOR_ONE_HOUR; 564 } 565 566 public void setCurrentMinute(int minute) { 567 setCurrentMinuteInternal(minute, true); 568 } 569 570 private void setCurrentMinuteInternal(int minute, boolean callback) { 571 mSelectionDegrees[MINUTES] = (minute % 60) * DEGREES_FOR_ONE_MINUTE; 572 573 invalidate(); 574 575 if (callback && mListener != null) { 576 mListener.onValueSelected(MINUTES, minute, false); 577 } 578 } 579 580 // Returns minutes in 0-59 range 581 public int getCurrentMinute() { 582 return getMinuteForDegrees(mSelectionDegrees[MINUTES]); 583 } 584 585 private int getMinuteForDegrees(int degrees) { 586 return degrees / DEGREES_FOR_ONE_MINUTE; 587 } 588 589 private int getDegreesForMinute(int minute) { 590 return minute * DEGREES_FOR_ONE_MINUTE; 591 } 592 593 public void setAmOrPm(int val) { 594 mAmOrPm = (val % 2); 595 invalidate(); 596 mTouchHelper.invalidateRoot(); 597 } 598 599 public int getAmOrPm() { 600 return mAmOrPm; 601 } 602 603 public void showHours(boolean animate) { 604 if (mShowHours) return; 605 mShowHours = true; 606 if (animate) { 607 startMinutesToHoursAnimation(); 608 } 609 initData(); 610 updateLayoutData(); 611 invalidate(); 612 } 613 614 public void showMinutes(boolean animate) { 615 if (!mShowHours) return; 616 mShowHours = false; 617 if (animate) { 618 startHoursToMinutesAnimation(); 619 } 620 initData(); 621 updateLayoutData(); 622 invalidate(); 623 } 624 625 private void initHoursAndMinutesText() { 626 // Initialize the hours and minutes numbers. 627 for (int i = 0; i < 12; i++) { 628 mHours12Texts[i] = String.format("%d", HOURS_NUMBERS[i]); 629 mOuterHours24Texts[i] = String.format("%02d", HOURS_NUMBERS_24[i]); 630 mInnerHours24Texts[i] = String.format("%d", HOURS_NUMBERS[i]); 631 mMinutesTexts[i] = String.format("%02d", MINUTES_NUMBERS[i]); 632 } 633 } 634 635 private void initData() { 636 if (mIs24HourMode) { 637 mOuterTextHours = mOuterHours24Texts; 638 mInnerTextHours = mInnerHours24Texts; 639 } else { 640 mOuterTextHours = mHours12Texts; 641 mInnerTextHours = null; 642 } 643 644 mOuterTextMinutes = mMinutesTexts; 645 646 final Resources res = getResources(); 647 648 if (mShowHours) { 649 if (mIs24HourMode) { 650 mCircleRadiusMultiplier[HOURS] = Float.parseFloat( 651 res.getString(R.string.timepicker_circle_radius_multiplier_24HourMode)); 652 mNumbersRadiusMultiplier[HOURS] = Float.parseFloat( 653 res.getString(R.string.timepicker_numbers_radius_multiplier_outer)); 654 mTextSizeMultiplier[HOURS] = Float.parseFloat( 655 res.getString(R.string.timepicker_text_size_multiplier_outer)); 656 657 mNumbersRadiusMultiplier[HOURS_INNER] = Float.parseFloat( 658 res.getString(R.string.timepicker_numbers_radius_multiplier_inner)); 659 mTextSizeMultiplier[HOURS_INNER] = Float.parseFloat( 660 res.getString(R.string.timepicker_text_size_multiplier_inner)); 661 } else { 662 mCircleRadiusMultiplier[HOURS] = Float.parseFloat( 663 res.getString(R.string.timepicker_circle_radius_multiplier)); 664 mNumbersRadiusMultiplier[HOURS] = Float.parseFloat( 665 res.getString(R.string.timepicker_numbers_radius_multiplier_normal)); 666 mTextSizeMultiplier[HOURS] = Float.parseFloat( 667 res.getString(R.string.timepicker_text_size_multiplier_normal)); 668 } 669 } else { 670 mCircleRadiusMultiplier[MINUTES] = Float.parseFloat( 671 res.getString(R.string.timepicker_circle_radius_multiplier)); 672 mNumbersRadiusMultiplier[MINUTES] = Float.parseFloat( 673 res.getString(R.string.timepicker_numbers_radius_multiplier_normal)); 674 mTextSizeMultiplier[MINUTES] = Float.parseFloat( 675 res.getString(R.string.timepicker_text_size_multiplier_normal)); 676 } 677 678 mAnimationRadiusMultiplier[HOURS] = 1; 679 mAnimationRadiusMultiplier[HOURS_INNER] = 1; 680 mAnimationRadiusMultiplier[MINUTES] = 1; 681 682 mAlpha[HOURS].setValue(mShowHours ? ALPHA_OPAQUE : ALPHA_TRANSPARENT); 683 mAlpha[MINUTES].setValue(mShowHours ? ALPHA_TRANSPARENT : ALPHA_OPAQUE); 684 685 mAlphaSelector[HOURS][SELECTOR_CIRCLE].setValue( 686 mShowHours ? ALPHA_SELECTOR : ALPHA_TRANSPARENT); 687 mAlphaSelector[HOURS][SELECTOR_DOT].setValue( 688 mShowHours ? ALPHA_OPAQUE : ALPHA_TRANSPARENT); 689 mAlphaSelector[HOURS][SELECTOR_LINE].setValue( 690 mShowHours ? ALPHA_SELECTOR : ALPHA_TRANSPARENT); 691 692 mAlphaSelector[MINUTES][SELECTOR_CIRCLE].setValue( 693 mShowHours ? ALPHA_TRANSPARENT : ALPHA_SELECTOR); 694 mAlphaSelector[MINUTES][SELECTOR_DOT].setValue( 695 mShowHours ? ALPHA_TRANSPARENT : ALPHA_OPAQUE); 696 mAlphaSelector[MINUTES][SELECTOR_LINE].setValue( 697 mShowHours ? ALPHA_TRANSPARENT : ALPHA_SELECTOR); 698 } 699 700 @Override 701 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 702 updateLayoutData(); 703 } 704 705 private void updateLayoutData() { 706 mXCenter = getWidth() / 2; 707 mYCenter = getHeight() / 2; 708 709 final int min = Math.min(mXCenter, mYCenter); 710 711 mCircleRadius[HOURS] = min * mCircleRadiusMultiplier[HOURS]; 712 mCircleRadius[HOURS_INNER] = min * mCircleRadiusMultiplier[HOURS]; 713 mCircleRadius[MINUTES] = min * mCircleRadiusMultiplier[MINUTES]; 714 715 mMinHypotenuseForInnerNumber = (int) (mCircleRadius[HOURS] 716 * mNumbersRadiusMultiplier[HOURS_INNER]) - mSelectionRadius[HOURS]; 717 mMaxHypotenuseForOuterNumber = (int) (mCircleRadius[HOURS] 718 * mNumbersRadiusMultiplier[HOURS]) + mSelectionRadius[HOURS]; 719 mHalfwayHypotenusePoint = (int) (mCircleRadius[HOURS] 720 * ((mNumbersRadiusMultiplier[HOURS] + mNumbersRadiusMultiplier[HOURS_INNER]) / 2)); 721 722 mTextSize[HOURS] = mCircleRadius[HOURS] * mTextSizeMultiplier[HOURS]; 723 mTextSize[MINUTES] = mCircleRadius[MINUTES] * mTextSizeMultiplier[MINUTES]; 724 725 if (mIs24HourMode) { 726 mInnerTextSize = mCircleRadius[HOURS] * mTextSizeMultiplier[HOURS_INNER]; 727 } 728 729 calculateGridSizesHours(); 730 calculateGridSizesMinutes(); 731 732 mSelectionRadius[HOURS] = (int) (mCircleRadius[HOURS] * mSelectionRadiusMultiplier); 733 mSelectionRadius[HOURS_INNER] = mSelectionRadius[HOURS]; 734 mSelectionRadius[MINUTES] = (int) (mCircleRadius[MINUTES] * mSelectionRadiusMultiplier); 735 736 mTouchHelper.invalidateRoot(); 737 } 738 739 @Override 740 public void onDraw(Canvas canvas) { 741 if (!mInputEnabled) { 742 canvas.saveLayerAlpha(0, 0, getWidth(), getHeight(), mDisabledAlpha); 743 } else { 744 canvas.save(); 745 } 746 747 calculateGridSizesHours(); 748 calculateGridSizesMinutes(); 749 750 drawCircleBackground(canvas); 751 drawSelector(canvas); 752 753 drawTextElements(canvas, mTextSize[HOURS], mTypeface, mOuterTextHours, 754 mTextGridWidths[HOURS], mTextGridHeights[HOURS], mPaint[HOURS], 755 mColor[HOURS], mAlpha[HOURS].getValue()); 756 757 if (mIs24HourMode && mInnerTextHours != null) { 758 drawTextElements(canvas, mInnerTextSize, mTypeface, mInnerTextHours, 759 mInnerTextGridWidths, mInnerTextGridHeights, mPaint[HOURS], 760 mColor[HOURS], mAlpha[HOURS].getValue()); 761 } 762 763 drawTextElements(canvas, mTextSize[MINUTES], mTypeface, mOuterTextMinutes, 764 mTextGridWidths[MINUTES], mTextGridHeights[MINUTES], mPaint[MINUTES], 765 mColor[MINUTES], mAlpha[MINUTES].getValue()); 766 767 drawCenter(canvas); 768 769 if (DEBUG) { 770 drawDebug(canvas); 771 } 772 773 canvas.restore(); 774 } 775 776 private void drawCircleBackground(Canvas canvas) { 777 canvas.drawCircle(mXCenter, mYCenter, mCircleRadius[HOURS], mPaintBackground); 778 } 779 780 private void drawCenter(Canvas canvas) { 781 canvas.drawCircle(mXCenter, mYCenter, CENTER_RADIUS, mPaintCenter); 782 } 783 784 private void drawSelector(Canvas canvas) { 785 drawSelector(canvas, mIsOnInnerCircle ? HOURS_INNER : HOURS); 786 drawSelector(canvas, MINUTES); 787 } 788 789 private int getMultipliedAlpha(int argb, int alpha) { 790 return (int) (Color.alpha(argb) * (alpha / 255.0) + 0.5); 791 } 792 793 private void drawSelector(Canvas canvas, int index) { 794 // Calculate the current radius at which to place the selection circle. 795 mLineLength[index] = (int) (mCircleRadius[index] 796 * mNumbersRadiusMultiplier[index] * mAnimationRadiusMultiplier[index]); 797 798 double selectionRadians = Math.toRadians(mSelectionDegrees[index]); 799 800 int pointX = mXCenter + (int) (mLineLength[index] * Math.sin(selectionRadians)); 801 int pointY = mYCenter - (int) (mLineLength[index] * Math.cos(selectionRadians)); 802 803 int color; 804 int alpha; 805 Paint paint; 806 807 // Draw the selection circle 808 color = mColorSelector[index % 2][SELECTOR_CIRCLE]; 809 alpha = mAlphaSelector[index % 2][SELECTOR_CIRCLE].getValue(); 810 paint = mPaintSelector[index % 2][SELECTOR_CIRCLE]; 811 paint.setColor(color); 812 paint.setAlpha(getMultipliedAlpha(color, alpha)); 813 canvas.drawCircle(pointX, pointY, mSelectionRadius[index], paint); 814 815 // Draw the dot if needed 816 if (mSelectionDegrees[index] % 30 != 0) { 817 // We're not on a direct tick 818 color = mColorSelector[index % 2][SELECTOR_DOT]; 819 alpha = mAlphaSelector[index % 2][SELECTOR_DOT].getValue(); 820 paint = mPaintSelector[index % 2][SELECTOR_DOT]; 821 paint.setColor(color); 822 paint.setAlpha(getMultipliedAlpha(color, alpha)); 823 canvas.drawCircle(pointX, pointY, (mSelectionRadius[index] * 2 / 7), paint); 824 } else { 825 // We're not drawing the dot, so shorten the line to only go as far as the edge of the 826 // selection circle 827 int lineLength = mLineLength[index] - mSelectionRadius[index]; 828 pointX = mXCenter + (int) (lineLength * Math.sin(selectionRadians)); 829 pointY = mYCenter - (int) (lineLength * Math.cos(selectionRadians)); 830 } 831 832 // Draw the line 833 color = mColorSelector[index % 2][SELECTOR_LINE]; 834 alpha = mAlphaSelector[index % 2][SELECTOR_LINE].getValue(); 835 paint = mPaintSelector[index % 2][SELECTOR_LINE]; 836 paint.setColor(color); 837 paint.setAlpha(getMultipliedAlpha(color, alpha)); 838 canvas.drawLine(mXCenter, mYCenter, pointX, pointY, paint); 839 } 840 841 private void drawDebug(Canvas canvas) { 842 // Draw outer numbers circle 843 final float outerRadius = mCircleRadius[HOURS] * mNumbersRadiusMultiplier[HOURS]; 844 canvas.drawCircle(mXCenter, mYCenter, outerRadius, mPaintDebug); 845 846 // Draw inner numbers circle 847 final float innerRadius = mCircleRadius[HOURS] * mNumbersRadiusMultiplier[HOURS_INNER]; 848 canvas.drawCircle(mXCenter, mYCenter, innerRadius, mPaintDebug); 849 850 // Draw outer background circle 851 canvas.drawCircle(mXCenter, mYCenter, mCircleRadius[HOURS], mPaintDebug); 852 853 // Draw outer rectangle for circles 854 float left = mXCenter - outerRadius; 855 float top = mYCenter - outerRadius; 856 float right = mXCenter + outerRadius; 857 float bottom = mYCenter + outerRadius; 858 canvas.drawRect(left, top, right, bottom, mPaintDebug); 859 860 // Draw outer rectangle for background 861 left = mXCenter - mCircleRadius[HOURS]; 862 top = mYCenter - mCircleRadius[HOURS]; 863 right = mXCenter + mCircleRadius[HOURS]; 864 bottom = mYCenter + mCircleRadius[HOURS]; 865 canvas.drawRect(left, top, right, bottom, mPaintDebug); 866 867 // Draw outer view rectangle 868 canvas.drawRect(0, 0, getWidth(), getHeight(), mPaintDebug); 869 870 // Draw selected time 871 final String selected = String.format("%02d:%02d", getCurrentHour(), getCurrentMinute()); 872 873 ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 874 ViewGroup.LayoutParams.WRAP_CONTENT); 875 TextView tv = new TextView(getContext()); 876 tv.setLayoutParams(lp); 877 tv.setText(selected); 878 tv.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); 879 Paint paint = tv.getPaint(); 880 paint.setColor(DEBUG_TEXT_COLOR); 881 882 final int width = tv.getMeasuredWidth(); 883 884 float height = paint.descent() - paint.ascent(); 885 float x = mXCenter - width / 2; 886 float y = mYCenter + 1.5f * height; 887 888 canvas.drawText(selected, x, y, paint); 889 } 890 891 private void calculateGridSizesHours() { 892 // Calculate the text positions 893 float numbersRadius = mCircleRadius[HOURS] 894 * mNumbersRadiusMultiplier[HOURS] * mAnimationRadiusMultiplier[HOURS]; 895 896 // Calculate the positions for the 12 numbers in the main circle. 897 calculateGridSizes(mPaint[HOURS], numbersRadius, mXCenter, mYCenter, 898 mTextSize[HOURS], mTextGridHeights[HOURS], mTextGridWidths[HOURS]); 899 900 // If we have an inner circle, calculate those positions too. 901 if (mIs24HourMode) { 902 float innerNumbersRadius = mCircleRadius[HOURS_INNER] 903 * mNumbersRadiusMultiplier[HOURS_INNER] 904 * mAnimationRadiusMultiplier[HOURS_INNER]; 905 906 calculateGridSizes(mPaint[HOURS], innerNumbersRadius, mXCenter, mYCenter, 907 mInnerTextSize, mInnerTextGridHeights, mInnerTextGridWidths); 908 } 909 } 910 911 private void calculateGridSizesMinutes() { 912 // Calculate the text positions 913 float numbersRadius = mCircleRadius[MINUTES] 914 * mNumbersRadiusMultiplier[MINUTES] * mAnimationRadiusMultiplier[MINUTES]; 915 916 // Calculate the positions for the 12 numbers in the main circle. 917 calculateGridSizes(mPaint[MINUTES], numbersRadius, mXCenter, mYCenter, 918 mTextSize[MINUTES], mTextGridHeights[MINUTES], mTextGridWidths[MINUTES]); 919 } 920 921 922 /** 923 * Using the trigonometric Unit Circle, calculate the positions that the text will need to be 924 * drawn at based on the specified circle radius. Place the values in the textGridHeights and 925 * textGridWidths parameters. 926 */ 927 private static void calculateGridSizes(Paint paint, float numbersRadius, float xCenter, 928 float yCenter, float textSize, float[] textGridHeights, float[] textGridWidths) { 929 /* 930 * The numbers need to be drawn in a 7x7 grid, representing the points on the Unit Circle. 931 */ 932 final float offset1 = numbersRadius; 933 // cos(30) = a / r => r * cos(30) 934 final float offset2 = numbersRadius * COSINE_30_DEGREES; 935 // sin(30) = o / r => r * sin(30) 936 final float offset3 = numbersRadius * SINE_30_DEGREES; 937 938 paint.setTextSize(textSize); 939 // We'll need yTextBase to be slightly lower to account for the text's baseline. 940 yCenter -= (paint.descent() + paint.ascent()) / 2; 941 942 textGridHeights[0] = yCenter - offset1; 943 textGridWidths[0] = xCenter - offset1; 944 textGridHeights[1] = yCenter - offset2; 945 textGridWidths[1] = xCenter - offset2; 946 textGridHeights[2] = yCenter - offset3; 947 textGridWidths[2] = xCenter - offset3; 948 textGridHeights[3] = yCenter; 949 textGridWidths[3] = xCenter; 950 textGridHeights[4] = yCenter + offset3; 951 textGridWidths[4] = xCenter + offset3; 952 textGridHeights[5] = yCenter + offset2; 953 textGridWidths[5] = xCenter + offset2; 954 textGridHeights[6] = yCenter + offset1; 955 textGridWidths[6] = xCenter + offset1; 956 } 957 958 /** 959 * Draw the 12 text values at the positions specified by the textGrid parameters. 960 */ 961 private void drawTextElements(Canvas canvas, float textSize, Typeface typeface, String[] texts, 962 float[] textGridWidths, float[] textGridHeights, Paint paint, int color, int alpha) { 963 paint.setTextSize(textSize); 964 paint.setTypeface(typeface); 965 paint.setColor(color); 966 paint.setAlpha(getMultipliedAlpha(color, alpha)); 967 canvas.drawText(texts[0], textGridWidths[3], textGridHeights[0], paint); 968 canvas.drawText(texts[1], textGridWidths[4], textGridHeights[1], paint); 969 canvas.drawText(texts[2], textGridWidths[5], textGridHeights[2], paint); 970 canvas.drawText(texts[3], textGridWidths[6], textGridHeights[3], paint); 971 canvas.drawText(texts[4], textGridWidths[5], textGridHeights[4], paint); 972 canvas.drawText(texts[5], textGridWidths[4], textGridHeights[5], paint); 973 canvas.drawText(texts[6], textGridWidths[3], textGridHeights[6], paint); 974 canvas.drawText(texts[7], textGridWidths[2], textGridHeights[5], paint); 975 canvas.drawText(texts[8], textGridWidths[1], textGridHeights[4], paint); 976 canvas.drawText(texts[9], textGridWidths[0], textGridHeights[3], paint); 977 canvas.drawText(texts[10], textGridWidths[1], textGridHeights[2], paint); 978 canvas.drawText(texts[11], textGridWidths[2], textGridHeights[1], paint); 979 } 980 981 // Used for animating the hours by changing their radius 982 @SuppressWarnings("unused") 983 private void setAnimationRadiusMultiplierHours(float animationRadiusMultiplier) { 984 mAnimationRadiusMultiplier[HOURS] = animationRadiusMultiplier; 985 mAnimationRadiusMultiplier[HOURS_INNER] = animationRadiusMultiplier; 986 } 987 988 // Used for animating the minutes by changing their radius 989 @SuppressWarnings("unused") 990 private void setAnimationRadiusMultiplierMinutes(float animationRadiusMultiplier) { 991 mAnimationRadiusMultiplier[MINUTES] = animationRadiusMultiplier; 992 } 993 994 private static ObjectAnimator getRadiusDisappearAnimator(Object target, 995 String radiusPropertyName, InvalidateUpdateListener updateListener, 996 float midRadiusMultiplier, float endRadiusMultiplier) { 997 Keyframe kf0, kf1, kf2; 998 float midwayPoint = 0.2f; 999 int duration = 500; 1000 1001 kf0 = Keyframe.ofFloat(0f, 1); 1002 kf1 = Keyframe.ofFloat(midwayPoint, midRadiusMultiplier); 1003 kf2 = Keyframe.ofFloat(1f, endRadiusMultiplier); 1004 PropertyValuesHolder radiusDisappear = PropertyValuesHolder.ofKeyframe( 1005 radiusPropertyName, kf0, kf1, kf2); 1006 1007 ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder( 1008 target, radiusDisappear).setDuration(duration); 1009 animator.addUpdateListener(updateListener); 1010 return animator; 1011 } 1012 1013 private static ObjectAnimator getRadiusReappearAnimator(Object target, 1014 String radiusPropertyName, InvalidateUpdateListener updateListener, 1015 float midRadiusMultiplier, float endRadiusMultiplier) { 1016 Keyframe kf0, kf1, kf2, kf3; 1017 float midwayPoint = 0.2f; 1018 int duration = 500; 1019 1020 // Set up animator for reappearing. 1021 float delayMultiplier = 0.25f; 1022 float transitionDurationMultiplier = 1f; 1023 float totalDurationMultiplier = transitionDurationMultiplier + delayMultiplier; 1024 int totalDuration = (int) (duration * totalDurationMultiplier); 1025 float delayPoint = (delayMultiplier * duration) / totalDuration; 1026 midwayPoint = 1 - (midwayPoint * (1 - delayPoint)); 1027 1028 kf0 = Keyframe.ofFloat(0f, endRadiusMultiplier); 1029 kf1 = Keyframe.ofFloat(delayPoint, endRadiusMultiplier); 1030 kf2 = Keyframe.ofFloat(midwayPoint, midRadiusMultiplier); 1031 kf3 = Keyframe.ofFloat(1f, 1); 1032 PropertyValuesHolder radiusReappear = PropertyValuesHolder.ofKeyframe( 1033 radiusPropertyName, kf0, kf1, kf2, kf3); 1034 1035 ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder( 1036 target, radiusReappear).setDuration(totalDuration); 1037 animator.addUpdateListener(updateListener); 1038 return animator; 1039 } 1040 1041 private static ObjectAnimator getFadeOutAnimator(IntHolder target, int startAlpha, int endAlpha, 1042 InvalidateUpdateListener updateListener) { 1043 int duration = 500; 1044 ObjectAnimator animator = ObjectAnimator.ofInt(target, "value", startAlpha, endAlpha); 1045 animator.setDuration(duration); 1046 animator.addUpdateListener(updateListener); 1047 1048 return animator; 1049 } 1050 1051 private static ObjectAnimator getFadeInAnimator(IntHolder target, int startAlpha, int endAlpha, 1052 InvalidateUpdateListener updateListener) { 1053 Keyframe kf0, kf1, kf2; 1054 int duration = 500; 1055 1056 // Set up animator for reappearing. 1057 float delayMultiplier = 0.25f; 1058 float transitionDurationMultiplier = 1f; 1059 float totalDurationMultiplier = transitionDurationMultiplier + delayMultiplier; 1060 int totalDuration = (int) (duration * totalDurationMultiplier); 1061 float delayPoint = (delayMultiplier * duration) / totalDuration; 1062 1063 kf0 = Keyframe.ofInt(0f, startAlpha); 1064 kf1 = Keyframe.ofInt(delayPoint, startAlpha); 1065 kf2 = Keyframe.ofInt(1f, endAlpha); 1066 PropertyValuesHolder fadeIn = PropertyValuesHolder.ofKeyframe("value", kf0, kf1, kf2); 1067 1068 ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder( 1069 target, fadeIn).setDuration(totalDuration); 1070 animator.addUpdateListener(updateListener); 1071 return animator; 1072 } 1073 1074 private class InvalidateUpdateListener implements ValueAnimator.AnimatorUpdateListener { 1075 @Override 1076 public void onAnimationUpdate(ValueAnimator animation) { 1077 RadialTimePickerView.this.invalidate(); 1078 } 1079 } 1080 1081 private void startHoursToMinutesAnimation() { 1082 if (mHoursToMinutesAnims.size() == 0) { 1083 mHoursToMinutesAnims.add(getRadiusDisappearAnimator(this, 1084 "animationRadiusMultiplierHours", mInvalidateUpdateListener, 1085 mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier)); 1086 mHoursToMinutesAnims.add(getFadeOutAnimator(mAlpha[HOURS], 1087 ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); 1088 mHoursToMinutesAnims.add(getFadeOutAnimator(mAlphaSelector[HOURS][SELECTOR_CIRCLE], 1089 ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); 1090 mHoursToMinutesAnims.add(getFadeOutAnimator(mAlphaSelector[HOURS][SELECTOR_DOT], 1091 ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); 1092 mHoursToMinutesAnims.add(getFadeOutAnimator(mAlphaSelector[HOURS][SELECTOR_LINE], 1093 ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); 1094 1095 mHoursToMinutesAnims.add(getRadiusReappearAnimator(this, 1096 "animationRadiusMultiplierMinutes", mInvalidateUpdateListener, 1097 mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier)); 1098 mHoursToMinutesAnims.add(getFadeInAnimator(mAlpha[MINUTES], 1099 ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); 1100 mHoursToMinutesAnims.add(getFadeInAnimator(mAlphaSelector[MINUTES][SELECTOR_CIRCLE], 1101 ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener)); 1102 mHoursToMinutesAnims.add(getFadeInAnimator(mAlphaSelector[MINUTES][SELECTOR_DOT], 1103 ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); 1104 mHoursToMinutesAnims.add(getFadeInAnimator(mAlphaSelector[MINUTES][SELECTOR_LINE], 1105 ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener)); 1106 } 1107 1108 if (mTransition != null && mTransition.isRunning()) { 1109 mTransition.end(); 1110 } 1111 mTransition = new AnimatorSet(); 1112 mTransition.playTogether(mHoursToMinutesAnims); 1113 mTransition.start(); 1114 } 1115 1116 private void startMinutesToHoursAnimation() { 1117 if (mMinuteToHoursAnims.size() == 0) { 1118 mMinuteToHoursAnims.add(getRadiusDisappearAnimator(this, 1119 "animationRadiusMultiplierMinutes", mInvalidateUpdateListener, 1120 mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier)); 1121 mMinuteToHoursAnims.add(getFadeOutAnimator(mAlpha[MINUTES], 1122 ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); 1123 mMinuteToHoursAnims.add(getFadeOutAnimator(mAlphaSelector[MINUTES][SELECTOR_CIRCLE], 1124 ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); 1125 mMinuteToHoursAnims.add(getFadeOutAnimator(mAlphaSelector[MINUTES][SELECTOR_DOT], 1126 ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); 1127 mMinuteToHoursAnims.add(getFadeOutAnimator(mAlphaSelector[MINUTES][SELECTOR_LINE], 1128 ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); 1129 1130 mMinuteToHoursAnims.add(getRadiusReappearAnimator(this, 1131 "animationRadiusMultiplierHours", mInvalidateUpdateListener, 1132 mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier)); 1133 mMinuteToHoursAnims.add(getFadeInAnimator(mAlpha[HOURS], 1134 ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); 1135 mMinuteToHoursAnims.add(getFadeInAnimator(mAlphaSelector[HOURS][SELECTOR_CIRCLE], 1136 ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener)); 1137 mMinuteToHoursAnims.add(getFadeInAnimator(mAlphaSelector[HOURS][SELECTOR_DOT], 1138 ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); 1139 mMinuteToHoursAnims.add(getFadeInAnimator(mAlphaSelector[HOURS][SELECTOR_LINE], 1140 ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener)); 1141 } 1142 1143 if (mTransition != null && mTransition.isRunning()) { 1144 mTransition.end(); 1145 } 1146 mTransition = new AnimatorSet(); 1147 mTransition.playTogether(mMinuteToHoursAnims); 1148 mTransition.start(); 1149 } 1150 1151 private int getDegreesFromXY(float x, float y) { 1152 final double hypotenuse = Math.sqrt( 1153 (y - mYCenter) * (y - mYCenter) + (x - mXCenter) * (x - mXCenter)); 1154 1155 // Basic check if we're outside the range of the disk 1156 if (hypotenuse > mCircleRadius[HOURS]) { 1157 return -1; 1158 } 1159 // Check 1160 if (mIs24HourMode && mShowHours) { 1161 if (hypotenuse >= mMinHypotenuseForInnerNumber 1162 && hypotenuse <= mHalfwayHypotenusePoint) { 1163 mIsOnInnerCircle = true; 1164 } else if (hypotenuse <= mMaxHypotenuseForOuterNumber 1165 && hypotenuse >= mHalfwayHypotenusePoint) { 1166 mIsOnInnerCircle = false; 1167 } else { 1168 return -1; 1169 } 1170 } else { 1171 final int index = (mShowHours) ? HOURS : MINUTES; 1172 final float length = (mCircleRadius[index] * mNumbersRadiusMultiplier[index]); 1173 final int distanceToNumber = (int) Math.abs(hypotenuse - length); 1174 final int maxAllowedDistance = 1175 (int) (mCircleRadius[index] * (1 - mNumbersRadiusMultiplier[index])); 1176 if (distanceToNumber > maxAllowedDistance) { 1177 return -1; 1178 } 1179 } 1180 1181 final float opposite = Math.abs(y - mYCenter); 1182 int degrees = (int) (Math.toDegrees(Math.asin(opposite / hypotenuse)) + 0.5); 1183 1184 // Now we have to translate to the correct quadrant. 1185 final boolean rightSide = (x > mXCenter); 1186 final boolean topSide = (y < mYCenter); 1187 if (rightSide) { 1188 if (topSide) { 1189 degrees = 90 - degrees; 1190 } else { 1191 degrees = 90 + degrees; 1192 } 1193 } else { 1194 if (topSide) { 1195 degrees = 270 + degrees; 1196 } else { 1197 degrees = 270 - degrees; 1198 } 1199 } 1200 return degrees; 1201 } 1202 1203 boolean mChangedDuringTouch = false; 1204 1205 @Override 1206 public boolean onTouch(View v, MotionEvent event) { 1207 if (!mInputEnabled) { 1208 return true; 1209 } 1210 1211 final int action = event.getActionMasked(); 1212 if (action == MotionEvent.ACTION_MOVE 1213 || action == MotionEvent.ACTION_UP 1214 || action == MotionEvent.ACTION_DOWN) { 1215 boolean forceSelection = false; 1216 boolean autoAdvance = false; 1217 1218 if (action == MotionEvent.ACTION_DOWN) { 1219 // This is a new event stream, reset whether the value changed. 1220 mChangedDuringTouch = false; 1221 } else if (action == MotionEvent.ACTION_UP) { 1222 autoAdvance = true; 1223 1224 // If we saw a down/up pair without the value changing, assume 1225 // this is a single-tap selection and force a change. 1226 if (!mChangedDuringTouch) { 1227 forceSelection = true; 1228 } 1229 } 1230 1231 mChangedDuringTouch |= handleTouchInput( 1232 event.getX(), event.getY(), forceSelection, autoAdvance); 1233 } 1234 1235 return true; 1236 } 1237 1238 private boolean handleTouchInput( 1239 float x, float y, boolean forceSelection, boolean autoAdvance) { 1240 // Calling getDegreesFromXY has side effects, so cache 1241 // whether we used to be on the inner circle. 1242 final boolean wasOnInnerCircle = mIsOnInnerCircle; 1243 final int degrees = getDegreesFromXY(x, y); 1244 if (degrees == -1) { 1245 return false; 1246 } 1247 1248 final int[] selectionDegrees = mSelectionDegrees; 1249 final int type; 1250 final int newValue; 1251 final boolean valueChanged; 1252 1253 if (mShowHours) { 1254 final int snapDegrees = snapOnly30s(degrees, 0) % 360; 1255 valueChanged = selectionDegrees[HOURS] != snapDegrees 1256 || selectionDegrees[HOURS_INNER] != snapDegrees 1257 || wasOnInnerCircle != mIsOnInnerCircle; 1258 1259 selectionDegrees[HOURS] = snapDegrees; 1260 selectionDegrees[HOURS_INNER] = snapDegrees; 1261 type = HOURS; 1262 newValue = getCurrentHour(); 1263 } else { 1264 final int snapDegrees = snapPrefer30s(degrees) % 360; 1265 valueChanged = selectionDegrees[MINUTES] != snapDegrees; 1266 1267 selectionDegrees[MINUTES] = snapDegrees; 1268 type = MINUTES; 1269 newValue = getCurrentMinute(); 1270 } 1271 1272 if (valueChanged || forceSelection || autoAdvance) { 1273 // Fire the listener even if we just need to auto-advance. 1274 if (mListener != null) { 1275 mListener.onValueSelected(type, newValue, autoAdvance); 1276 } 1277 1278 // Only provide feedback if the value actually changed. 1279 if (valueChanged || forceSelection) { 1280 performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK); 1281 invalidate(); 1282 } 1283 return true; 1284 } 1285 1286 return false; 1287 } 1288 1289 @Override 1290 public boolean dispatchHoverEvent(MotionEvent event) { 1291 // First right-of-refusal goes the touch exploration helper. 1292 if (mTouchHelper.dispatchHoverEvent(event)) { 1293 return true; 1294 } 1295 return super.dispatchHoverEvent(event); 1296 } 1297 1298 public void setInputEnabled(boolean inputEnabled) { 1299 mInputEnabled = inputEnabled; 1300 invalidate(); 1301 } 1302 1303 private class RadialPickerTouchHelper extends ExploreByTouchHelper { 1304 private final Rect mTempRect = new Rect(); 1305 1306 private final int TYPE_HOUR = 1; 1307 private final int TYPE_MINUTE = 2; 1308 1309 private final int SHIFT_TYPE = 0; 1310 private final int MASK_TYPE = 0xF; 1311 1312 private final int SHIFT_VALUE = 8; 1313 private final int MASK_VALUE = 0xFF; 1314 1315 /** Increment in which virtual views are exposed for minutes. */ 1316 private final int MINUTE_INCREMENT = 5; 1317 1318 public RadialPickerTouchHelper() { 1319 super(RadialTimePickerView.this); 1320 } 1321 1322 @Override 1323 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { 1324 super.onInitializeAccessibilityNodeInfo(host, info); 1325 1326 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD); 1327 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD); 1328 } 1329 1330 @Override 1331 public boolean performAccessibilityAction(View host, int action, Bundle arguments) { 1332 if (super.performAccessibilityAction(host, action, arguments)) { 1333 return true; 1334 } 1335 1336 switch (action) { 1337 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: 1338 adjustPicker(1); 1339 return true; 1340 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: 1341 adjustPicker(-1); 1342 return true; 1343 } 1344 1345 return false; 1346 } 1347 1348 private void adjustPicker(int step) { 1349 final int stepSize; 1350 final int initialValue; 1351 final int maxValue; 1352 final int minValue; 1353 if (mShowHours) { 1354 stepSize = DEGREES_FOR_ONE_HOUR; 1355 initialValue = getCurrentHour() % 12; 1356 1357 if (mIs24HourMode) { 1358 maxValue = 23; 1359 minValue = 0; 1360 } else { 1361 maxValue = 12; 1362 minValue = 1; 1363 } 1364 } else { 1365 stepSize = DEGREES_FOR_ONE_MINUTE; 1366 initialValue = getCurrentMinute(); 1367 1368 maxValue = 55; 1369 minValue = 0; 1370 } 1371 1372 final int steppedValue = snapOnly30s(initialValue * stepSize, step) / stepSize; 1373 final int clampedValue = MathUtils.constrain(steppedValue, minValue, maxValue); 1374 if (mShowHours) { 1375 setCurrentHour(clampedValue); 1376 } else { 1377 setCurrentMinute(clampedValue); 1378 } 1379 } 1380 1381 @Override 1382 protected int getVirtualViewAt(float x, float y) { 1383 final int id; 1384 1385 // Calling getDegreesXY() has side-effects, so we need to cache the 1386 // current inner circle value and restore after the call. 1387 final boolean wasOnInnerCircle = mIsOnInnerCircle; 1388 final int degrees = getDegreesFromXY(x, y); 1389 final boolean isOnInnerCircle = mIsOnInnerCircle; 1390 mIsOnInnerCircle = wasOnInnerCircle; 1391 1392 if (degrees != -1) { 1393 final int snapDegrees = snapOnly30s(degrees, 0) % 360; 1394 if (mShowHours) { 1395 final int hour24 = getHourForDegrees(snapDegrees, isOnInnerCircle); 1396 final int hour = mIs24HourMode ? hour24 : hour24To12(hour24); 1397 id = makeId(TYPE_HOUR, hour); 1398 } else { 1399 final int current = getCurrentMinute(); 1400 final int touched = getMinuteForDegrees(degrees); 1401 final int snapped = getMinuteForDegrees(snapDegrees); 1402 1403 // If the touched minute is closer to the current minute 1404 // than it is to the snapped minute, return current. 1405 final int minute; 1406 if (Math.abs(current - touched) < Math.abs(snapped - touched)) { 1407 minute = current; 1408 } else { 1409 minute = snapped; 1410 } 1411 id = makeId(TYPE_MINUTE, minute); 1412 } 1413 } else { 1414 id = INVALID_ID; 1415 } 1416 1417 return id; 1418 } 1419 1420 @Override 1421 protected void getVisibleVirtualViews(IntArray virtualViewIds) { 1422 if (mShowHours) { 1423 final int min = mIs24HourMode ? 0 : 1; 1424 final int max = mIs24HourMode ? 23 : 12; 1425 for (int i = min; i <= max ; i++) { 1426 virtualViewIds.add(makeId(TYPE_HOUR, i)); 1427 } 1428 } else { 1429 final int current = getCurrentMinute(); 1430 for (int i = 0; i < 60; i += MINUTE_INCREMENT) { 1431 virtualViewIds.add(makeId(TYPE_MINUTE, i)); 1432 1433 // If the current minute falls between two increments, 1434 // insert an extra node for it. 1435 if (current > i && current < i + MINUTE_INCREMENT) { 1436 virtualViewIds.add(makeId(TYPE_MINUTE, current)); 1437 } 1438 } 1439 } 1440 } 1441 1442 @Override 1443 protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { 1444 event.setClassName(getClass().getName()); 1445 1446 final int type = getTypeFromId(virtualViewId); 1447 final int value = getValueFromId(virtualViewId); 1448 final CharSequence description = getVirtualViewDescription(type, value); 1449 event.setContentDescription(description); 1450 } 1451 1452 @Override 1453 protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) { 1454 node.setClassName(getClass().getName()); 1455 node.addAction(AccessibilityAction.ACTION_CLICK); 1456 1457 final int type = getTypeFromId(virtualViewId); 1458 final int value = getValueFromId(virtualViewId); 1459 final CharSequence description = getVirtualViewDescription(type, value); 1460 node.setContentDescription(description); 1461 1462 getBoundsForVirtualView(virtualViewId, mTempRect); 1463 node.setBoundsInParent(mTempRect); 1464 1465 final boolean selected = isVirtualViewSelected(type, value); 1466 node.setSelected(selected); 1467 1468 final int nextId = getVirtualViewIdAfter(type, value); 1469 if (nextId != INVALID_ID) { 1470 node.setTraversalBefore(RadialTimePickerView.this, nextId); 1471 } 1472 } 1473 1474 private int getVirtualViewIdAfter(int type, int value) { 1475 if (type == TYPE_HOUR) { 1476 final int nextValue = value + 1; 1477 final int max = mIs24HourMode ? 23 : 12; 1478 if (nextValue <= max) { 1479 return makeId(type, nextValue); 1480 } 1481 } else if (type == TYPE_MINUTE) { 1482 final int current = getCurrentMinute(); 1483 final int snapValue = value - (value % MINUTE_INCREMENT); 1484 final int nextValue = snapValue + MINUTE_INCREMENT; 1485 if (value < current && nextValue > current) { 1486 // The current value is between two snap values. 1487 return makeId(type, current); 1488 } else if (nextValue < 60) { 1489 return makeId(type, nextValue); 1490 } 1491 } 1492 return INVALID_ID; 1493 } 1494 1495 @Override 1496 protected boolean onPerformActionForVirtualView(int virtualViewId, int action, 1497 Bundle arguments) { 1498 if (action == AccessibilityNodeInfo.ACTION_CLICK) { 1499 final int type = getTypeFromId(virtualViewId); 1500 final int value = getValueFromId(virtualViewId); 1501 if (type == TYPE_HOUR) { 1502 final int hour = mIs24HourMode ? value : hour12To24(value, mAmOrPm); 1503 setCurrentHour(hour); 1504 return true; 1505 } else if (type == TYPE_MINUTE) { 1506 setCurrentMinute(value); 1507 return true; 1508 } 1509 } 1510 return false; 1511 } 1512 1513 private int hour12To24(int hour12, int amOrPm) { 1514 int hour24 = hour12; 1515 if (hour12 == 12) { 1516 if (amOrPm == AM) { 1517 hour24 = 0; 1518 } 1519 } else if (amOrPm == PM) { 1520 hour24 += 12; 1521 } 1522 return hour24; 1523 } 1524 1525 private int hour24To12(int hour24) { 1526 if (hour24 == 0) { 1527 return 12; 1528 } else if (hour24 > 12) { 1529 return hour24 - 12; 1530 } else { 1531 return hour24; 1532 } 1533 } 1534 1535 private void getBoundsForVirtualView(int virtualViewId, Rect bounds) { 1536 final float radius; 1537 final int type = getTypeFromId(virtualViewId); 1538 final int value = getValueFromId(virtualViewId); 1539 final float centerRadius; 1540 final float degrees; 1541 if (type == TYPE_HOUR) { 1542 final boolean innerCircle = mIs24HourMode && value > 0 && value <= 12; 1543 if (innerCircle) { 1544 centerRadius = mCircleRadius[HOURS_INNER] * mNumbersRadiusMultiplier[HOURS_INNER]; 1545 radius = mSelectionRadius[HOURS_INNER]; 1546 } else { 1547 centerRadius = mCircleRadius[HOURS] * mNumbersRadiusMultiplier[HOURS]; 1548 radius = mSelectionRadius[HOURS]; 1549 } 1550 1551 degrees = getDegreesForHour(value); 1552 } else if (type == TYPE_MINUTE) { 1553 centerRadius = mCircleRadius[MINUTES] * mNumbersRadiusMultiplier[MINUTES]; 1554 degrees = getDegreesForMinute(value); 1555 radius = mSelectionRadius[MINUTES]; 1556 } else { 1557 // This should never happen. 1558 centerRadius = 0; 1559 degrees = 0; 1560 radius = 0; 1561 } 1562 1563 final double radians = Math.toRadians(degrees); 1564 final float xCenter = mXCenter + centerRadius * (float) Math.sin(radians); 1565 final float yCenter = mYCenter - centerRadius * (float) Math.cos(radians); 1566 1567 bounds.set((int) (xCenter - radius), (int) (yCenter - radius), 1568 (int) (xCenter + radius), (int) (yCenter + radius)); 1569 } 1570 1571 private CharSequence getVirtualViewDescription(int type, int value) { 1572 final CharSequence description; 1573 if (type == TYPE_HOUR || type == TYPE_MINUTE) { 1574 description = Integer.toString(value); 1575 } else { 1576 description = null; 1577 } 1578 return description; 1579 } 1580 1581 private boolean isVirtualViewSelected(int type, int value) { 1582 final boolean selected; 1583 if (type == TYPE_HOUR) { 1584 selected = getCurrentHour() == value; 1585 } else if (type == TYPE_MINUTE) { 1586 selected = getCurrentMinute() == value; 1587 } else { 1588 selected = false; 1589 } 1590 return selected; 1591 } 1592 1593 private int makeId(int type, int value) { 1594 return type << SHIFT_TYPE | value << SHIFT_VALUE; 1595 } 1596 1597 private int getTypeFromId(int id) { 1598 return id >>> SHIFT_TYPE & MASK_TYPE; 1599 } 1600 1601 private int getValueFromId(int id) { 1602 return id >>> SHIFT_VALUE & MASK_VALUE; 1603 } 1604 } 1605 1606 private static class IntHolder { 1607 private int mValue; 1608 1609 public IntHolder(int value) { 1610 mValue = value; 1611 } 1612 1613 public void setValue(int value) { 1614 mValue = value; 1615 } 1616 1617 public int getValue() { 1618 return mValue; 1619 } 1620 } 1621} 1622