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