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