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