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