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