1 /* 2 * Copyright (C) 2014 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.systemui.statusbar.phone; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.AnimatorSet; 22 import android.animation.ObjectAnimator; 23 import android.content.Context; 24 import android.graphics.drawable.AnimatedVectorDrawable; 25 import android.graphics.drawable.Drawable; 26 import android.text.TextUtils; 27 import android.util.AttributeSet; 28 import android.view.View; 29 30 import androidx.annotation.StyleRes; 31 import androidx.core.graphics.ColorUtils; 32 33 import com.android.app.animation.Interpolators; 34 import com.android.internal.annotations.VisibleForTesting; 35 import com.android.systemui.Flags; 36 import com.android.systemui.keyguard.KeyguardIndication; 37 import com.android.systemui.res.R; 38 import com.android.systemui.shared.shadow.DoubleShadowTextView; 39 40 /** 41 * A view to show hints on Keyguard ("Swipe up to unlock", "Tap again to open"). 42 */ 43 public class KeyguardIndicationTextView extends DoubleShadowTextView { 44 // Minimum luminance for texts to receive shadows. 45 private static final float MIN_TEXT_SHADOW_LUMINANCE = 0.5f; 46 public static final long Y_IN_DURATION = 600L; 47 48 @StyleRes 49 private static int sStyleId = R.style.TextAppearance_Keyguard_BottomArea; 50 @StyleRes 51 private static int sStyleWithDoubleShadowTextId = 52 R.style.TextAppearance_Keyguard_BottomArea_DoubleShadow; 53 @StyleRes 54 private static int sButtonStyleId = R.style.TextAppearance_Keyguard_BottomArea_Button; 55 56 private boolean mAnimationsEnabled = true; 57 private CharSequence mMessage; 58 private KeyguardIndication mKeyguardIndicationInfo; 59 60 private Animator mLastAnimator; 61 private boolean mAlwaysAnnounceText; 62 KeyguardIndicationTextView(Context context)63 public KeyguardIndicationTextView(Context context) { 64 super(context); 65 } 66 KeyguardIndicationTextView(Context context, AttributeSet attrs)67 public KeyguardIndicationTextView(Context context, AttributeSet attrs) { 68 super(context, attrs); 69 } 70 KeyguardIndicationTextView(Context context, AttributeSet attrs, int defStyleAttr)71 public KeyguardIndicationTextView(Context context, AttributeSet attrs, int defStyleAttr) { 72 super(context, attrs, defStyleAttr); 73 } 74 KeyguardIndicationTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)75 public KeyguardIndicationTextView(Context context, AttributeSet attrs, int defStyleAttr, 76 int defStyleRes) { 77 super(context, attrs, defStyleAttr, defStyleRes); 78 } 79 80 /** 81 * Clears message queue and currently shown message. 82 */ clearMessages()83 public void clearMessages() { 84 if (mLastAnimator != null) { 85 mLastAnimator.cancel(); 86 } 87 mMessage = ""; 88 setText(""); 89 } 90 91 /** 92 * Changes the text with an animation. 93 */ switchIndication(int textResId)94 public void switchIndication(int textResId) { 95 switchIndication(getResources().getText(textResId), null); 96 } 97 98 /** 99 * Changes the text with an animation. 100 * 101 * @param indication The text to show. 102 */ switchIndication(KeyguardIndication indication)103 public void switchIndication(KeyguardIndication indication) { 104 switchIndication(indication == null ? null : indication.getMessage(), indication); 105 } 106 107 /** 108 * Changes the text with an animation. 109 */ switchIndication(CharSequence text, KeyguardIndication indication)110 public void switchIndication(CharSequence text, KeyguardIndication indication) { 111 switchIndication(text, indication, true, null); 112 } 113 114 /** 115 * Controls whether the text displayed in the indication area will be announced always. 116 */ setAlwaysAnnounceEnabled(boolean enabled)117 public void setAlwaysAnnounceEnabled(boolean enabled) { 118 this.mAlwaysAnnounceText = enabled; 119 if (mAlwaysAnnounceText) { 120 // We will announce the text programmatically anyway. 121 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_NONE); 122 } else { 123 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE); 124 } 125 } 126 127 /** 128 * Updates the text with an optional animation. 129 * 130 * @param text The text to show. 131 * @param indication optional display information for the text 132 * @param animate whether to animate this indication in - we may not want this on AOD 133 * @param onAnimationEndCallback runnable called after this indication is animated in 134 */ switchIndication(CharSequence text, KeyguardIndication indication, boolean animate, Runnable onAnimationEndCallback)135 public void switchIndication(CharSequence text, KeyguardIndication indication, 136 boolean animate, Runnable onAnimationEndCallback) { 137 mMessage = text; 138 mKeyguardIndicationInfo = indication; 139 140 if (animate) { 141 final boolean hasIcon = indication != null && indication.getIcon() != null; 142 AnimatorSet animator = new AnimatorSet(); 143 // Make sure each animation is visible for a minimum amount of time, while not worrying 144 // about fading in blank text 145 if (!TextUtils.isEmpty(mMessage) || hasIcon) { 146 Animator inAnimator = getInAnimator(); 147 inAnimator.addListener(new AnimatorListenerAdapter() { 148 @Override 149 public void onAnimationEnd(Animator animation) { 150 super.onAnimationEnd(animation); 151 if (onAnimationEndCallback != null) { 152 onAnimationEndCallback.run(); 153 } 154 } 155 }); 156 animator.playSequentially(getOutAnimator(), inAnimator); 157 } else { 158 Animator outAnimator = getOutAnimator(); 159 outAnimator.addListener(new AnimatorListenerAdapter() { 160 @Override 161 public void onAnimationEnd(Animator animation) { 162 super.onAnimationEnd(animation); 163 if (onAnimationEndCallback != null) { 164 onAnimationEndCallback.run(); 165 } 166 } 167 }); 168 animator.play(outAnimator); 169 } 170 171 if (mLastAnimator != null) { 172 mLastAnimator.cancel(); 173 } 174 mLastAnimator = animator; 175 animator.start(); 176 } else { 177 setAlpha(1f); 178 setTranslationY(0f); 179 setNextIndication(); 180 if (onAnimationEndCallback != null) { 181 onAnimationEndCallback.run(); 182 } 183 if (mLastAnimator != null) { 184 mLastAnimator.cancel(); 185 mLastAnimator = null; 186 } 187 } 188 } 189 190 /** 191 * Get the message that should be shown after the previous text animates out. 192 */ getMessage()193 public CharSequence getMessage() { 194 return mMessage; 195 } 196 getOutAnimator()197 private AnimatorSet getOutAnimator() { 198 AnimatorSet animatorSet = new AnimatorSet(); 199 Animator fadeOut = ObjectAnimator.ofFloat(this, View.ALPHA, 0f); 200 fadeOut.setDuration(getFadeOutDuration()); 201 fadeOut.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN); 202 fadeOut.addListener(new AnimatorListenerAdapter() { 203 private boolean mCancelled = false; 204 @Override 205 public void onAnimationEnd(Animator animator) { 206 super.onAnimationEnd(animator); 207 if (!mCancelled) { 208 setNextIndication(); 209 } 210 } 211 212 @Override 213 public void onAnimationCancel(Animator animator) { 214 super.onAnimationCancel(animator); 215 mCancelled = true; 216 setAlpha(0); 217 } 218 }); 219 220 Animator yTranslate = 221 ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, 0, -getYTranslationPixels()); 222 yTranslate.setDuration(getFadeOutDuration()); 223 animatorSet.playTogether(fadeOut, yTranslate); 224 225 return animatorSet; 226 } 227 setNextIndication()228 private void setNextIndication() { 229 boolean forceAssertiveAccessibilityLiveRegion = false; 230 if (mKeyguardIndicationInfo != null) { 231 // First, update the style. 232 // If a background is set on the text, we don't want shadow on the text 233 if (mKeyguardIndicationInfo.getBackground() != null) { 234 setTextAppearance(sButtonStyleId); 235 } else { 236 // If text is transparent or dark color, don't draw any shadow 237 if (Flags.indicationTextA11yFix() && ColorUtils.calculateLuminance( 238 mKeyguardIndicationInfo.getTextColor().getDefaultColor()) 239 > MIN_TEXT_SHADOW_LUMINANCE) { 240 setTextAppearance(sStyleWithDoubleShadowTextId); 241 } else { 242 setTextAppearance(sStyleId); 243 } 244 } 245 setBackground(mKeyguardIndicationInfo.getBackground()); 246 setTextColor(mKeyguardIndicationInfo.getTextColor()); 247 setOnClickListener(mKeyguardIndicationInfo.getClickListener()); 248 setClickable(mKeyguardIndicationInfo.getClickListener() != null); 249 final Drawable icon = mKeyguardIndicationInfo.getIcon(); 250 if (icon != null) { 251 icon.setTint(getCurrentTextColor()); 252 if (icon instanceof AnimatedVectorDrawable) { 253 ((AnimatedVectorDrawable) icon).start(); 254 } 255 } 256 setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null); 257 forceAssertiveAccessibilityLiveRegion = 258 mKeyguardIndicationInfo.getForceAssertiveAccessibilityLiveRegion(); 259 } 260 if (!forceAssertiveAccessibilityLiveRegion) { 261 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_NONE); 262 } 263 setText(mMessage); 264 if (forceAssertiveAccessibilityLiveRegion) { 265 setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_ASSERTIVE); 266 } 267 if (mAlwaysAnnounceText) { 268 announceForAccessibility(mMessage); 269 } 270 } 271 getInAnimator()272 private AnimatorSet getInAnimator() { 273 AnimatorSet animatorSet = new AnimatorSet(); 274 ObjectAnimator fadeIn = ObjectAnimator.ofFloat(this, View.ALPHA, 1f); 275 fadeIn.setStartDelay(getFadeInDelay()); 276 fadeIn.setDuration(getFadeInDuration()); 277 fadeIn.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); 278 279 Animator yTranslate = 280 ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, getYTranslationPixels(), 0); 281 yTranslate.setDuration(getYInDuration()); 282 yTranslate.addListener(new AnimatorListenerAdapter() { 283 @Override 284 public void onAnimationCancel(Animator animation) { 285 super.onAnimationCancel(animation); 286 setTranslationY(0); 287 setAlpha(1f); 288 } 289 }); 290 animatorSet.playTogether(yTranslate, fadeIn); 291 292 return animatorSet; 293 } 294 295 @VisibleForTesting setAnimationsEnabled(boolean enabled)296 public void setAnimationsEnabled(boolean enabled) { 297 mAnimationsEnabled = enabled; 298 } 299 getFadeInDelay()300 private long getFadeInDelay() { 301 if (!mAnimationsEnabled) return 0L; 302 return 150L; 303 } 304 getFadeInDuration()305 private long getFadeInDuration() { 306 if (!mAnimationsEnabled) return 0L; 307 return 317L; 308 } 309 getYInDuration()310 private long getYInDuration() { 311 if (!mAnimationsEnabled) return 0L; 312 return Y_IN_DURATION; 313 } 314 getFadeOutDuration()315 private long getFadeOutDuration() { 316 if (!mAnimationsEnabled) return 0L; 317 return 167L; 318 } 319 getYTranslationPixels()320 private int getYTranslationPixels() { 321 return mContext.getResources().getDimensionPixelSize( 322 com.android.systemui.res.R.dimen.keyguard_indication_y_translation); 323 } 324 } 325