blob: 3df09c24ca3021814743e74f3ea2ec6bb6dbd946 [file] [log] [blame]
/*
* 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.IntentFilter;
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 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 display 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.
*/
@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);
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();
IntentFilter filter = new IntentFilter();
if (!mReceiverAttached) {
filter.addAction(Intent.ACTION_TIME_CHANGED);
filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
// OK, this is gross but needed. This class is supported by the
// remote views mechanism and as a part of that the remote views
// can be inflated by a context for another user without the app
// having interact users permission - just for loading resources.
// For example, when adding widgets from a user profile to the
// home screen. Therefore, we register the receiver as the current
// user not the one the context is for.
getContext().registerReceiverAsUser(mIntentReceiver,
android.os.Process.myUserHandle(), filter, null, 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) {
getContext().unregisterReceiver(mIntentReceiver);
mReceiverAttached = false;
}
super.onDetachedFromWindow();
}
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 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;
}
}
}