1 /* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.widget; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.app.AppGlobals; 22 import android.compat.annotation.UnsupportedAppUsage; 23 import android.content.BroadcastReceiver; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.res.ColorStateList; 27 import android.content.res.TypedArray; 28 import android.graphics.BlendMode; 29 import android.graphics.Canvas; 30 import android.graphics.drawable.Drawable; 31 import android.graphics.drawable.Icon; 32 import android.text.format.DateUtils; 33 import android.util.AttributeSet; 34 import android.util.Log; 35 import android.view.RemotableViewMethod; 36 import android.view.View; 37 import android.view.inspector.InspectableProperty; 38 import android.widget.RemoteViews.RemoteView; 39 import android.widget.TextClock.ClockEventDelegate; 40 41 import com.android.internal.util.Preconditions; 42 43 import java.time.Clock; 44 import java.time.DateTimeException; 45 import java.time.Duration; 46 import java.time.Instant; 47 import java.time.LocalTime; 48 import java.time.ZoneId; 49 import java.time.ZonedDateTime; 50 import java.util.Formatter; 51 import java.util.Locale; 52 53 /** 54 * This widget displays an analogic clock with two hands for hours and minutes. 55 * 56 * @attr ref android.R.styleable#AnalogClock_dial 57 * @attr ref android.R.styleable#AnalogClock_hand_hour 58 * @attr ref android.R.styleable#AnalogClock_hand_minute 59 * @attr ref android.R.styleable#AnalogClock_hand_second 60 * @attr ref android.R.styleable#AnalogClock_timeZone 61 * @deprecated This widget is no longer supported; except for 62 * {@link android.widget.RemoteViews} use cases like 63 * <a href="https://developer.android.com/develop/ui/views/appwidgets/overview"> 64 * app widgets</a>. 65 * 66 */ 67 @RemoteView 68 @Deprecated 69 public class AnalogClock extends View { 70 private static final String LOG_TAG = "AnalogClock"; 71 72 /** How many times per second that the seconds hand advances. */ 73 private final int mSecondsHandFps; 74 75 private Clock mClock; 76 @Nullable 77 private ZoneId mTimeZone; 78 79 @UnsupportedAppUsage 80 private Drawable mHourHand; 81 private final TintInfo mHourHandTintInfo = new TintInfo(); 82 @UnsupportedAppUsage 83 private Drawable mMinuteHand; 84 private final TintInfo mMinuteHandTintInfo = new TintInfo(); 85 @Nullable 86 private Drawable mSecondHand; 87 private final TintInfo mSecondHandTintInfo = new TintInfo(); 88 @UnsupportedAppUsage 89 private Drawable mDial; 90 private final TintInfo mDialTintInfo = new TintInfo(); 91 92 private int mDialWidth; 93 private int mDialHeight; 94 95 private boolean mVisible; 96 97 private float mSeconds; 98 private float mMinutes; 99 private float mHour; 100 private boolean mChanged; 101 AnalogClock(Context context)102 public AnalogClock(Context context) { 103 this(context, null); 104 } 105 AnalogClock(Context context, AttributeSet attrs)106 public AnalogClock(Context context, AttributeSet attrs) { 107 this(context, attrs, 0); 108 } 109 AnalogClock(Context context, AttributeSet attrs, int defStyleAttr)110 public AnalogClock(Context context, AttributeSet attrs, int defStyleAttr) { 111 this(context, attrs, defStyleAttr, 0); 112 } 113 AnalogClock(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)114 public AnalogClock(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 115 super(context, attrs, defStyleAttr, defStyleRes); 116 117 mClockEventDelegate = new ClockEventDelegate(context); 118 mSecondsHandFps = AppGlobals.getIntCoreSetting( 119 WidgetFlags.KEY_ANALOG_CLOCK_SECONDS_HAND_FPS, 120 context.getResources() 121 .getInteger(com.android.internal.R.integer 122 .config_defaultAnalogClockSecondsHandFps)); 123 124 final TypedArray a = context.obtainStyledAttributes( 125 attrs, com.android.internal.R.styleable.AnalogClock, defStyleAttr, defStyleRes); 126 saveAttributeDataForStyleable(context, com.android.internal.R.styleable.AnalogClock, 127 attrs, a, defStyleAttr, defStyleRes); 128 129 mDial = a.getDrawable(com.android.internal.R.styleable.AnalogClock_dial); 130 if (mDial == null) { 131 mDial = context.getDrawable(com.android.internal.R.drawable.clock_dial); 132 } 133 134 ColorStateList dialTintList = a.getColorStateList( 135 com.android.internal.R.styleable.AnalogClock_dialTint); 136 if (dialTintList != null) { 137 mDialTintInfo.mTintList = dialTintList; 138 mDialTintInfo.mHasTintList = true; 139 } 140 BlendMode dialTintMode = Drawable.parseBlendMode( 141 a.getInt(com.android.internal.R.styleable.AnalogClock_dialTintMode, -1), 142 null); 143 if (dialTintMode != null) { 144 mDialTintInfo.mTintBlendMode = dialTintMode; 145 mDialTintInfo.mHasTintBlendMode = true; 146 } 147 if (mDialTintInfo.mHasTintList || mDialTintInfo.mHasTintBlendMode) { 148 mDial = mDialTintInfo.apply(mDial); 149 } 150 151 mHourHand = a.getDrawable(com.android.internal.R.styleable.AnalogClock_hand_hour); 152 if (mHourHand == null) { 153 mHourHand = context.getDrawable(com.android.internal.R.drawable.clock_hand_hour); 154 } 155 156 ColorStateList hourHandTintList = a.getColorStateList( 157 com.android.internal.R.styleable.AnalogClock_hand_hourTint); 158 if (hourHandTintList != null) { 159 mHourHandTintInfo.mTintList = hourHandTintList; 160 mHourHandTintInfo.mHasTintList = true; 161 } 162 BlendMode hourHandTintMode = Drawable.parseBlendMode( 163 a.getInt(com.android.internal.R.styleable.AnalogClock_hand_hourTintMode, -1), 164 null); 165 if (hourHandTintMode != null) { 166 mHourHandTintInfo.mTintBlendMode = hourHandTintMode; 167 mHourHandTintInfo.mHasTintBlendMode = true; 168 } 169 if (mHourHandTintInfo.mHasTintList || mHourHandTintInfo.mHasTintBlendMode) { 170 mHourHand = mHourHandTintInfo.apply(mHourHand); 171 } 172 173 mMinuteHand = a.getDrawable(com.android.internal.R.styleable.AnalogClock_hand_minute); 174 if (mMinuteHand == null) { 175 mMinuteHand = context.getDrawable(com.android.internal.R.drawable.clock_hand_minute); 176 } 177 178 ColorStateList minuteHandTintList = a.getColorStateList( 179 com.android.internal.R.styleable.AnalogClock_hand_minuteTint); 180 if (minuteHandTintList != null) { 181 mMinuteHandTintInfo.mTintList = minuteHandTintList; 182 mMinuteHandTintInfo.mHasTintList = true; 183 } 184 BlendMode minuteHandTintMode = Drawable.parseBlendMode( 185 a.getInt(com.android.internal.R.styleable.AnalogClock_hand_minuteTintMode, -1), 186 null); 187 if (minuteHandTintMode != null) { 188 mMinuteHandTintInfo.mTintBlendMode = minuteHandTintMode; 189 mMinuteHandTintInfo.mHasTintBlendMode = true; 190 } 191 if (mMinuteHandTintInfo.mHasTintList || mMinuteHandTintInfo.mHasTintBlendMode) { 192 mMinuteHand = mMinuteHandTintInfo.apply(mMinuteHand); 193 } 194 195 mSecondHand = a.getDrawable(com.android.internal.R.styleable.AnalogClock_hand_second); 196 197 ColorStateList secondHandTintList = a.getColorStateList( 198 com.android.internal.R.styleable.AnalogClock_hand_secondTint); 199 if (secondHandTintList != null) { 200 mSecondHandTintInfo.mTintList = secondHandTintList; 201 mSecondHandTintInfo.mHasTintList = true; 202 } 203 BlendMode secondHandTintMode = Drawable.parseBlendMode( 204 a.getInt(com.android.internal.R.styleable.AnalogClock_hand_secondTintMode, -1), 205 null); 206 if (secondHandTintMode != null) { 207 mSecondHandTintInfo.mTintBlendMode = secondHandTintMode; 208 mSecondHandTintInfo.mHasTintBlendMode = true; 209 } 210 if (mSecondHandTintInfo.mHasTintList || mSecondHandTintInfo.mHasTintBlendMode) { 211 mSecondHand = mSecondHandTintInfo.apply(mSecondHand); 212 } 213 214 mTimeZone = toZoneId(a.getString(com.android.internal.R.styleable.AnalogClock_timeZone)); 215 createClock(); 216 217 a.recycle(); 218 219 mDialWidth = mDial.getIntrinsicWidth(); 220 mDialHeight = mDial.getIntrinsicHeight(); 221 } 222 223 /** Sets the dial of the clock to the specified Icon. */ 224 @RemotableViewMethod setDial(@onNull Icon icon)225 public void setDial(@NonNull Icon icon) { 226 mDial = icon.loadDrawable(getContext()); 227 mDialWidth = mDial.getIntrinsicWidth(); 228 mDialHeight = mDial.getIntrinsicHeight(); 229 if (mDialTintInfo.mHasTintList || mDialTintInfo.mHasTintBlendMode) { 230 mDial = mDialTintInfo.apply(mDial); 231 } 232 233 mChanged = true; 234 invalidate(); 235 } 236 237 /** 238 * Applies a tint to the dial drawable. 239 * <p> 240 * Subsequent calls to {@link #setDial(Icon)} will 241 * automatically mutate the drawable and apply the specified tint and tint 242 * mode using {@link Drawable#setTintList(ColorStateList)}. 243 * 244 * @param tint the tint to apply, may be {@code null} to clear tint 245 * 246 * @attr ref android.R.styleable#AnalogClock_dialTint 247 * @see #getDialTintList() 248 * @see Drawable#setTintList(ColorStateList) 249 */ 250 @RemotableViewMethod setDialTintList(@ullable ColorStateList tint)251 public void setDialTintList(@Nullable ColorStateList tint) { 252 mDialTintInfo.mTintList = tint; 253 mDialTintInfo.mHasTintList = true; 254 255 mDial = mDialTintInfo.apply(mDial); 256 } 257 258 /** 259 * @return the tint applied to the dial drawable 260 * @attr ref android.R.styleable#AnalogClock_dialTint 261 * @see #setDialTintList(ColorStateList) 262 */ 263 @InspectableProperty(attributeId = com.android.internal.R.styleable.AnalogClock_dialTint) 264 @Nullable getDialTintList()265 public ColorStateList getDialTintList() { 266 return mDialTintInfo.mTintList; 267 } 268 269 /** 270 * Specifies the blending mode used to apply the tint specified by 271 * {@link #setDialTintList(ColorStateList)}} to the dial drawable. 272 * The default mode is {@link BlendMode#SRC_IN}. 273 * 274 * @param blendMode the blending mode used to apply the tint, may be 275 * {@code null} to clear tint 276 * @attr ref android.R.styleable#AnalogClock_dialTintMode 277 * @see #getDialTintBlendMode() 278 * @see Drawable#setTintBlendMode(BlendMode) 279 */ 280 @RemotableViewMethod setDialTintBlendMode(@ullable BlendMode blendMode)281 public void setDialTintBlendMode(@Nullable BlendMode blendMode) { 282 mDialTintInfo.mTintBlendMode = blendMode; 283 mDialTintInfo.mHasTintBlendMode = true; 284 285 mDial = mDialTintInfo.apply(mDial); 286 } 287 288 /** 289 * @return the blending mode used to apply the tint to the dial drawable 290 * @attr ref android.R.styleable#AnalogClock_dialTintMode 291 * @see #setDialTintBlendMode(BlendMode) 292 */ 293 @InspectableProperty(attributeId = com.android.internal.R.styleable.AnalogClock_dialTintMode) 294 @Nullable getDialTintBlendMode()295 public BlendMode getDialTintBlendMode() { 296 return mDialTintInfo.mTintBlendMode; 297 } 298 299 /** Sets the hour hand of the clock to the specified Icon. */ 300 @RemotableViewMethod setHourHand(@onNull Icon icon)301 public void setHourHand(@NonNull Icon icon) { 302 mHourHand = icon.loadDrawable(getContext()); 303 if (mHourHandTintInfo.mHasTintList || mHourHandTintInfo.mHasTintBlendMode) { 304 mHourHand = mHourHandTintInfo.apply(mHourHand); 305 } 306 307 mChanged = true; 308 invalidate(); 309 } 310 311 /** 312 * Applies a tint to the hour hand drawable. 313 * <p> 314 * Subsequent calls to {@link #setHourHand(Icon)} will 315 * automatically mutate the drawable and apply the specified tint and tint 316 * mode using {@link Drawable#setTintList(ColorStateList)}. 317 * 318 * @param tint the tint to apply, may be {@code null} to clear tint 319 * 320 * @attr ref android.R.styleable#AnalogClock_hand_hourTint 321 * @see #getHourHandTintList() 322 * @see Drawable#setTintList(ColorStateList) 323 */ 324 @RemotableViewMethod setHourHandTintList(@ullable ColorStateList tint)325 public void setHourHandTintList(@Nullable ColorStateList tint) { 326 mHourHandTintInfo.mTintList = tint; 327 mHourHandTintInfo.mHasTintList = true; 328 329 mHourHand = mHourHandTintInfo.apply(mHourHand); 330 } 331 332 /** 333 * @return the tint applied to the hour hand drawable 334 * @attr ref android.R.styleable#AnalogClock_hand_hourTint 335 * @see #setHourHandTintList(ColorStateList) 336 */ 337 @InspectableProperty( 338 attributeId = com.android.internal.R.styleable.AnalogClock_hand_hourTint 339 ) 340 @Nullable getHourHandTintList()341 public ColorStateList getHourHandTintList() { 342 return mHourHandTintInfo.mTintList; 343 } 344 345 /** 346 * Specifies the blending mode used to apply the tint specified by 347 * {@link #setHourHandTintList(ColorStateList)}} to the hour hand drawable. 348 * The default mode is {@link BlendMode#SRC_IN}. 349 * 350 * @param blendMode the blending mode used to apply the tint, may be 351 * {@code null} to clear tint 352 * @attr ref android.R.styleable#AnalogClock_hand_hourTintMode 353 * @see #getHourHandTintBlendMode() 354 * @see Drawable#setTintBlendMode(BlendMode) 355 */ 356 @RemotableViewMethod setHourHandTintBlendMode(@ullable BlendMode blendMode)357 public void setHourHandTintBlendMode(@Nullable BlendMode blendMode) { 358 mHourHandTintInfo.mTintBlendMode = blendMode; 359 mHourHandTintInfo.mHasTintBlendMode = true; 360 361 mHourHand = mHourHandTintInfo.apply(mHourHand); 362 } 363 364 /** 365 * @return the blending mode used to apply the tint to the hour hand drawable 366 * @attr ref android.R.styleable#AnalogClock_hand_hourTintMode 367 * @see #setHourHandTintBlendMode(BlendMode) 368 */ 369 @InspectableProperty( 370 attributeId = com.android.internal.R.styleable.AnalogClock_hand_hourTintMode) 371 @Nullable getHourHandTintBlendMode()372 public BlendMode getHourHandTintBlendMode() { 373 return mHourHandTintInfo.mTintBlendMode; 374 } 375 376 /** Sets the minute hand of the clock to the specified Icon. */ 377 @RemotableViewMethod setMinuteHand(@onNull Icon icon)378 public void setMinuteHand(@NonNull Icon icon) { 379 mMinuteHand = icon.loadDrawable(getContext()); 380 if (mMinuteHandTintInfo.mHasTintList || mMinuteHandTintInfo.mHasTintBlendMode) { 381 mMinuteHand = mMinuteHandTintInfo.apply(mMinuteHand); 382 } 383 384 mChanged = true; 385 invalidate(); 386 } 387 388 /** 389 * Applies a tint to the minute hand drawable. 390 * <p> 391 * Subsequent calls to {@link #setMinuteHand(Icon)} will 392 * automatically mutate the drawable and apply the specified tint and tint 393 * mode using {@link Drawable#setTintList(ColorStateList)}. 394 * 395 * @param tint the tint to apply, may be {@code null} to clear tint 396 * 397 * @attr ref android.R.styleable#AnalogClock_hand_minuteTint 398 * @see #getMinuteHandTintList() 399 * @see Drawable#setTintList(ColorStateList) 400 */ 401 @RemotableViewMethod setMinuteHandTintList(@ullable ColorStateList tint)402 public void setMinuteHandTintList(@Nullable ColorStateList tint) { 403 mMinuteHandTintInfo.mTintList = tint; 404 mMinuteHandTintInfo.mHasTintList = true; 405 406 mMinuteHand = mMinuteHandTintInfo.apply(mMinuteHand); 407 } 408 409 /** 410 * @return the tint applied to the minute hand drawable 411 * @attr ref android.R.styleable#AnalogClock_hand_minuteTint 412 * @see #setMinuteHandTintList(ColorStateList) 413 */ 414 @InspectableProperty( 415 attributeId = com.android.internal.R.styleable.AnalogClock_hand_minuteTint 416 ) 417 @Nullable getMinuteHandTintList()418 public ColorStateList getMinuteHandTintList() { 419 return mMinuteHandTintInfo.mTintList; 420 } 421 422 /** 423 * Specifies the blending mode used to apply the tint specified by 424 * {@link #setMinuteHandTintList(ColorStateList)}} to the minute hand drawable. 425 * The default mode is {@link BlendMode#SRC_IN}. 426 * 427 * @param blendMode the blending mode used to apply the tint, may be 428 * {@code null} to clear tint 429 * @attr ref android.R.styleable#AnalogClock_hand_minuteTintMode 430 * @see #getMinuteHandTintBlendMode() 431 * @see Drawable#setTintBlendMode(BlendMode) 432 */ 433 @RemotableViewMethod setMinuteHandTintBlendMode(@ullable BlendMode blendMode)434 public void setMinuteHandTintBlendMode(@Nullable BlendMode blendMode) { 435 mMinuteHandTintInfo.mTintBlendMode = blendMode; 436 mMinuteHandTintInfo.mHasTintBlendMode = true; 437 438 mMinuteHand = mMinuteHandTintInfo.apply(mMinuteHand); 439 } 440 441 /** 442 * @return the blending mode used to apply the tint to the minute hand drawable 443 * @attr ref android.R.styleable#AnalogClock_hand_minuteTintMode 444 * @see #setMinuteHandTintBlendMode(BlendMode) 445 */ 446 @InspectableProperty( 447 attributeId = com.android.internal.R.styleable.AnalogClock_hand_minuteTintMode) 448 @Nullable getMinuteHandTintBlendMode()449 public BlendMode getMinuteHandTintBlendMode() { 450 return mMinuteHandTintInfo.mTintBlendMode; 451 } 452 453 /** 454 * Sets the second hand of the clock to the specified Icon, or hides the second hand if it is 455 * null. 456 */ 457 @RemotableViewMethod setSecondHand(@ullable Icon icon)458 public void setSecondHand(@Nullable Icon icon) { 459 mSecondHand = icon == null ? null : icon.loadDrawable(getContext()); 460 if (mSecondHandTintInfo.mHasTintList || mSecondHandTintInfo.mHasTintBlendMode) { 461 mSecondHand = mSecondHandTintInfo.apply(mSecondHand); 462 } 463 // Re-run the tick runnable immediately as the presence or absence of a seconds hand affects 464 // the next time we need to tick the clock. 465 mTick.run(); 466 467 mChanged = true; 468 invalidate(); 469 } 470 471 /** 472 * Applies a tint to the second hand drawable. 473 * <p> 474 * Subsequent calls to {@link #setSecondHand(Icon)} will 475 * automatically mutate the drawable and apply the specified tint and tint 476 * mode using {@link Drawable#setTintList(ColorStateList)}. 477 * 478 * @param tint the tint to apply, may be {@code null} to clear tint 479 * 480 * @attr ref android.R.styleable#AnalogClock_hand_secondTint 481 * @see #getSecondHandTintList() 482 * @see Drawable#setTintList(ColorStateList) 483 */ 484 @RemotableViewMethod setSecondHandTintList(@ullable ColorStateList tint)485 public void setSecondHandTintList(@Nullable ColorStateList tint) { 486 mSecondHandTintInfo.mTintList = tint; 487 mSecondHandTintInfo.mHasTintList = true; 488 489 mSecondHand = mSecondHandTintInfo.apply(mSecondHand); 490 } 491 492 /** 493 * @return the tint applied to the second hand drawable 494 * @attr ref android.R.styleable#AnalogClock_hand_secondTint 495 * @see #setSecondHandTintList(ColorStateList) 496 */ 497 @InspectableProperty( 498 attributeId = com.android.internal.R.styleable.AnalogClock_hand_secondTint 499 ) 500 @Nullable getSecondHandTintList()501 public ColorStateList getSecondHandTintList() { 502 return mSecondHandTintInfo.mTintList; 503 } 504 505 /** 506 * Specifies the blending mode used to apply the tint specified by 507 * {@link #setSecondHandTintList(ColorStateList)}} to the second hand drawable. 508 * The default mode is {@link BlendMode#SRC_IN}. 509 * 510 * @param blendMode the blending mode used to apply the tint, may be 511 * {@code null} to clear tint 512 * @attr ref android.R.styleable#AnalogClock_hand_secondTintMode 513 * @see #getSecondHandTintBlendMode() 514 * @see Drawable#setTintBlendMode(BlendMode) 515 */ 516 @RemotableViewMethod setSecondHandTintBlendMode(@ullable BlendMode blendMode)517 public void setSecondHandTintBlendMode(@Nullable BlendMode blendMode) { 518 mSecondHandTintInfo.mTintBlendMode = blendMode; 519 mSecondHandTintInfo.mHasTintBlendMode = true; 520 521 mSecondHand = mSecondHandTintInfo.apply(mSecondHand); 522 } 523 524 /** 525 * @return the blending mode used to apply the tint to the second hand drawable 526 * @attr ref android.R.styleable#AnalogClock_hand_secondTintMode 527 * @see #setSecondHandTintBlendMode(BlendMode) 528 */ 529 @InspectableProperty( 530 attributeId = com.android.internal.R.styleable.AnalogClock_hand_secondTintMode) 531 @Nullable getSecondHandTintBlendMode()532 public BlendMode getSecondHandTintBlendMode() { 533 return mSecondHandTintInfo.mTintBlendMode; 534 } 535 536 /** 537 * Indicates which time zone is currently used by this view. 538 * 539 * @return The ID of the current time zone or null if the default time zone, 540 * as set by the user, must be used 541 * 542 * @see java.util.TimeZone 543 * @see java.util.TimeZone#getAvailableIDs() 544 * @see #setTimeZone(String) 545 */ 546 @InspectableProperty 547 @Nullable getTimeZone()548 public String getTimeZone() { 549 ZoneId zoneId = mTimeZone; 550 return zoneId == null ? null : zoneId.getId(); 551 } 552 553 /** 554 * Sets the specified time zone to use in this clock. When the time zone 555 * is set through this method, system time zone changes (when the user 556 * sets the time zone in settings for instance) will be ignored. 557 * 558 * @param timeZone The desired time zone's ID as specified in {@link java.util.TimeZone} 559 * or null to user the time zone specified by the user 560 * (system time zone) 561 * 562 * @see #getTimeZone() 563 * @see java.util.TimeZone#getAvailableIDs() 564 * @see java.util.TimeZone#getTimeZone(String) 565 * 566 * @attr ref android.R.styleable#AnalogClock_timeZone 567 */ 568 @RemotableViewMethod setTimeZone(@ullable String timeZone)569 public void setTimeZone(@Nullable String timeZone) { 570 mTimeZone = toZoneId(timeZone); 571 572 createClock(); 573 onTimeChanged(); 574 } 575 576 @Override onVisibilityAggregated(boolean isVisible)577 public void onVisibilityAggregated(boolean isVisible) { 578 super.onVisibilityAggregated(isVisible); 579 580 if (isVisible) { 581 onVisible(); 582 } else { 583 onInvisible(); 584 } 585 } 586 587 @Override onAttachedToWindow()588 protected void onAttachedToWindow() { 589 super.onAttachedToWindow(); 590 591 if (!mReceiverAttached) { 592 mClockEventDelegate.registerTimeChangeReceiver(mIntentReceiver, getHandler()); 593 mReceiverAttached = true; 594 } 595 596 // NOTE: It's safe to do these after registering the receiver since the receiver always runs 597 // in the main thread, therefore the receiver can't run before this method returns. 598 599 // The time zone may have changed while the receiver wasn't registered, so update the clock. 600 createClock(); 601 602 // Make sure we update to the current time 603 onTimeChanged(); 604 } 605 606 @Override onDetachedFromWindow()607 protected void onDetachedFromWindow() { 608 if (mReceiverAttached) { 609 mClockEventDelegate.unregisterTimeChangeReceiver(mIntentReceiver); 610 mReceiverAttached = false; 611 } 612 super.onDetachedFromWindow(); 613 } 614 615 /** 616 * Sets a delegate to handle clock event registration. This must be called before the view is 617 * attached to the window 618 * 619 * @hide 620 */ setClockEventDelegate(ClockEventDelegate delegate)621 public void setClockEventDelegate(ClockEventDelegate delegate) { 622 Preconditions.checkState(!mReceiverAttached, "Clock events already registered"); 623 mClockEventDelegate = delegate; 624 } 625 onVisible()626 private void onVisible() { 627 if (!mVisible) { 628 mVisible = true; 629 mTick.run(); 630 } 631 632 } 633 onInvisible()634 private void onInvisible() { 635 if (mVisible) { 636 removeCallbacks(mTick); 637 mVisible = false; 638 } 639 } 640 641 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)642 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 643 644 int widthMode = MeasureSpec.getMode(widthMeasureSpec); 645 int widthSize = MeasureSpec.getSize(widthMeasureSpec); 646 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 647 int heightSize = MeasureSpec.getSize(heightMeasureSpec); 648 649 float hScale = 1.0f; 650 float vScale = 1.0f; 651 652 if (widthMode != MeasureSpec.UNSPECIFIED && widthSize < mDialWidth) { 653 hScale = (float) widthSize / (float) mDialWidth; 654 } 655 656 if (heightMode != MeasureSpec.UNSPECIFIED && heightSize < mDialHeight) { 657 vScale = (float )heightSize / (float) mDialHeight; 658 } 659 660 float scale = Math.min(hScale, vScale); 661 662 setMeasuredDimension(resolveSizeAndState((int) (mDialWidth * scale), widthMeasureSpec, 0), 663 resolveSizeAndState((int) (mDialHeight * scale), heightMeasureSpec, 0)); 664 } 665 666 @Override onSizeChanged(int w, int h, int oldw, int oldh)667 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 668 super.onSizeChanged(w, h, oldw, oldh); 669 mChanged = true; 670 } 671 672 @Override onDraw(Canvas canvas)673 protected void onDraw(Canvas canvas) { 674 super.onDraw(canvas); 675 676 boolean changed = mChanged; 677 if (changed) { 678 mChanged = false; 679 } 680 681 int availableWidth = mRight - mLeft; 682 int availableHeight = mBottom - mTop; 683 684 int x = availableWidth / 2; 685 int y = availableHeight / 2; 686 687 final Drawable dial = mDial; 688 int w = dial.getIntrinsicWidth(); 689 int h = dial.getIntrinsicHeight(); 690 691 boolean scaled = false; 692 693 if (availableWidth < w || availableHeight < h) { 694 scaled = true; 695 float scale = Math.min((float) availableWidth / (float) w, 696 (float) availableHeight / (float) h); 697 canvas.save(); 698 canvas.scale(scale, scale, x, y); 699 } 700 701 if (changed) { 702 dial.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2)); 703 } 704 dial.draw(canvas); 705 706 canvas.save(); 707 canvas.rotate(mHour / 12.0f * 360.0f, x, y); 708 final Drawable hourHand = mHourHand; 709 if (changed) { 710 w = hourHand.getIntrinsicWidth(); 711 h = hourHand.getIntrinsicHeight(); 712 hourHand.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2)); 713 } 714 hourHand.draw(canvas); 715 canvas.restore(); 716 717 canvas.save(); 718 canvas.rotate(mMinutes / 60.0f * 360.0f, x, y); 719 720 final Drawable minuteHand = mMinuteHand; 721 if (changed) { 722 w = minuteHand.getIntrinsicWidth(); 723 h = minuteHand.getIntrinsicHeight(); 724 minuteHand.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2)); 725 } 726 minuteHand.draw(canvas); 727 canvas.restore(); 728 729 final Drawable secondHand = mSecondHand; 730 if (secondHand != null && mSecondsHandFps > 0) { 731 canvas.save(); 732 canvas.rotate(mSeconds / 60.0f * 360.0f, x, y); 733 734 if (changed) { 735 w = secondHand.getIntrinsicWidth(); 736 h = secondHand.getIntrinsicHeight(); 737 secondHand.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2)); 738 } 739 secondHand.draw(canvas); 740 canvas.restore(); 741 } 742 743 if (scaled) { 744 canvas.restore(); 745 } 746 } 747 748 /** 749 * Return the current Instant to be used for drawing the clockface. Protected to allow 750 * subclasses to override this to show a different time from the system clock. 751 * 752 * @return the Instant to be shown on the clockface 753 * @hide 754 */ now()755 protected Instant now() { 756 return mClock.instant(); 757 } 758 759 /** 760 * @hide 761 */ onTimeChanged()762 protected void onTimeChanged() { 763 Instant now = now(); 764 onTimeChanged(now.atZone(mClock.getZone()).toLocalTime(), now.toEpochMilli()); 765 } 766 onTimeChanged(LocalTime localTime, long nowMillis)767 private void onTimeChanged(LocalTime localTime, long nowMillis) { 768 float previousHour = mHour; 769 float previousMinutes = mMinutes; 770 771 float rawSeconds = localTime.getSecond() + localTime.getNano() / 1_000_000_000f; 772 // We round the fraction of the second so that the seconds hand always occupies the same 773 // n positions between two given numbers, where n is the number of ticks per second. This 774 // ensures the second hand advances by a consistent distance despite our handler callbacks 775 // occurring at inconsistent frequencies. 776 mSeconds = 777 mSecondsHandFps <= 0 778 ? rawSeconds 779 : Math.round(rawSeconds * mSecondsHandFps) / (float) mSecondsHandFps; 780 mMinutes = localTime.getMinute() + mSeconds / 60.0f; 781 mHour = localTime.getHour() + mMinutes / 60.0f; 782 mChanged = true; 783 784 // Update the content description only if the announced hours and minutes have changed. 785 if ((int) previousHour != (int) mHour || (int) previousMinutes != (int) mMinutes) { 786 updateContentDescription(nowMillis); 787 } 788 } 789 790 /** Intent receiver for the time or time zone changing. */ 791 private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { 792 @Override 793 public void onReceive(Context context, Intent intent) { 794 if (Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) { 795 createClock(); 796 } 797 798 mTick.run(); 799 } 800 }; 801 private boolean mReceiverAttached; 802 private ClockEventDelegate mClockEventDelegate; 803 804 private final Runnable mTick = new Runnable() { 805 @Override 806 public void run() { 807 removeCallbacks(this); 808 if (!mVisible) { 809 return; 810 } 811 812 Instant now = now(); 813 ZonedDateTime zonedDateTime = now.atZone(mClock.getZone()); 814 LocalTime localTime = zonedDateTime.toLocalTime(); 815 816 long millisUntilNextTick; 817 if (mSecondHand == null || mSecondsHandFps <= 0) { 818 // If there's no second hand, then tick at the start of the next minute. 819 // 820 // This must be done with ZonedDateTime as opposed to LocalDateTime to ensure proper 821 // handling of DST. Also note that because of leap seconds, it should not be assumed 822 // that one minute == 60 seconds. 823 Instant startOfNextMinute = zonedDateTime.plusMinutes(1).withSecond(0).toInstant(); 824 millisUntilNextTick = Duration.between(now, startOfNextMinute).toMillis(); 825 if (millisUntilNextTick <= 0) { 826 // This should never occur, but if it does, then just check the tick again in 827 // one minute to ensure we're always moving forward. 828 millisUntilNextTick = Duration.ofMinutes(1).toMillis(); 829 } 830 } else { 831 // If there is a seconds hand, then determine the next tick point based on the fps. 832 // 833 // How many milliseconds through the second we currently are. 834 long millisOfSecond = Duration.ofNanos(localTime.getNano()).toMillis(); 835 // How many milliseconds there are between tick positions for the seconds hand. 836 double millisPerTick = 1000 / (double) mSecondsHandFps; 837 // How many milliseconds we are past the last tick position. 838 long millisPastLastTick = Math.round(millisOfSecond % millisPerTick); 839 // How many milliseconds there are until the next tick position. 840 millisUntilNextTick = Math.round(millisPerTick - millisPastLastTick); 841 // If we are exactly at the tick position, this could be 0 milliseconds due to 842 // rounding. In this case, advance by the full amount of millis to the next 843 // position. 844 if (millisUntilNextTick <= 0) { 845 millisUntilNextTick = Math.round(millisPerTick); 846 } 847 } 848 849 // Schedule a callback for when the next tick should occur. 850 postDelayed(this, millisUntilNextTick); 851 852 onTimeChanged(localTime, now.toEpochMilli()); 853 854 invalidate(); 855 } 856 }; 857 createClock()858 private void createClock() { 859 ZoneId zoneId = mTimeZone; 860 if (zoneId == null) { 861 mClock = Clock.systemDefaultZone(); 862 } else { 863 mClock = Clock.system(zoneId); 864 } 865 } 866 updateContentDescription(long timeMillis)867 private void updateContentDescription(long timeMillis) { 868 final int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_24HOUR; 869 String contentDescription = 870 DateUtils.formatDateRange( 871 mContext, 872 new Formatter(new StringBuilder(50), Locale.getDefault()), 873 timeMillis /* startMillis */, 874 timeMillis /* endMillis */, 875 flags, 876 getTimeZone()) 877 .toString(); 878 setContentDescription(contentDescription); 879 } 880 881 /** 882 * Tries to parse a {@link ZoneId} from {@code timeZone}, returning null if it is null or there 883 * is an error parsing. 884 */ 885 @Nullable toZoneId(@ullable String timeZone)886 private static ZoneId toZoneId(@Nullable String timeZone) { 887 if (timeZone == null) { 888 return null; 889 } 890 891 try { 892 return ZoneId.of(timeZone); 893 } catch (DateTimeException e) { 894 Log.w(LOG_TAG, "Failed to parse time zone from " + timeZone, e); 895 return null; 896 } 897 } 898 899 private final class TintInfo { 900 boolean mHasTintList; 901 @Nullable ColorStateList mTintList; 902 boolean mHasTintBlendMode; 903 @Nullable BlendMode mTintBlendMode; 904 905 /** 906 * Returns a mutated copy of {@code drawable} with tinting applied, or null if it's null. 907 */ 908 @Nullable apply(@ullable Drawable drawable)909 Drawable apply(@Nullable Drawable drawable) { 910 if (drawable == null) return null; 911 912 Drawable newDrawable = drawable.mutate(); 913 914 if (mHasTintList) { 915 newDrawable.setTintList(mTintList); 916 } 917 918 if (mHasTintBlendMode) { 919 newDrawable.setTintBlendMode(mTintBlendMode); 920 } 921 922 // All drawables should have the same state as the View itself. 923 if (drawable.isStateful()) { 924 newDrawable.setState(getDrawableState()); 925 } 926 927 return newDrawable; 928 } 929 } 930 } 931