blob: 409ae3f3c7d604318ccee65d84c26c5b8d3a5596 [file] [log] [blame]
Selim Cinek4e8b9ed2014-06-20 16:37:04 -07001/*
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
17package com.android.keyguard;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.AnimatorSet;
22import android.animation.ValueAnimator;
23import android.content.Context;
24import android.content.res.TypedArray;
25import android.graphics.Canvas;
Lucas Dupin987f1932017-05-13 21:02:52 -070026import android.graphics.Color;
Selim Cinek4e8b9ed2014-06-20 16:37:04 -070027import android.graphics.Paint;
28import android.graphics.Rect;
29import android.graphics.Typeface;
30import android.os.PowerManager;
31import android.os.SystemClock;
32import android.provider.Settings;
Adrian Roosa48caee2015-01-15 19:57:51 +010033import android.text.InputType;
Phil Weaverc355c7a2017-12-20 10:54:13 -080034import android.text.TextUtils;
Selim Cinek4e8b9ed2014-06-20 16:37:04 -070035import android.util.AttributeSet;
Evan Rosky90427502016-04-01 16:04:23 -070036import android.view.Gravity;
Selim Cinek4e8b9ed2014-06-20 16:37:04 -070037import android.view.View;
Adrian Roosa48caee2015-01-15 19:57:51 +010038import android.view.accessibility.AccessibilityEvent;
39import android.view.accessibility.AccessibilityManager;
40import android.view.accessibility.AccessibilityNodeInfo;
Selim Cinek4e8b9ed2014-06-20 16:37:04 -070041import android.view.animation.AnimationUtils;
42import android.view.animation.Interpolator;
Phil Weaver385912e2017-02-10 10:06:56 -080043import android.widget.EditText;
Selim Cinek4e8b9ed2014-06-20 16:37:04 -070044
Sunny Goyal87fccf02019-08-13 17:39:10 -070045import com.android.systemui.R;
46
Selim Cinek4e8b9ed2014-06-20 16:37:04 -070047import java.util.ArrayList;
48import java.util.Stack;
49
50/**
51 * A View similar to a textView which contains password text and can animate when the text is
52 * changed
53 */
54public class PasswordTextView extends View {
55
56 private static final float DOT_OVERSHOOT_FACTOR = 1.5f;
57 private static final long DOT_APPEAR_DURATION_OVERSHOOT = 320;
58 private static final long APPEAR_DURATION = 160;
59 private static final long DISAPPEAR_DURATION = 160;
60 private static final long RESET_DELAY_PER_ELEMENT = 40;
61 private static final long RESET_MAX_DELAY = 200;
62
63 /**
64 * The overlap between the text disappearing and the dot appearing animation
65 */
66 private static final long DOT_APPEAR_TEXT_DISAPPEAR_OVERLAP_DURATION = 130;
67
68 /**
69 * The duration the text needs to stay there at least before it can morph into a dot
70 */
71 private static final long TEXT_REST_DURATION_AFTER_APPEAR = 100;
72
73 /**
74 * The duration the text should be visible, starting with the appear animation
75 */
76 private static final long TEXT_VISIBILITY_DURATION = 1300;
77
78 /**
79 * The position in time from [0,1] where the overshoot should be finished and the settle back
80 * animation of the dot should start
81 */
82 private static final float OVERSHOOT_TIME_POSITION = 0.5f;
83
Phil Weaverc355c7a2017-12-20 10:54:13 -080084 private static char DOT = '\u2022';
85
Selim Cinek4e8b9ed2014-06-20 16:37:04 -070086 /**
87 * The raw text size, will be multiplied by the scaled density when drawn
88 */
89 private final int mTextHeightRaw;
Evan Rosky90427502016-04-01 16:04:23 -070090 private final int mGravity;
Selim Cinek4e8b9ed2014-06-20 16:37:04 -070091 private ArrayList<CharState> mTextChars = new ArrayList<>();
92 private String mText = "";
93 private Stack<CharState> mCharPool = new Stack<>();
94 private int mDotSize;
95 private PowerManager mPM;
96 private int mCharPadding;
97 private final Paint mDrawPaint = new Paint();
98 private Interpolator mAppearInterpolator;
99 private Interpolator mDisappearInterpolator;
100 private Interpolator mFastOutSlowInInterpolator;
101 private boolean mShowPassword;
Xiyuan Xia09eb0332015-05-13 15:29:42 -0700102 private UserActivityListener mUserActivityListener;
103
104 public interface UserActivityListener {
105 void onUserActivity();
106 }
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700107
108 public PasswordTextView(Context context) {
109 this(context, null);
110 }
111
112 public PasswordTextView(Context context, AttributeSet attrs) {
113 this(context, attrs, 0);
114 }
115
116 public PasswordTextView(Context context, AttributeSet attrs, int defStyleAttr) {
117 this(context, attrs, defStyleAttr, 0);
118 }
119
120 public PasswordTextView(Context context, AttributeSet attrs, int defStyleAttr,
121 int defStyleRes) {
122 super(context, attrs, defStyleAttr, defStyleRes);
123 setFocusableInTouchMode(true);
124 setFocusable(true);
125 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PasswordTextView);
126 try {
127 mTextHeightRaw = a.getInt(R.styleable.PasswordTextView_scaledTextSize, 0);
Evan Rosky90427502016-04-01 16:04:23 -0700128 mGravity = a.getInt(R.styleable.PasswordTextView_android_gravity, Gravity.CENTER);
129 mDotSize = a.getDimensionPixelSize(R.styleable.PasswordTextView_dotSize,
130 getContext().getResources().getDimensionPixelSize(R.dimen.password_dot_size));
131 mCharPadding = a.getDimensionPixelSize(R.styleable.PasswordTextView_charPadding,
132 getContext().getResources().getDimensionPixelSize(
133 R.dimen.password_char_padding));
Lucas Dupin987f1932017-05-13 21:02:52 -0700134 int textColor = a.getColor(R.styleable.PasswordTextView_android_textColor, Color.WHITE);
135 mDrawPaint.setColor(textColor);
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700136 } finally {
137 a.recycle();
138 }
139 mDrawPaint.setFlags(Paint.SUBPIXEL_TEXT_FLAG | Paint.ANTI_ALIAS_FLAG);
140 mDrawPaint.setTextAlign(Paint.Align.CENTER);
Andrew Sappersteinfec80922017-06-13 18:36:28 -0700141 mDrawPaint.setTypeface(Typeface.create(
Fabian Kozynski8a7a3342018-12-13 17:11:57 -0500142 context.getString(com.android.internal.R.string.config_headlineFontFamily),
Andrew Sappersteinfec80922017-06-13 18:36:28 -0700143 0));
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700144 mShowPassword = Settings.System.getInt(mContext.getContentResolver(),
145 Settings.System.TEXT_SHOW_PASSWORD, 1) == 1;
146 mAppearInterpolator = AnimationUtils.loadInterpolator(mContext,
147 android.R.interpolator.linear_out_slow_in);
148 mDisappearInterpolator = AnimationUtils.loadInterpolator(mContext,
149 android.R.interpolator.fast_out_linear_in);
150 mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(mContext,
151 android.R.interpolator.fast_out_slow_in);
152 mPM = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
153 }
154
155 @Override
156 protected void onDraw(Canvas canvas) {
157 float totalDrawingWidth = getDrawingWidth();
Evan Rosky90427502016-04-01 16:04:23 -0700158 float currentDrawPosition;
Evan Rosky22d12012016-04-07 14:17:13 -0700159 if ((mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.LEFT) {
160 if ((mGravity & Gravity.RELATIVE_LAYOUT_DIRECTION) != 0
161 && getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
Evan Rosky90427502016-04-01 16:04:23 -0700162 currentDrawPosition = getWidth() - getPaddingRight() - totalDrawingWidth;
163 } else {
164 currentDrawPosition = getPaddingLeft();
165 }
166 } else {
167 currentDrawPosition = getWidth() / 2 - totalDrawingWidth / 2;
168 }
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700169 int length = mTextChars.size();
170 Rect bounds = getCharBounds();
171 int charHeight = (bounds.bottom - bounds.top);
Evan Rosky90427502016-04-01 16:04:23 -0700172 float yPosition =
173 (getHeight() - getPaddingBottom() - getPaddingTop()) / 2 + getPaddingTop();
174 canvas.clipRect(getPaddingLeft(), getPaddingTop(),
Evan Rosky22d12012016-04-07 14:17:13 -0700175 getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700176 float charLength = bounds.right - bounds.left;
177 for (int i = 0; i < length; i++) {
178 CharState charState = mTextChars.get(i);
179 float charWidth = charState.draw(canvas, currentDrawPosition, charHeight, yPosition,
180 charLength);
181 currentDrawPosition += charWidth;
182 }
183 }
184
Selim Cinek63804822014-09-10 16:39:01 +0200185 @Override
186 public boolean hasOverlappingRendering() {
187 return false;
188 }
189
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700190 private Rect getCharBounds() {
191 float textHeight = mTextHeightRaw * getResources().getDisplayMetrics().scaledDensity;
192 mDrawPaint.setTextSize(textHeight);
193 Rect bounds = new Rect();
194 mDrawPaint.getTextBounds("0", 0, 1, bounds);
195 return bounds;
196 }
197
198 private float getDrawingWidth() {
199 int width = 0;
200 int length = mTextChars.size();
201 Rect bounds = getCharBounds();
202 int charLength = bounds.right - bounds.left;
203 for (int i = 0; i < length; i++) {
204 CharState charState = mTextChars.get(i);
205 if (i != 0) {
206 width += mCharPadding * charState.currentWidthFactor;
207 }
208 width += charLength * charState.currentWidthFactor;
209 }
210 return width;
211 }
212
213
214 public void append(char c) {
215 int visibleChars = mTextChars.size();
Phil Weaverc355c7a2017-12-20 10:54:13 -0800216 CharSequence textbefore = getTransformedText();
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700217 mText = mText + c;
218 int newLength = mText.length();
219 CharState charState;
220 if (newLength > visibleChars) {
221 charState = obtainCharState(c);
222 mTextChars.add(charState);
223 } else {
224 charState = mTextChars.get(newLength - 1);
225 charState.whichChar = c;
226 }
227 charState.startAppearAnimation();
228
229 // ensure that the previous element is being swapped
230 if (newLength > 1) {
231 CharState previousState = mTextChars.get(newLength - 2);
232 if (previousState.isDotSwapPending) {
233 previousState.swapToDotWhenAppearFinished();
234 }
235 }
236 userActivity();
Adrian Roosa48caee2015-01-15 19:57:51 +0100237 sendAccessibilityEventTypeViewTextChanged(textbefore, textbefore.length(), 0, 1);
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700238 }
239
Xiyuan Xia09eb0332015-05-13 15:29:42 -0700240 public void setUserActivityListener(UserActivityListener userActivitiListener) {
241 mUserActivityListener = userActivitiListener;
242 }
243
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700244 private void userActivity() {
245 mPM.userActivity(SystemClock.uptimeMillis(), false);
Xiyuan Xia09eb0332015-05-13 15:29:42 -0700246 if (mUserActivityListener != null) {
247 mUserActivityListener.onUserActivity();
248 }
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700249 }
250
251 public void deleteLastChar() {
252 int length = mText.length();
Phil Weaverc355c7a2017-12-20 10:54:13 -0800253 CharSequence textbefore = getTransformedText();
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700254 if (length > 0) {
255 mText = mText.substring(0, length - 1);
256 CharState charState = mTextChars.get(length - 1);
257 charState.startRemoveAnimation(0, 0);
Selim Cinek8a72b062017-06-22 15:24:11 -0700258 sendAccessibilityEventTypeViewTextChanged(textbefore, textbefore.length() - 1, 1, 0);
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700259 }
260 userActivity();
261 }
262
263 public String getText() {
264 return mText;
265 }
266
Phil Weaverc355c7a2017-12-20 10:54:13 -0800267 private CharSequence getTransformedText() {
268 int textLength = mTextChars.size();
269 StringBuilder stringBuilder = new StringBuilder(textLength);
270 for (int i = 0; i < textLength; i++) {
271 CharState charState = mTextChars.get(i);
272 // If the dot is disappearing, the character is disappearing entirely. Consider
273 // it gone.
274 if (charState.dotAnimator != null && !charState.dotAnimationIsGrowing) {
275 continue;
276 }
277 stringBuilder.append(charState.isCharVisibleForA11y() ? charState.whichChar : DOT);
278 }
279 return stringBuilder;
280 }
281
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700282 private CharState obtainCharState(char c) {
283 CharState charState;
284 if(mCharPool.isEmpty()) {
285 charState = new CharState();
286 } else {
287 charState = mCharPool.pop();
288 charState.reset();
289 }
290 charState.whichChar = c;
291 return charState;
292 }
293
Jim Miller4db942c2016-05-16 18:06:50 -0700294 public void reset(boolean animated, boolean announce) {
Phil Weaverc355c7a2017-12-20 10:54:13 -0800295 CharSequence textbefore = getTransformedText();
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700296 mText = "";
297 int length = mTextChars.size();
298 int middleIndex = (length - 1) / 2;
299 long delayPerElement = RESET_DELAY_PER_ELEMENT;
300 for (int i = 0; i < length; i++) {
301 CharState charState = mTextChars.get(i);
302 if (animated) {
303 int delayIndex;
304 if (i <= middleIndex) {
305 delayIndex = i * 2;
306 } else {
307 int distToMiddle = i - middleIndex;
308 delayIndex = (length - 1) - (distToMiddle - 1) * 2;
309 }
310 long startDelay = delayIndex * delayPerElement;
311 startDelay = Math.min(startDelay, RESET_MAX_DELAY);
312 long maxDelay = delayPerElement * (length - 1);
313 maxDelay = Math.min(maxDelay, RESET_MAX_DELAY) + DISAPPEAR_DURATION;
314 charState.startRemoveAnimation(startDelay, maxDelay);
315 charState.removeDotSwapCallbacks();
316 } else {
317 mCharPool.push(charState);
318 }
319 }
320 if (!animated) {
321 mTextChars.clear();
322 }
Jim Miller4db942c2016-05-16 18:06:50 -0700323 if (announce) {
324 sendAccessibilityEventTypeViewTextChanged(textbefore, 0, textbefore.length(), 0);
325 }
Adrian Roosa48caee2015-01-15 19:57:51 +0100326 }
327
Phil Weaverc355c7a2017-12-20 10:54:13 -0800328 void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText, int fromIndex,
Adrian Roosa48caee2015-01-15 19:57:51 +0100329 int removedCount, int addedCount) {
Eugene Suslad4128ec2017-12-04 19:48:41 +0000330 if (AccessibilityManager.getInstance(mContext).isEnabled() &&
331 (isFocused() || isSelected() && isShown())) {
Adrian Roosa48caee2015-01-15 19:57:51 +0100332 AccessibilityEvent event =
333 AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
334 event.setFromIndex(fromIndex);
335 event.setRemovedCount(removedCount);
336 event.setAddedCount(addedCount);
337 event.setBeforeText(beforeText);
Phil Weaverc355c7a2017-12-20 10:54:13 -0800338 CharSequence transformedText = getTransformedText();
339 if (!TextUtils.isEmpty(transformedText)) {
340 event.getText().add(transformedText);
341 }
Adrian Roosa48caee2015-01-15 19:57:51 +0100342 event.setPassword(true);
343 sendAccessibilityEventUnchecked(event);
344 }
345 }
346
347 @Override
348 public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
349 super.onInitializeAccessibilityEvent(event);
350
Phil Weaver385912e2017-02-10 10:06:56 -0800351 event.setClassName(EditText.class.getName());
Adrian Roosa48caee2015-01-15 19:57:51 +0100352 event.setPassword(true);
353 }
354
355 @Override
Adrian Roosa48caee2015-01-15 19:57:51 +0100356 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
357 super.onInitializeAccessibilityNodeInfo(info);
358
Phil Weaverc355c7a2017-12-20 10:54:13 -0800359 info.setClassName(EditText.class.getName());
Adrian Roosa48caee2015-01-15 19:57:51 +0100360 info.setPassword(true);
Phil Weaverc355c7a2017-12-20 10:54:13 -0800361 info.setText(getTransformedText());
Adrian Roosa48caee2015-01-15 19:57:51 +0100362
Adrian Roosa48caee2015-01-15 19:57:51 +0100363 info.setEditable(true);
364
365 info.setInputType(InputType.TYPE_NUMBER_VARIATION_PASSWORD);
366 }
367
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700368 private class CharState {
369 char whichChar;
370 ValueAnimator textAnimator;
371 boolean textAnimationIsGrowing;
372 Animator dotAnimator;
373 boolean dotAnimationIsGrowing;
374 ValueAnimator widthAnimator;
375 boolean widthAnimationIsGrowing;
376 float currentTextSizeFactor;
377 float currentDotSizeFactor;
378 float currentWidthFactor;
379 boolean isDotSwapPending;
380 float currentTextTranslationY = 1.0f;
381 ValueAnimator textTranslateAnimator;
382
383 Animator.AnimatorListener removeEndListener = new AnimatorListenerAdapter() {
384 private boolean mCancelled;
385 @Override
386 public void onAnimationCancel(Animator animation) {
387 mCancelled = true;
388 }
389
390 @Override
391 public void onAnimationEnd(Animator animation) {
392 if (!mCancelled) {
393 mTextChars.remove(CharState.this);
394 mCharPool.push(CharState.this);
395 reset();
396 cancelAnimator(textTranslateAnimator);
397 textTranslateAnimator = null;
398 }
399 }
400
401 @Override
402 public void onAnimationStart(Animator animation) {
403 mCancelled = false;
404 }
405 };
406
407 Animator.AnimatorListener dotFinishListener = new AnimatorListenerAdapter() {
408 @Override
409 public void onAnimationEnd(Animator animation) {
410 dotAnimator = null;
411 }
412 };
413
414 Animator.AnimatorListener textFinishListener = new AnimatorListenerAdapter() {
415 @Override
416 public void onAnimationEnd(Animator animation) {
417 textAnimator = null;
418 }
419 };
420
421 Animator.AnimatorListener textTranslateFinishListener = new AnimatorListenerAdapter() {
422 @Override
423 public void onAnimationEnd(Animator animation) {
424 textTranslateAnimator = null;
425 }
426 };
427
428 Animator.AnimatorListener widthFinishListener = new AnimatorListenerAdapter() {
429 @Override
430 public void onAnimationEnd(Animator animation) {
431 widthAnimator = null;
432 }
433 };
434
435 private ValueAnimator.AnimatorUpdateListener dotSizeUpdater
436 = new ValueAnimator.AnimatorUpdateListener() {
437 @Override
438 public void onAnimationUpdate(ValueAnimator animation) {
439 currentDotSizeFactor = (float) animation.getAnimatedValue();
440 invalidate();
441 }
442 };
443
444 private ValueAnimator.AnimatorUpdateListener textSizeUpdater
445 = new ValueAnimator.AnimatorUpdateListener() {
446 @Override
447 public void onAnimationUpdate(ValueAnimator animation) {
Phil Weaverc355c7a2017-12-20 10:54:13 -0800448 boolean textVisibleBefore = isCharVisibleForA11y();
449 float beforeTextSizeFactor = currentTextSizeFactor;
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700450 currentTextSizeFactor = (float) animation.getAnimatedValue();
Phil Weaverc355c7a2017-12-20 10:54:13 -0800451 if (textVisibleBefore != isCharVisibleForA11y()) {
452 currentTextSizeFactor = beforeTextSizeFactor;
453 CharSequence beforeText = getTransformedText();
454 currentTextSizeFactor = (float) animation.getAnimatedValue();
455 int indexOfThisChar = mTextChars.indexOf(CharState.this);
456 if (indexOfThisChar >= 0) {
457 sendAccessibilityEventTypeViewTextChanged(
458 beforeText, indexOfThisChar, 1, 1);
459 }
460 }
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700461 invalidate();
462 }
463 };
464
465 private ValueAnimator.AnimatorUpdateListener textTranslationUpdater
466 = new ValueAnimator.AnimatorUpdateListener() {
467 @Override
468 public void onAnimationUpdate(ValueAnimator animation) {
469 currentTextTranslationY = (float) animation.getAnimatedValue();
470 invalidate();
471 }
472 };
473
474 private ValueAnimator.AnimatorUpdateListener widthUpdater
475 = new ValueAnimator.AnimatorUpdateListener() {
476 @Override
477 public void onAnimationUpdate(ValueAnimator animation) {
478 currentWidthFactor = (float) animation.getAnimatedValue();
479 invalidate();
480 }
481 };
482
483 private Runnable dotSwapperRunnable = new Runnable() {
484 @Override
485 public void run() {
486 performSwap();
487 isDotSwapPending = false;
488 }
489 };
490
491 void reset() {
492 whichChar = 0;
493 currentTextSizeFactor = 0.0f;
494 currentDotSizeFactor = 0.0f;
495 currentWidthFactor = 0.0f;
496 cancelAnimator(textAnimator);
497 textAnimator = null;
498 cancelAnimator(dotAnimator);
499 dotAnimator = null;
500 cancelAnimator(widthAnimator);
501 widthAnimator = null;
502 currentTextTranslationY = 1.0f;
503 removeDotSwapCallbacks();
504 }
505
506 void startRemoveAnimation(long startDelay, long widthDelay) {
507 boolean dotNeedsAnimation = (currentDotSizeFactor > 0.0f && dotAnimator == null)
508 || (dotAnimator != null && dotAnimationIsGrowing);
509 boolean textNeedsAnimation = (currentTextSizeFactor > 0.0f && textAnimator == null)
510 || (textAnimator != null && textAnimationIsGrowing);
511 boolean widthNeedsAnimation = (currentWidthFactor > 0.0f && widthAnimator == null)
512 || (widthAnimator != null && widthAnimationIsGrowing);
513 if (dotNeedsAnimation) {
514 startDotDisappearAnimation(startDelay);
515 }
516 if (textNeedsAnimation) {
517 startTextDisappearAnimation(startDelay);
518 }
519 if (widthNeedsAnimation) {
520 startWidthDisappearAnimation(widthDelay);
521 }
522 }
523
524 void startAppearAnimation() {
525 boolean dotNeedsAnimation = !mShowPassword
526 && (dotAnimator == null || !dotAnimationIsGrowing);
527 boolean textNeedsAnimation = mShowPassword
528 && (textAnimator == null || !textAnimationIsGrowing);
529 boolean widthNeedsAnimation = (widthAnimator == null || !widthAnimationIsGrowing);
530 if (dotNeedsAnimation) {
531 startDotAppearAnimation(0);
532 }
533 if (textNeedsAnimation) {
534 startTextAppearAnimation();
535 }
536 if (widthNeedsAnimation) {
537 startWidthAppearAnimation();
538 }
539 if (mShowPassword) {
540 postDotSwap(TEXT_VISIBILITY_DURATION);
541 }
542 }
543
544 /**
545 * Posts a runnable which ensures that the text will be replaced by a dot after {@link
546 * com.android.keyguard.PasswordTextView#TEXT_VISIBILITY_DURATION}.
547 */
548 private void postDotSwap(long delay) {
549 removeDotSwapCallbacks();
550 postDelayed(dotSwapperRunnable, delay);
551 isDotSwapPending = true;
552 }
553
554 private void removeDotSwapCallbacks() {
555 removeCallbacks(dotSwapperRunnable);
556 isDotSwapPending = false;
557 }
558
559 void swapToDotWhenAppearFinished() {
560 removeDotSwapCallbacks();
561 if (textAnimator != null) {
562 long remainingDuration = textAnimator.getDuration()
563 - textAnimator.getCurrentPlayTime();
564 postDotSwap(remainingDuration + TEXT_REST_DURATION_AFTER_APPEAR);
565 } else {
566 performSwap();
567 }
568 }
569
570 private void performSwap() {
571 startTextDisappearAnimation(0);
572 startDotAppearAnimation(DISAPPEAR_DURATION
573 - DOT_APPEAR_TEXT_DISAPPEAR_OVERLAP_DURATION);
574 }
575
576 private void startWidthDisappearAnimation(long widthDelay) {
577 cancelAnimator(widthAnimator);
578 widthAnimator = ValueAnimator.ofFloat(currentWidthFactor, 0.0f);
579 widthAnimator.addUpdateListener(widthUpdater);
580 widthAnimator.addListener(widthFinishListener);
581 widthAnimator.addListener(removeEndListener);
582 widthAnimator.setDuration((long) (DISAPPEAR_DURATION * currentWidthFactor));
583 widthAnimator.setStartDelay(widthDelay);
584 widthAnimator.start();
585 widthAnimationIsGrowing = false;
586 }
587
588 private void startTextDisappearAnimation(long startDelay) {
589 cancelAnimator(textAnimator);
590 textAnimator = ValueAnimator.ofFloat(currentTextSizeFactor, 0.0f);
591 textAnimator.addUpdateListener(textSizeUpdater);
592 textAnimator.addListener(textFinishListener);
593 textAnimator.setInterpolator(mDisappearInterpolator);
594 textAnimator.setDuration((long) (DISAPPEAR_DURATION * currentTextSizeFactor));
595 textAnimator.setStartDelay(startDelay);
596 textAnimator.start();
597 textAnimationIsGrowing = false;
598 }
599
600 private void startDotDisappearAnimation(long startDelay) {
601 cancelAnimator(dotAnimator);
602 ValueAnimator animator = ValueAnimator.ofFloat(currentDotSizeFactor, 0.0f);
603 animator.addUpdateListener(dotSizeUpdater);
604 animator.addListener(dotFinishListener);
605 animator.setInterpolator(mDisappearInterpolator);
606 long duration = (long) (DISAPPEAR_DURATION * Math.min(currentDotSizeFactor, 1.0f));
607 animator.setDuration(duration);
608 animator.setStartDelay(startDelay);
609 animator.start();
610 dotAnimator = animator;
611 dotAnimationIsGrowing = false;
612 }
613
614 private void startWidthAppearAnimation() {
615 cancelAnimator(widthAnimator);
616 widthAnimator = ValueAnimator.ofFloat(currentWidthFactor, 1.0f);
617 widthAnimator.addUpdateListener(widthUpdater);
618 widthAnimator.addListener(widthFinishListener);
619 widthAnimator.setDuration((long) (APPEAR_DURATION * (1f - currentWidthFactor)));
620 widthAnimator.start();
621 widthAnimationIsGrowing = true;
622 }
623
624 private void startTextAppearAnimation() {
625 cancelAnimator(textAnimator);
626 textAnimator = ValueAnimator.ofFloat(currentTextSizeFactor, 1.0f);
627 textAnimator.addUpdateListener(textSizeUpdater);
628 textAnimator.addListener(textFinishListener);
629 textAnimator.setInterpolator(mAppearInterpolator);
630 textAnimator.setDuration((long) (APPEAR_DURATION * (1f - currentTextSizeFactor)));
631 textAnimator.start();
632 textAnimationIsGrowing = true;
633
634 // handle translation
635 if (textTranslateAnimator == null) {
636 textTranslateAnimator = ValueAnimator.ofFloat(1.0f, 0.0f);
637 textTranslateAnimator.addUpdateListener(textTranslationUpdater);
638 textTranslateAnimator.addListener(textTranslateFinishListener);
639 textTranslateAnimator.setInterpolator(mAppearInterpolator);
640 textTranslateAnimator.setDuration(APPEAR_DURATION);
641 textTranslateAnimator.start();
642 }
643 }
644
645 private void startDotAppearAnimation(long delay) {
646 cancelAnimator(dotAnimator);
647 if (!mShowPassword) {
648 // We perform an overshoot animation
649 ValueAnimator overShootAnimator = ValueAnimator.ofFloat(currentDotSizeFactor,
650 DOT_OVERSHOOT_FACTOR);
651 overShootAnimator.addUpdateListener(dotSizeUpdater);
652 overShootAnimator.setInterpolator(mAppearInterpolator);
653 long overShootDuration = (long) (DOT_APPEAR_DURATION_OVERSHOOT
654 * OVERSHOOT_TIME_POSITION);
655 overShootAnimator.setDuration(overShootDuration);
656 ValueAnimator settleBackAnimator = ValueAnimator.ofFloat(DOT_OVERSHOOT_FACTOR,
657 1.0f);
658 settleBackAnimator.addUpdateListener(dotSizeUpdater);
659 settleBackAnimator.setDuration(DOT_APPEAR_DURATION_OVERSHOOT - overShootDuration);
660 settleBackAnimator.addListener(dotFinishListener);
661 AnimatorSet animatorSet = new AnimatorSet();
662 animatorSet.playSequentially(overShootAnimator, settleBackAnimator);
663 animatorSet.setStartDelay(delay);
664 animatorSet.start();
665 dotAnimator = animatorSet;
666 } else {
667 ValueAnimator growAnimator = ValueAnimator.ofFloat(currentDotSizeFactor, 1.0f);
668 growAnimator.addUpdateListener(dotSizeUpdater);
669 growAnimator.setDuration((long) (APPEAR_DURATION * (1.0f - currentDotSizeFactor)));
670 growAnimator.addListener(dotFinishListener);
671 growAnimator.setStartDelay(delay);
672 growAnimator.start();
673 dotAnimator = growAnimator;
674 }
675 dotAnimationIsGrowing = true;
676 }
677
678 private void cancelAnimator(Animator animator) {
679 if (animator != null) {
680 animator.cancel();
681 }
682 }
683
684 /**
685 * Draw this char to the canvas.
686 *
687 * @return The width this character contributes, including padding.
688 */
689 public float draw(Canvas canvas, float currentDrawPosition, int charHeight, float yPosition,
690 float charLength) {
691 boolean textVisible = currentTextSizeFactor > 0;
692 boolean dotVisible = currentDotSizeFactor > 0;
693 float charWidth = charLength * currentWidthFactor;
694 if (textVisible) {
695 float currYPosition = yPosition + charHeight / 2.0f * currentTextSizeFactor
696 + charHeight * currentTextTranslationY * 0.8f;
697 canvas.save();
698 float centerX = currentDrawPosition + charWidth / 2;
699 canvas.translate(centerX, currYPosition);
700 canvas.scale(currentTextSizeFactor, currentTextSizeFactor);
701 canvas.drawText(Character.toString(whichChar), 0, 0, mDrawPaint);
702 canvas.restore();
703 }
704 if (dotVisible) {
705 canvas.save();
706 float centerX = currentDrawPosition + charWidth / 2;
707 canvas.translate(centerX, yPosition);
708 canvas.drawCircle(0, 0, mDotSize / 2 * currentDotSizeFactor, mDrawPaint);
709 canvas.restore();
710 }
711 return charWidth + mCharPadding * currentWidthFactor;
712 }
Phil Weaverc355c7a2017-12-20 10:54:13 -0800713
714 public boolean isCharVisibleForA11y() {
715 // The text has size 0 when it is first added, but we want to count it as visible if
716 // it will become visible presently. Count text as visible if an animator
717 // is configured to make it grow.
718 boolean textIsGrowing = textAnimator != null && textAnimationIsGrowing;
719 return (currentTextSizeFactor > 0) || textIsGrowing;
720 }
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700721 }
722}