| /* |
| * Copyright (C) 2006 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.widget; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.app.AppGlobals; |
| import android.compat.annotation.UnsupportedAppUsage; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.res.ColorStateList; |
| import android.content.res.TypedArray; |
| import android.graphics.BlendMode; |
| import android.graphics.Canvas; |
| import android.graphics.drawable.Drawable; |
| import android.graphics.drawable.Icon; |
| import android.text.format.DateUtils; |
| import android.util.AttributeSet; |
| import android.util.Log; |
| import android.view.RemotableViewMethod; |
| import android.view.View; |
| import android.view.inspector.InspectableProperty; |
| import android.widget.RemoteViews.RemoteView; |
| import android.widget.TextClock.ClockEventDelegate; |
| |
| import com.android.internal.util.Preconditions; |
| |
| import java.time.Clock; |
| import java.time.DateTimeException; |
| import java.time.Duration; |
| import java.time.Instant; |
| import java.time.LocalTime; |
| import java.time.ZoneId; |
| import java.time.ZonedDateTime; |
| import java.util.Formatter; |
| import java.util.Locale; |
| |
| /** |
| * This widget displays an analogic clock with two hands for hours and minutes. |
| * |
| * @attr ref android.R.styleable#AnalogClock_dial |
| * @attr ref android.R.styleable#AnalogClock_hand_hour |
| * @attr ref android.R.styleable#AnalogClock_hand_minute |
| * @attr ref android.R.styleable#AnalogClock_hand_second |
| * @attr ref android.R.styleable#AnalogClock_timeZone |
| * @deprecated This widget is no longer supported; except for |
| * {@link android.widget.RemoteViews} use cases like |
| * <a href="https://developer.android.com/develop/ui/views/appwidgets/overview"> |
| * app widgets</a>. |
| * |
| */ |
| @RemoteView |
| @Deprecated |
| public class AnalogClock extends View { |
| private static final String LOG_TAG = "AnalogClock"; |
| |
| /** How many times per second that the seconds hand advances. */ |
| private final int mSecondsHandFps; |
| |
| private Clock mClock; |
| @Nullable |
| private ZoneId mTimeZone; |
| |
| @UnsupportedAppUsage |
| private Drawable mHourHand; |
| private final TintInfo mHourHandTintInfo = new TintInfo(); |
| @UnsupportedAppUsage |
| private Drawable mMinuteHand; |
| private final TintInfo mMinuteHandTintInfo = new TintInfo(); |
| @Nullable |
| private Drawable mSecondHand; |
| private final TintInfo mSecondHandTintInfo = new TintInfo(); |
| @UnsupportedAppUsage |
| private Drawable mDial; |
| private final TintInfo mDialTintInfo = new TintInfo(); |
| |
| private int mDialWidth; |
| private int mDialHeight; |
| |
| private boolean mVisible; |
| |
| private float mSeconds; |
| private float mMinutes; |
| private float mHour; |
| private boolean mChanged; |
| |
| public AnalogClock(Context context) { |
| this(context, null); |
| } |
| |
| public AnalogClock(Context context, AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| public AnalogClock(Context context, AttributeSet attrs, int defStyleAttr) { |
| this(context, attrs, defStyleAttr, 0); |
| } |
| |
| public AnalogClock(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { |
| super(context, attrs, defStyleAttr, defStyleRes); |
| |
| mClockEventDelegate = new ClockEventDelegate(context); |
| mSecondsHandFps = AppGlobals.getIntCoreSetting( |
| WidgetFlags.KEY_ANALOG_CLOCK_SECONDS_HAND_FPS, |
| context.getResources() |
| .getInteger(com.android.internal.R.integer |
| .config_defaultAnalogClockSecondsHandFps)); |
| |
| final TypedArray a = context.obtainStyledAttributes( |
| attrs, com.android.internal.R.styleable.AnalogClock, defStyleAttr, defStyleRes); |
| saveAttributeDataForStyleable(context, com.android.internal.R.styleable.AnalogClock, |
| attrs, a, defStyleAttr, defStyleRes); |
| |
| mDial = a.getDrawable(com.android.internal.R.styleable.AnalogClock_dial); |
| if (mDial == null) { |
| mDial = context.getDrawable(com.android.internal.R.drawable.clock_dial); |
| } |
| |
| ColorStateList dialTintList = a.getColorStateList( |
| com.android.internal.R.styleable.AnalogClock_dialTint); |
| if (dialTintList != null) { |
| mDialTintInfo.mTintList = dialTintList; |
| mDialTintInfo.mHasTintList = true; |
| } |
| BlendMode dialTintMode = Drawable.parseBlendMode( |
| a.getInt(com.android.internal.R.styleable.AnalogClock_dialTintMode, -1), |
| null); |
| if (dialTintMode != null) { |
| mDialTintInfo.mTintBlendMode = dialTintMode; |
| mDialTintInfo.mHasTintBlendMode = true; |
| } |
| if (mDialTintInfo.mHasTintList || mDialTintInfo.mHasTintBlendMode) { |
| mDial = mDialTintInfo.apply(mDial); |
| } |
| |
| mHourHand = a.getDrawable(com.android.internal.R.styleable.AnalogClock_hand_hour); |
| if (mHourHand == null) { |
| mHourHand = context.getDrawable(com.android.internal.R.drawable.clock_hand_hour); |
| } |
| |
| ColorStateList hourHandTintList = a.getColorStateList( |
| com.android.internal.R.styleable.AnalogClock_hand_hourTint); |
| if (hourHandTintList != null) { |
| mHourHandTintInfo.mTintList = hourHandTintList; |
| mHourHandTintInfo.mHasTintList = true; |
| } |
| BlendMode hourHandTintMode = Drawable.parseBlendMode( |
| a.getInt(com.android.internal.R.styleable.AnalogClock_hand_hourTintMode, -1), |
| null); |
| if (hourHandTintMode != null) { |
| mHourHandTintInfo.mTintBlendMode = hourHandTintMode; |
| mHourHandTintInfo.mHasTintBlendMode = true; |
| } |
| if (mHourHandTintInfo.mHasTintList || mHourHandTintInfo.mHasTintBlendMode) { |
| mHourHand = mHourHandTintInfo.apply(mHourHand); |
| } |
| |
| mMinuteHand = a.getDrawable(com.android.internal.R.styleable.AnalogClock_hand_minute); |
| if (mMinuteHand == null) { |
| mMinuteHand = context.getDrawable(com.android.internal.R.drawable.clock_hand_minute); |
| } |
| |
| ColorStateList minuteHandTintList = a.getColorStateList( |
| com.android.internal.R.styleable.AnalogClock_hand_minuteTint); |
| if (minuteHandTintList != null) { |
| mMinuteHandTintInfo.mTintList = minuteHandTintList; |
| mMinuteHandTintInfo.mHasTintList = true; |
| } |
| BlendMode minuteHandTintMode = Drawable.parseBlendMode( |
| a.getInt(com.android.internal.R.styleable.AnalogClock_hand_minuteTintMode, -1), |
| null); |
| if (minuteHandTintMode != null) { |
| mMinuteHandTintInfo.mTintBlendMode = minuteHandTintMode; |
| mMinuteHandTintInfo.mHasTintBlendMode = true; |
| } |
| if (mMinuteHandTintInfo.mHasTintList || mMinuteHandTintInfo.mHasTintBlendMode) { |
| mMinuteHand = mMinuteHandTintInfo.apply(mMinuteHand); |
| } |
| |
| mSecondHand = a.getDrawable(com.android.internal.R.styleable.AnalogClock_hand_second); |
| |
| ColorStateList secondHandTintList = a.getColorStateList( |
| com.android.internal.R.styleable.AnalogClock_hand_secondTint); |
| if (secondHandTintList != null) { |
| mSecondHandTintInfo.mTintList = secondHandTintList; |
| mSecondHandTintInfo.mHasTintList = true; |
| } |
| BlendMode secondHandTintMode = Drawable.parseBlendMode( |
| a.getInt(com.android.internal.R.styleable.AnalogClock_hand_secondTintMode, -1), |
| null); |
| if (secondHandTintMode != null) { |
| mSecondHandTintInfo.mTintBlendMode = secondHandTintMode; |
| mSecondHandTintInfo.mHasTintBlendMode = true; |
| } |
| if (mSecondHandTintInfo.mHasTintList || mSecondHandTintInfo.mHasTintBlendMode) { |
| mSecondHand = mSecondHandTintInfo.apply(mSecondHand); |
| } |
| |
| mTimeZone = toZoneId(a.getString(com.android.internal.R.styleable.AnalogClock_timeZone)); |
| createClock(); |
| |
| a.recycle(); |
| |
| mDialWidth = mDial.getIntrinsicWidth(); |
| mDialHeight = mDial.getIntrinsicHeight(); |
| } |
| |
| /** Sets the dial of the clock to the specified Icon. */ |
| @RemotableViewMethod |
| public void setDial(@NonNull Icon icon) { |
| mDial = icon.loadDrawable(getContext()); |
| mDialWidth = mDial.getIntrinsicWidth(); |
| mDialHeight = mDial.getIntrinsicHeight(); |
| if (mDialTintInfo.mHasTintList || mDialTintInfo.mHasTintBlendMode) { |
| mDial = mDialTintInfo.apply(mDial); |
| } |
| |
| mChanged = true; |
| invalidate(); |
| } |
| |
| /** |
| * Applies a tint to the dial drawable. |
| * <p> |
| * Subsequent calls to {@link #setDial(Icon)} will |
| * automatically mutate the drawable and apply the specified tint and tint |
| * mode using {@link Drawable#setTintList(ColorStateList)}. |
| * |
| * @param tint the tint to apply, may be {@code null} to clear tint |
| * |
| * @attr ref android.R.styleable#AnalogClock_dialTint |
| * @see #getDialTintList() |
| * @see Drawable#setTintList(ColorStateList) |
| */ |
| @RemotableViewMethod |
| public void setDialTintList(@Nullable ColorStateList tint) { |
| mDialTintInfo.mTintList = tint; |
| mDialTintInfo.mHasTintList = true; |
| |
| mDial = mDialTintInfo.apply(mDial); |
| } |
| |
| /** |
| * @return the tint applied to the dial drawable |
| * @attr ref android.R.styleable#AnalogClock_dialTint |
| * @see #setDialTintList(ColorStateList) |
| */ |
| @InspectableProperty(attributeId = com.android.internal.R.styleable.AnalogClock_dialTint) |
| @Nullable |
| public ColorStateList getDialTintList() { |
| return mDialTintInfo.mTintList; |
| } |
| |
| /** |
| * Specifies the blending mode used to apply the tint specified by |
| * {@link #setDialTintList(ColorStateList)}} to the dial drawable. |
| * The default mode is {@link BlendMode#SRC_IN}. |
| * |
| * @param blendMode the blending mode used to apply the tint, may be |
| * {@code null} to clear tint |
| * @attr ref android.R.styleable#AnalogClock_dialTintMode |
| * @see #getDialTintBlendMode() |
| * @see Drawable#setTintBlendMode(BlendMode) |
| */ |
| @RemotableViewMethod |
| public void setDialTintBlendMode(@Nullable BlendMode blendMode) { |
| mDialTintInfo.mTintBlendMode = blendMode; |
| mDialTintInfo.mHasTintBlendMode = true; |
| |
| mDial = mDialTintInfo.apply(mDial); |
| } |
| |
| /** |
| * @return the blending mode used to apply the tint to the dial drawable |
| * @attr ref android.R.styleable#AnalogClock_dialTintMode |
| * @see #setDialTintBlendMode(BlendMode) |
| */ |
| @InspectableProperty(attributeId = com.android.internal.R.styleable.AnalogClock_dialTintMode) |
| @Nullable |
| public BlendMode getDialTintBlendMode() { |
| return mDialTintInfo.mTintBlendMode; |
| } |
| |
| /** Sets the hour hand of the clock to the specified Icon. */ |
| @RemotableViewMethod |
| public void setHourHand(@NonNull Icon icon) { |
| mHourHand = icon.loadDrawable(getContext()); |
| if (mHourHandTintInfo.mHasTintList || mHourHandTintInfo.mHasTintBlendMode) { |
| mHourHand = mHourHandTintInfo.apply(mHourHand); |
| } |
| |
| mChanged = true; |
| invalidate(); |
| } |
| |
| /** |
| * Applies a tint to the hour hand drawable. |
| * <p> |
| * Subsequent calls to {@link #setHourHand(Icon)} will |
| * automatically mutate the drawable and apply the specified tint and tint |
| * mode using {@link Drawable#setTintList(ColorStateList)}. |
| * |
| * @param tint the tint to apply, may be {@code null} to clear tint |
| * |
| * @attr ref android.R.styleable#AnalogClock_hand_hourTint |
| * @see #getHourHandTintList() |
| * @see Drawable#setTintList(ColorStateList) |
| */ |
| @RemotableViewMethod |
| public void setHourHandTintList(@Nullable ColorStateList tint) { |
| mHourHandTintInfo.mTintList = tint; |
| mHourHandTintInfo.mHasTintList = true; |
| |
| mHourHand = mHourHandTintInfo.apply(mHourHand); |
| } |
| |
| /** |
| * @return the tint applied to the hour hand drawable |
| * @attr ref android.R.styleable#AnalogClock_hand_hourTint |
| * @see #setHourHandTintList(ColorStateList) |
| */ |
| @InspectableProperty( |
| attributeId = com.android.internal.R.styleable.AnalogClock_hand_hourTint |
| ) |
| @Nullable |
| public ColorStateList getHourHandTintList() { |
| return mHourHandTintInfo.mTintList; |
| } |
| |
| /** |
| * Specifies the blending mode used to apply the tint specified by |
| * {@link #setHourHandTintList(ColorStateList)}} to the hour hand drawable. |
| * The default mode is {@link BlendMode#SRC_IN}. |
| * |
| * @param blendMode the blending mode used to apply the tint, may be |
| * {@code null} to clear tint |
| * @attr ref android.R.styleable#AnalogClock_hand_hourTintMode |
| * @see #getHourHandTintBlendMode() |
| * @see Drawable#setTintBlendMode(BlendMode) |
| */ |
| @RemotableViewMethod |
| public void setHourHandTintBlendMode(@Nullable BlendMode blendMode) { |
| mHourHandTintInfo.mTintBlendMode = blendMode; |
| mHourHandTintInfo.mHasTintBlendMode = true; |
| |
| mHourHand = mHourHandTintInfo.apply(mHourHand); |
| } |
| |
| /** |
| * @return the blending mode used to apply the tint to the hour hand drawable |
| * @attr ref android.R.styleable#AnalogClock_hand_hourTintMode |
| * @see #setHourHandTintBlendMode(BlendMode) |
| */ |
| @InspectableProperty( |
| attributeId = com.android.internal.R.styleable.AnalogClock_hand_hourTintMode) |
| @Nullable |
| public BlendMode getHourHandTintBlendMode() { |
| return mHourHandTintInfo.mTintBlendMode; |
| } |
| |
| /** Sets the minute hand of the clock to the specified Icon. */ |
| @RemotableViewMethod |
| public void setMinuteHand(@NonNull Icon icon) { |
| mMinuteHand = icon.loadDrawable(getContext()); |
| if (mMinuteHandTintInfo.mHasTintList || mMinuteHandTintInfo.mHasTintBlendMode) { |
| mMinuteHand = mMinuteHandTintInfo.apply(mMinuteHand); |
| } |
| |
| mChanged = true; |
| invalidate(); |
| } |
| |
| /** |
| * Applies a tint to the minute hand drawable. |
| * <p> |
| * Subsequent calls to {@link #setMinuteHand(Icon)} will |
| * automatically mutate the drawable and apply the specified tint and tint |
| * mode using {@link Drawable#setTintList(ColorStateList)}. |
| * |
| * @param tint the tint to apply, may be {@code null} to clear tint |
| * |
| * @attr ref android.R.styleable#AnalogClock_hand_minuteTint |
| * @see #getMinuteHandTintList() |
| * @see Drawable#setTintList(ColorStateList) |
| */ |
| @RemotableViewMethod |
| public void setMinuteHandTintList(@Nullable ColorStateList tint) { |
| mMinuteHandTintInfo.mTintList = tint; |
| mMinuteHandTintInfo.mHasTintList = true; |
| |
| mMinuteHand = mMinuteHandTintInfo.apply(mMinuteHand); |
| } |
| |
| /** |
| * @return the tint applied to the minute hand drawable |
| * @attr ref android.R.styleable#AnalogClock_hand_minuteTint |
| * @see #setMinuteHandTintList(ColorStateList) |
| */ |
| @InspectableProperty( |
| attributeId = com.android.internal.R.styleable.AnalogClock_hand_minuteTint |
| ) |
| @Nullable |
| public ColorStateList getMinuteHandTintList() { |
| return mMinuteHandTintInfo.mTintList; |
| } |
| |
| /** |
| * Specifies the blending mode used to apply the tint specified by |
| * {@link #setMinuteHandTintList(ColorStateList)}} to the minute hand drawable. |
| * The default mode is {@link BlendMode#SRC_IN}. |
| * |
| * @param blendMode the blending mode used to apply the tint, may be |
| * {@code null} to clear tint |
| * @attr ref android.R.styleable#AnalogClock_hand_minuteTintMode |
| * @see #getMinuteHandTintBlendMode() |
| * @see Drawable#setTintBlendMode(BlendMode) |
| */ |
| @RemotableViewMethod |
| public void setMinuteHandTintBlendMode(@Nullable BlendMode blendMode) { |
| mMinuteHandTintInfo.mTintBlendMode = blendMode; |
| mMinuteHandTintInfo.mHasTintBlendMode = true; |
| |
| mMinuteHand = mMinuteHandTintInfo.apply(mMinuteHand); |
| } |
| |
| /** |
| * @return the blending mode used to apply the tint to the minute hand drawable |
| * @attr ref android.R.styleable#AnalogClock_hand_minuteTintMode |
| * @see #setMinuteHandTintBlendMode(BlendMode) |
| */ |
| @InspectableProperty( |
| attributeId = com.android.internal.R.styleable.AnalogClock_hand_minuteTintMode) |
| @Nullable |
| public BlendMode getMinuteHandTintBlendMode() { |
| return mMinuteHandTintInfo.mTintBlendMode; |
| } |
| |
| /** |
| * Sets the second hand of the clock to the specified Icon, or hides the second hand if it is |
| * null. |
| */ |
| @RemotableViewMethod |
| public void setSecondHand(@Nullable Icon icon) { |
| mSecondHand = icon == null ? null : icon.loadDrawable(getContext()); |
| if (mSecondHandTintInfo.mHasTintList || mSecondHandTintInfo.mHasTintBlendMode) { |
| mSecondHand = mSecondHandTintInfo.apply(mSecondHand); |
| } |
| // Re-run the tick runnable immediately as the presence or absence of a seconds hand affects |
| // the next time we need to tick the clock. |
| mTick.run(); |
| |
| mChanged = true; |
| invalidate(); |
| } |
| |
| /** |
| * Applies a tint to the second hand drawable. |
| * <p> |
| * Subsequent calls to {@link #setSecondHand(Icon)} will |
| * automatically mutate the drawable and apply the specified tint and tint |
| * mode using {@link Drawable#setTintList(ColorStateList)}. |
| * |
| * @param tint the tint to apply, may be {@code null} to clear tint |
| * |
| * @attr ref android.R.styleable#AnalogClock_hand_secondTint |
| * @see #getSecondHandTintList() |
| * @see Drawable#setTintList(ColorStateList) |
| */ |
| @RemotableViewMethod |
| public void setSecondHandTintList(@Nullable ColorStateList tint) { |
| mSecondHandTintInfo.mTintList = tint; |
| mSecondHandTintInfo.mHasTintList = true; |
| |
| mSecondHand = mSecondHandTintInfo.apply(mSecondHand); |
| } |
| |
| /** |
| * @return the tint applied to the second hand drawable |
| * @attr ref android.R.styleable#AnalogClock_hand_secondTint |
| * @see #setSecondHandTintList(ColorStateList) |
| */ |
| @InspectableProperty( |
| attributeId = com.android.internal.R.styleable.AnalogClock_hand_secondTint |
| ) |
| @Nullable |
| public ColorStateList getSecondHandTintList() { |
| return mSecondHandTintInfo.mTintList; |
| } |
| |
| /** |
| * Specifies the blending mode used to apply the tint specified by |
| * {@link #setSecondHandTintList(ColorStateList)}} to the second hand drawable. |
| * The default mode is {@link BlendMode#SRC_IN}. |
| * |
| * @param blendMode the blending mode used to apply the tint, may be |
| * {@code null} to clear tint |
| * @attr ref android.R.styleable#AnalogClock_hand_secondTintMode |
| * @see #getSecondHandTintBlendMode() |
| * @see Drawable#setTintBlendMode(BlendMode) |
| */ |
| @RemotableViewMethod |
| public void setSecondHandTintBlendMode(@Nullable BlendMode blendMode) { |
| mSecondHandTintInfo.mTintBlendMode = blendMode; |
| mSecondHandTintInfo.mHasTintBlendMode = true; |
| |
| mSecondHand = mSecondHandTintInfo.apply(mSecondHand); |
| } |
| |
| /** |
| * @return the blending mode used to apply the tint to the second hand drawable |
| * @attr ref android.R.styleable#AnalogClock_hand_secondTintMode |
| * @see #setSecondHandTintBlendMode(BlendMode) |
| */ |
| @InspectableProperty( |
| attributeId = com.android.internal.R.styleable.AnalogClock_hand_secondTintMode) |
| @Nullable |
| public BlendMode getSecondHandTintBlendMode() { |
| return mSecondHandTintInfo.mTintBlendMode; |
| } |
| |
| /** |
| * Indicates which time zone is currently used by this view. |
| * |
| * @return The ID of the current time zone or null if the default time zone, |
| * as set by the user, must be used |
| * |
| * @see java.util.TimeZone |
| * @see java.util.TimeZone#getAvailableIDs() |
| * @see #setTimeZone(String) |
| */ |
| @InspectableProperty |
| @Nullable |
| public String getTimeZone() { |
| ZoneId zoneId = mTimeZone; |
| return zoneId == null ? null : zoneId.getId(); |
| } |
| |
| /** |
| * Sets the specified time zone to use in this clock. When the time zone |
| * is set through this method, system time zone changes (when the user |
| * sets the time zone in settings for instance) will be ignored. |
| * |
| * @param timeZone The desired time zone's ID as specified in {@link java.util.TimeZone} |
| * or null to user the time zone specified by the user |
| * (system time zone) |
| * |
| * @see #getTimeZone() |
| * @see java.util.TimeZone#getAvailableIDs() |
| * @see java.util.TimeZone#getTimeZone(String) |
| * |
| * @attr ref android.R.styleable#AnalogClock_timeZone |
| */ |
| @RemotableViewMethod |
| public void setTimeZone(@Nullable String timeZone) { |
| mTimeZone = toZoneId(timeZone); |
| |
| createClock(); |
| onTimeChanged(); |
| } |
| |
| @Override |
| public void onVisibilityAggregated(boolean isVisible) { |
| super.onVisibilityAggregated(isVisible); |
| |
| if (isVisible) { |
| onVisible(); |
| } else { |
| onInvisible(); |
| } |
| } |
| |
| @Override |
| protected void onAttachedToWindow() { |
| super.onAttachedToWindow(); |
| |
| if (!mReceiverAttached) { |
| mClockEventDelegate.registerTimeChangeReceiver(mIntentReceiver, getHandler()); |
| mReceiverAttached = true; |
| } |
| |
| // NOTE: It's safe to do these after registering the receiver since the receiver always runs |
| // in the main thread, therefore the receiver can't run before this method returns. |
| |
| // The time zone may have changed while the receiver wasn't registered, so update the clock. |
| createClock(); |
| |
| // Make sure we update to the current time |
| onTimeChanged(); |
| } |
| |
| @Override |
| protected void onDetachedFromWindow() { |
| if (mReceiverAttached) { |
| mClockEventDelegate.unregisterTimeChangeReceiver(mIntentReceiver); |
| mReceiverAttached = false; |
| } |
| super.onDetachedFromWindow(); |
| } |
| |
| /** |
| * Sets a delegate to handle clock event registration. This must be called before the view is |
| * attached to the window |
| * |
| * @hide |
| */ |
| public void setClockEventDelegate(ClockEventDelegate delegate) { |
| Preconditions.checkState(!mReceiverAttached, "Clock events already registered"); |
| mClockEventDelegate = delegate; |
| } |
| |
| private void onVisible() { |
| if (!mVisible) { |
| mVisible = true; |
| mTick.run(); |
| } |
| |
| } |
| |
| private void onInvisible() { |
| if (mVisible) { |
| removeCallbacks(mTick); |
| mVisible = false; |
| } |
| } |
| |
| @Override |
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| |
| int widthMode = MeasureSpec.getMode(widthMeasureSpec); |
| int widthSize = MeasureSpec.getSize(widthMeasureSpec); |
| int heightMode = MeasureSpec.getMode(heightMeasureSpec); |
| int heightSize = MeasureSpec.getSize(heightMeasureSpec); |
| |
| float hScale = 1.0f; |
| float vScale = 1.0f; |
| |
| if (widthMode != MeasureSpec.UNSPECIFIED && widthSize < mDialWidth) { |
| hScale = (float) widthSize / (float) mDialWidth; |
| } |
| |
| if (heightMode != MeasureSpec.UNSPECIFIED && heightSize < mDialHeight) { |
| vScale = (float )heightSize / (float) mDialHeight; |
| } |
| |
| float scale = Math.min(hScale, vScale); |
| |
| setMeasuredDimension(resolveSizeAndState((int) (mDialWidth * scale), widthMeasureSpec, 0), |
| resolveSizeAndState((int) (mDialHeight * scale), heightMeasureSpec, 0)); |
| } |
| |
| @Override |
| protected void onSizeChanged(int w, int h, int oldw, int oldh) { |
| super.onSizeChanged(w, h, oldw, oldh); |
| mChanged = true; |
| } |
| |
| @Override |
| protected void onDraw(Canvas canvas) { |
| super.onDraw(canvas); |
| |
| boolean changed = mChanged; |
| if (changed) { |
| mChanged = false; |
| } |
| |
| int availableWidth = mRight - mLeft; |
| int availableHeight = mBottom - mTop; |
| |
| int x = availableWidth / 2; |
| int y = availableHeight / 2; |
| |
| final Drawable dial = mDial; |
| int w = dial.getIntrinsicWidth(); |
| int h = dial.getIntrinsicHeight(); |
| |
| boolean scaled = false; |
| |
| if (availableWidth < w || availableHeight < h) { |
| scaled = true; |
| float scale = Math.min((float) availableWidth / (float) w, |
| (float) availableHeight / (float) h); |
| canvas.save(); |
| canvas.scale(scale, scale, x, y); |
| } |
| |
| if (changed) { |
| dial.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2)); |
| } |
| dial.draw(canvas); |
| |
| canvas.save(); |
| canvas.rotate(mHour / 12.0f * 360.0f, x, y); |
| final Drawable hourHand = mHourHand; |
| if (changed) { |
| w = hourHand.getIntrinsicWidth(); |
| h = hourHand.getIntrinsicHeight(); |
| hourHand.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2)); |
| } |
| hourHand.draw(canvas); |
| canvas.restore(); |
| |
| canvas.save(); |
| canvas.rotate(mMinutes / 60.0f * 360.0f, x, y); |
| |
| final Drawable minuteHand = mMinuteHand; |
| if (changed) { |
| w = minuteHand.getIntrinsicWidth(); |
| h = minuteHand.getIntrinsicHeight(); |
| minuteHand.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2)); |
| } |
| minuteHand.draw(canvas); |
| canvas.restore(); |
| |
| final Drawable secondHand = mSecondHand; |
| if (secondHand != null && mSecondsHandFps > 0) { |
| canvas.save(); |
| canvas.rotate(mSeconds / 60.0f * 360.0f, x, y); |
| |
| if (changed) { |
| w = secondHand.getIntrinsicWidth(); |
| h = secondHand.getIntrinsicHeight(); |
| secondHand.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2)); |
| } |
| secondHand.draw(canvas); |
| canvas.restore(); |
| } |
| |
| if (scaled) { |
| canvas.restore(); |
| } |
| } |
| |
| /** |
| * Return the current Instant to be used for drawing the clockface. Protected to allow |
| * subclasses to override this to show a different time from the system clock. |
| * |
| * @return the Instant to be shown on the clockface |
| * @hide |
| */ |
| protected Instant now() { |
| return mClock.instant(); |
| } |
| |
| /** |
| * @hide |
| */ |
| protected void onTimeChanged() { |
| Instant now = now(); |
| onTimeChanged(now.atZone(mClock.getZone()).toLocalTime(), now.toEpochMilli()); |
| } |
| |
| private void onTimeChanged(LocalTime localTime, long nowMillis) { |
| float previousHour = mHour; |
| float previousMinutes = mMinutes; |
| |
| float rawSeconds = localTime.getSecond() + localTime.getNano() / 1_000_000_000f; |
| // We round the fraction of the second so that the seconds hand always occupies the same |
| // n positions between two given numbers, where n is the number of ticks per second. This |
| // ensures the second hand advances by a consistent distance despite our handler callbacks |
| // occurring at inconsistent frequencies. |
| mSeconds = |
| mSecondsHandFps <= 0 |
| ? rawSeconds |
| : Math.round(rawSeconds * mSecondsHandFps) / (float) mSecondsHandFps; |
| mMinutes = localTime.getMinute() + mSeconds / 60.0f; |
| mHour = localTime.getHour() + mMinutes / 60.0f; |
| mChanged = true; |
| |
| // Update the content description only if the announced hours and minutes have changed. |
| if ((int) previousHour != (int) mHour || (int) previousMinutes != (int) mMinutes) { |
| updateContentDescription(nowMillis); |
| } |
| } |
| |
| /** Intent receiver for the time or time zone changing. */ |
| private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| if (Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) { |
| createClock(); |
| } |
| |
| mTick.run(); |
| } |
| }; |
| private boolean mReceiverAttached; |
| private ClockEventDelegate mClockEventDelegate; |
| |
| private final Runnable mTick = new Runnable() { |
| @Override |
| public void run() { |
| removeCallbacks(this); |
| if (!mVisible) { |
| return; |
| } |
| |
| Instant now = now(); |
| ZonedDateTime zonedDateTime = now.atZone(mClock.getZone()); |
| LocalTime localTime = zonedDateTime.toLocalTime(); |
| |
| long millisUntilNextTick; |
| if (mSecondHand == null || mSecondsHandFps <= 0) { |
| // If there's no second hand, then tick at the start of the next minute. |
| // |
| // This must be done with ZonedDateTime as opposed to LocalDateTime to ensure proper |
| // handling of DST. Also note that because of leap seconds, it should not be assumed |
| // that one minute == 60 seconds. |
| Instant startOfNextMinute = zonedDateTime.plusMinutes(1).withSecond(0).toInstant(); |
| millisUntilNextTick = Duration.between(now, startOfNextMinute).toMillis(); |
| if (millisUntilNextTick <= 0) { |
| // This should never occur, but if it does, then just check the tick again in |
| // one minute to ensure we're always moving forward. |
| millisUntilNextTick = Duration.ofMinutes(1).toMillis(); |
| } |
| } else { |
| // If there is a seconds hand, then determine the next tick point based on the fps. |
| // |
| // How many milliseconds through the second we currently are. |
| long millisOfSecond = Duration.ofNanos(localTime.getNano()).toMillis(); |
| // How many milliseconds there are between tick positions for the seconds hand. |
| double millisPerTick = 1000 / (double) mSecondsHandFps; |
| // How many milliseconds we are past the last tick position. |
| long millisPastLastTick = Math.round(millisOfSecond % millisPerTick); |
| // How many milliseconds there are until the next tick position. |
| millisUntilNextTick = Math.round(millisPerTick - millisPastLastTick); |
| // If we are exactly at the tick position, this could be 0 milliseconds due to |
| // rounding. In this case, advance by the full amount of millis to the next |
| // position. |
| if (millisUntilNextTick <= 0) { |
| millisUntilNextTick = Math.round(millisPerTick); |
| } |
| } |
| |
| // Schedule a callback for when the next tick should occur. |
| postDelayed(this, millisUntilNextTick); |
| |
| onTimeChanged(localTime, now.toEpochMilli()); |
| |
| invalidate(); |
| } |
| }; |
| |
| private void createClock() { |
| ZoneId zoneId = mTimeZone; |
| if (zoneId == null) { |
| mClock = Clock.systemDefaultZone(); |
| } else { |
| mClock = Clock.system(zoneId); |
| } |
| } |
| |
| private void updateContentDescription(long timeMillis) { |
| final int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_24HOUR; |
| String contentDescription = |
| DateUtils.formatDateRange( |
| mContext, |
| new Formatter(new StringBuilder(50), Locale.getDefault()), |
| timeMillis /* startMillis */, |
| timeMillis /* endMillis */, |
| flags, |
| getTimeZone()) |
| .toString(); |
| setContentDescription(contentDescription); |
| } |
| |
| /** |
| * Tries to parse a {@link ZoneId} from {@code timeZone}, returning null if it is null or there |
| * is an error parsing. |
| */ |
| @Nullable |
| private static ZoneId toZoneId(@Nullable String timeZone) { |
| if (timeZone == null) { |
| return null; |
| } |
| |
| try { |
| return ZoneId.of(timeZone); |
| } catch (DateTimeException e) { |
| Log.w(LOG_TAG, "Failed to parse time zone from " + timeZone, e); |
| return null; |
| } |
| } |
| |
| private final class TintInfo { |
| boolean mHasTintList; |
| @Nullable ColorStateList mTintList; |
| boolean mHasTintBlendMode; |
| @Nullable BlendMode mTintBlendMode; |
| |
| /** |
| * Returns a mutated copy of {@code drawable} with tinting applied, or null if it's null. |
| */ |
| @Nullable |
| Drawable apply(@Nullable Drawable drawable) { |
| if (drawable == null) return null; |
| |
| Drawable newDrawable = drawable.mutate(); |
| |
| if (mHasTintList) { |
| newDrawable.setTintList(mTintList); |
| } |
| |
| if (mHasTintBlendMode) { |
| newDrawable.setTintBlendMode(mTintBlendMode); |
| } |
| |
| // All drawables should have the same state as the View itself. |
| if (drawable.isStateful()) { |
| newDrawable.setState(getDrawableState()); |
| } |
| |
| return newDrawable; |
| } |
| } |
| } |