• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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 com.android.keyguard;
18 
19 import android.annotation.FloatRange;
20 import android.annotation.IntRange;
21 import android.content.Context;
22 import android.content.res.Resources;
23 import android.content.res.TypedArray;
24 import android.graphics.Canvas;
25 import android.text.format.DateFormat;
26 import android.util.AttributeSet;
27 import android.widget.TextView;
28 
29 import com.android.systemui.R;
30 import com.android.systemui.statusbar.phone.KeyguardBypassController;
31 
32 import java.util.Calendar;
33 import java.util.Locale;
34 import java.util.TimeZone;
35 
36 import kotlin.Unit;
37 
38 /**
39  * Displays the time with the hour positioned above the minutes. (ie: 09 above 30 is 9:30)
40  * The time's text color is a gradient that changes its colors based on its controller.
41  */
42 public class AnimatableClockView extends TextView {
43     private static final CharSequence DOUBLE_LINE_FORMAT_12_HOUR = "hh\nmm";
44     private static final CharSequence DOUBLE_LINE_FORMAT_24_HOUR = "HH\nmm";
45     private static final long DOZE_ANIM_DURATION = 300;
46     private static final long APPEAR_ANIM_DURATION = 350;
47     private static final long CHARGE_ANIM_DURATION_PHASE_0 = 500;
48     private static final long CHARGE_ANIM_DURATION_PHASE_1 = 1000;
49 
50     private final Calendar mTime = Calendar.getInstance();
51 
52     private final int mDozingWeight;
53     private final int mLockScreenWeight;
54     private CharSequence mFormat;
55     private CharSequence mDescFormat;
56     private int mDozingColor;
57     private int mLockScreenColor;
58     private float mLineSpacingScale = 1f;
59     private int mChargeAnimationDelay = 0;
60 
61     private TextAnimator mTextAnimator = null;
62     private Runnable mOnTextAnimatorInitialized;
63 
64     private boolean mIsSingleLine;
65 
AnimatableClockView(Context context)66     public AnimatableClockView(Context context) {
67         this(context, null, 0, 0);
68     }
69 
AnimatableClockView(Context context, AttributeSet attrs)70     public AnimatableClockView(Context context, AttributeSet attrs) {
71         this(context, attrs, 0, 0);
72     }
73 
AnimatableClockView(Context context, AttributeSet attrs, int defStyleAttr)74     public AnimatableClockView(Context context, AttributeSet attrs, int defStyleAttr) {
75         this(context, attrs, defStyleAttr, 0);
76     }
77 
AnimatableClockView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)78     public AnimatableClockView(Context context, AttributeSet attrs, int defStyleAttr,
79             int defStyleRes) {
80         super(context, attrs, defStyleAttr, defStyleRes);
81         TypedArray ta = context.obtainStyledAttributes(
82                 attrs, R.styleable.AnimatableClockView, defStyleAttr, defStyleRes);
83         try {
84             mDozingWeight = ta.getInt(R.styleable.AnimatableClockView_dozeWeight, 100);
85             mLockScreenWeight = ta.getInt(R.styleable.AnimatableClockView_lockScreenWeight, 300);
86             mChargeAnimationDelay = ta.getInt(
87                     R.styleable.AnimatableClockView_chargeAnimationDelay, 200);
88         } finally {
89             ta.recycle();
90         }
91 
92         ta = context.obtainStyledAttributes(
93                 attrs, android.R.styleable.TextView, defStyleAttr, defStyleRes);
94         try {
95             mIsSingleLine = ta.getBoolean(android.R.styleable.TextView_singleLine, false);
96         } finally {
97             ta.recycle();
98         }
99 
100         refreshFormat();
101     }
102 
103     @Override
onAttachedToWindow()104     public void onAttachedToWindow() {
105         super.onAttachedToWindow();
106         refreshFormat();
107     }
108 
109     @Override
onDetachedFromWindow()110     public void onDetachedFromWindow() {
111         super.onDetachedFromWindow();
112     }
113 
refreshTime()114     void refreshTime() {
115         mTime.setTimeInMillis(System.currentTimeMillis());
116         setText(DateFormat.format(mFormat, mTime));
117         setContentDescription(DateFormat.format(mDescFormat, mTime));
118     }
119 
onTimeZoneChanged(TimeZone timeZone)120     void onTimeZoneChanged(TimeZone timeZone) {
121         mTime.setTimeZone(timeZone);
122         refreshFormat();
123     }
124 
125     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)126     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
127         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
128         if (mTextAnimator == null) {
129             mTextAnimator = new TextAnimator(
130                     getLayout(),
131                     () -> {
132                         invalidate();
133                         return Unit.INSTANCE;
134                     });
135             if (mOnTextAnimatorInitialized != null) {
136                 mOnTextAnimatorInitialized.run();
137                 mOnTextAnimatorInitialized = null;
138             }
139         } else {
140             mTextAnimator.updateLayout(getLayout());
141         }
142     }
143 
144     @Override
onDraw(Canvas canvas)145     protected void onDraw(Canvas canvas) {
146         mTextAnimator.draw(canvas);
147     }
148 
setLineSpacingScale(float scale)149     void setLineSpacingScale(float scale) {
150         mLineSpacingScale = scale;
151         setLineSpacing(0, mLineSpacingScale);
152     }
153 
setColors(int dozingColor, int lockScreenColor)154     void setColors(int dozingColor, int lockScreenColor) {
155         mDozingColor = dozingColor;
156         mLockScreenColor = lockScreenColor;
157     }
158 
animateAppearOnLockscreen()159     void animateAppearOnLockscreen() {
160         if (mTextAnimator == null) {
161             return;
162         }
163 
164         setTextStyle(
165                 mDozingWeight,
166                 -1 /* text size, no update */,
167                 mLockScreenColor,
168                 false /* animate */,
169                 0 /* duration */,
170                 0 /* delay */,
171                 null /* onAnimationEnd */);
172 
173         setTextStyle(
174                 mLockScreenWeight,
175                 -1 /* text size, no update */,
176                 mLockScreenColor,
177                 true, /* animate */
178                 APPEAR_ANIM_DURATION,
179                 0 /* delay */,
180                 null /* onAnimationEnd */);
181     }
182 
animateDisappear()183     void animateDisappear() {
184         if (mTextAnimator == null) {
185             return;
186         }
187 
188         setTextStyle(
189                 0 /* weight */,
190                 -1 /* text size, no update */,
191                 null /* color, no update */,
192                 true /* animate */,
193                 KeyguardBypassController.BYPASS_FADE_DURATION /* duration */,
194                 0 /* delay */,
195                 null /* onAnimationEnd */);
196     }
197 
animateCharge(DozeStateGetter dozeStateGetter)198     void animateCharge(DozeStateGetter dozeStateGetter) {
199         if (mTextAnimator == null || mTextAnimator.isRunning()) {
200             // Skip charge animation if dozing animation is already playing.
201             return;
202         }
203         Runnable startAnimPhase2 = () -> setTextStyle(
204                 dozeStateGetter.isDozing() ? mDozingWeight : mLockScreenWeight/* weight */,
205                 -1,
206                 null,
207                 true /* animate */,
208                 CHARGE_ANIM_DURATION_PHASE_1,
209                 0 /* delay */,
210                 null /* onAnimationEnd */);
211         setTextStyle(dozeStateGetter.isDozing() ? mLockScreenWeight : mDozingWeight/* weight */,
212                 -1,
213                 null,
214                 true /* animate */,
215                 CHARGE_ANIM_DURATION_PHASE_0,
216                 mChargeAnimationDelay,
217                 startAnimPhase2);
218     }
219 
animateDoze(boolean isDozing, boolean animate)220     void animateDoze(boolean isDozing, boolean animate) {
221         setTextStyle(isDozing ? mDozingWeight : mLockScreenWeight /* weight */,
222                 -1,
223                 isDozing ? mDozingColor : mLockScreenColor,
224                 animate,
225                 DOZE_ANIM_DURATION,
226                 0 /* delay */,
227                 null /* onAnimationEnd */);
228     }
229 
230     /**
231      * Set text style with an optional animation.
232      *
233      * By passing -1 to weight, the view preserves its current weight.
234      * By passing -1 to textSize, the view preserves its current text size.
235      *
236      * @param weight text weight.
237      * @param textSize font size.
238      * @param animate true to animate the text style change, otherwise false.
239      */
setTextStyle( @ntRangefrom = 0, to = 1000) int weight, @FloatRange(from = 0) float textSize, Integer color, boolean animate, long duration, long delay, Runnable onAnimationEnd)240     private void setTextStyle(
241             @IntRange(from = 0, to = 1000) int weight,
242             @FloatRange(from = 0) float textSize,
243             Integer color,
244             boolean animate,
245             long duration,
246             long delay,
247             Runnable onAnimationEnd) {
248         if (mTextAnimator != null) {
249             mTextAnimator.setTextStyle(weight, textSize, color, animate, duration, null,
250                     delay, onAnimationEnd);
251         } else {
252             // when the text animator is set, update its start values
253             mOnTextAnimatorInitialized =
254                     () -> mTextAnimator.setTextStyle(
255                             weight, textSize, color, false, duration, null,
256                             delay, onAnimationEnd);
257         }
258     }
259 
refreshFormat()260     void refreshFormat() {
261         Patterns.update(mContext);
262 
263         final boolean use24HourFormat = DateFormat.is24HourFormat(getContext());
264         if (mIsSingleLine && use24HourFormat) {
265             mFormat = Patterns.sClockView24;
266         } else if (!mIsSingleLine && use24HourFormat) {
267             mFormat = DOUBLE_LINE_FORMAT_24_HOUR;
268         } else if (mIsSingleLine && !use24HourFormat) {
269             mFormat = Patterns.sClockView12;
270         } else {
271             mFormat = DOUBLE_LINE_FORMAT_12_HOUR;
272         }
273 
274         mDescFormat = use24HourFormat ? Patterns.sClockView24 : Patterns.sClockView12;
275         refreshTime();
276     }
277 
278     // DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often.
279     // This is an optimization to ensure we only recompute the patterns when the inputs change.
280     private static final class Patterns {
281         static String sClockView12;
282         static String sClockView24;
283         static String sCacheKey;
284 
update(Context context)285         static void update(Context context) {
286             final Locale locale = Locale.getDefault();
287             final Resources res = context.getResources();
288             final String clockView12Skel = res.getString(R.string.clock_12hr_format);
289             final String clockView24Skel = res.getString(R.string.clock_24hr_format);
290             final String key = locale.toString() + clockView12Skel + clockView24Skel;
291             if (key.equals(sCacheKey)) return;
292             sClockView12 = DateFormat.getBestDateTimePattern(locale, clockView12Skel);
293 
294             // CLDR insists on adding an AM/PM indicator even though it wasn't in the skeleton
295             // format.  The following code removes the AM/PM indicator if we didn't want it.
296             if (!clockView12Skel.contains("a")) {
297                 sClockView12 = sClockView12.replaceAll("a", "").trim();
298             }
299             sClockView24 = DateFormat.getBestDateTimePattern(locale, clockView24Skel);
300             sCacheKey = key;
301         }
302     }
303 
304     interface DozeStateGetter {
isDozing()305         boolean isDozing();
306     }
307 }
308