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