• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 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 static com.android.systemui.Flags.notifyPasswordTextViewUserActivityInBackground;
20 
21 import android.animation.Animator;
22 import android.animation.AnimatorListenerAdapter;
23 import android.animation.AnimatorSet;
24 import android.animation.ValueAnimator;
25 import android.content.Context;
26 import android.content.res.Configuration;
27 import android.content.res.TypedArray;
28 import android.graphics.Canvas;
29 import android.graphics.Color;
30 import android.graphics.Paint;
31 import android.graphics.Rect;
32 import android.graphics.Typeface;
33 import android.os.PowerManager;
34 import android.os.SystemClock;
35 import android.util.AttributeSet;
36 import android.view.Gravity;
37 import android.view.LayoutInflater;
38 import android.view.animation.AnimationUtils;
39 import android.view.animation.Interpolator;
40 
41 import com.android.settingslib.Utils;
42 import com.android.systemui.res.R;
43 
44 import java.util.ArrayList;
45 
46 /**
47  * A View similar to a textView which contains password text and can animate when the text is
48  * changed
49  */
50 public class PasswordTextView extends BasePasswordTextView {
51     public static final long APPEAR_DURATION = 160;
52     public static final long DISAPPEAR_DURATION = 160;
53     private static final float DOT_OVERSHOOT_FACTOR = 1.5f;
54     private static final long DOT_APPEAR_DURATION_OVERSHOOT = 320;
55     private static final long RESET_DELAY_PER_ELEMENT = 40;
56     private static final long RESET_MAX_DELAY = 200;
57 
58     /**
59      * The overlap between the text disappearing and the dot appearing animation
60      */
61     private static final long DOT_APPEAR_TEXT_DISAPPEAR_OVERLAP_DURATION = 130;
62 
63     /**
64      * The duration the text needs to stay there at least before it can morph into a dot
65      */
66     private static final long TEXT_REST_DURATION_AFTER_APPEAR = 100;
67 
68     /**
69      * The duration the text should be visible, starting with the appear animation
70      */
71     private static final long TEXT_VISIBILITY_DURATION = 1300;
72 
73     /**
74      * The position in time from [0,1] where the overshoot should be finished and the settle back
75      * animation of the dot should start
76      */
77     private static final float OVERSHOOT_TIME_POSITION = 0.5f;
78 
79     /**
80      * The raw text size, will be multiplied by the scaled density when drawn
81      */
82     private int mTextHeightRaw;
83     private final int mGravity;
84     private ArrayList<CharState> mTextChars = new ArrayList<>();
85     private int mDotSize;
86     private PowerManager mPM;
87     private int mCharPadding;
88     private final Paint mDrawPaint = new Paint();
89     private int mDrawColor;
90     private Interpolator mAppearInterpolator;
91     private Interpolator mDisappearInterpolator;
92     private Interpolator mFastOutSlowInInterpolator;
93 
PasswordTextView(Context context)94     public PasswordTextView(Context context) {
95         this(context, null);
96     }
97 
PasswordTextView(Context context, AttributeSet attrs)98     public PasswordTextView(Context context, AttributeSet attrs) {
99         this(context, attrs, 0);
100     }
101 
PasswordTextView(Context context, AttributeSet attrs, int defStyleAttr)102     public PasswordTextView(Context context, AttributeSet attrs, int defStyleAttr) {
103         this(context, attrs, defStyleAttr, 0);
104     }
105 
PasswordTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)106     public PasswordTextView(Context context, AttributeSet attrs, int defStyleAttr,
107             int defStyleRes) {
108         super(context, attrs, defStyleAttr, defStyleRes);
109         TypedArray a = context.obtainStyledAttributes(attrs, android.R.styleable.View);
110         try {
111             // If defined, use the provided values. If not, set them to true by default.
112             boolean isFocusable = a.getBoolean(android.R.styleable.View_focusable,
113                     /* defValue= */ true);
114             boolean isFocusableInTouchMode = a.getBoolean(
115                     android.R.styleable.View_focusableInTouchMode, /* defValue= */ true);
116             setFocusable(isFocusable);
117             setFocusableInTouchMode(isFocusableInTouchMode);
118         } finally {
119             a.recycle();
120         }
121         a = context.obtainStyledAttributes(attrs, R.styleable.PasswordTextView);
122         try {
123             mTextHeightRaw = a.getInt(R.styleable.PasswordTextView_scaledTextSize, 0);
124             mGravity = a.getInt(R.styleable.PasswordTextView_android_gravity, Gravity.CENTER);
125             mDotSize = a.getDimensionPixelSize(R.styleable.PasswordTextView_dotSize,
126                     getContext().getResources().getDimensionPixelSize(R.dimen.password_dot_size));
127             mCharPadding = a.getDimensionPixelSize(R.styleable.PasswordTextView_charPadding,
128                     getContext().getResources().getDimensionPixelSize(
129                             R.dimen.password_char_padding));
130             mDrawColor = a.getColor(R.styleable.PasswordTextView_android_textColor, Color.WHITE);
131             mDrawPaint.setColor(mDrawColor);
132 
133         } finally {
134             a.recycle();
135         }
136 
137         mDrawPaint.setFlags(Paint.SUBPIXEL_TEXT_FLAG | Paint.ANTI_ALIAS_FLAG);
138         mDrawPaint.setTextAlign(Paint.Align.CENTER);
139         mDrawPaint.setTypeface(Typeface.create(
140                 context.getString(com.android.internal.R.string.config_headlineFontFamily), 0));
141         mAppearInterpolator = AnimationUtils.loadInterpolator(mContext,
142                 android.R.interpolator.linear_out_slow_in);
143         mDisappearInterpolator = AnimationUtils.loadInterpolator(mContext,
144                 android.R.interpolator.fast_out_linear_in);
145         mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(mContext,
146                 android.R.interpolator.fast_out_slow_in);
147         mPM = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
148         setWillNotDraw(false);
149     }
150 
151     @Override
inflatePinShapeInput(boolean isPinHinting)152     protected PinShapeInput inflatePinShapeInput(boolean isPinHinting) {
153         if (isPinHinting) {
154             return (PinShapeInput) LayoutInflater.from(mContext).inflate(
155                     R.layout.keyguard_pin_shape_hinting_view, null);
156         } else {
157             return (PinShapeInput) LayoutInflater.from(mContext).inflate(
158                     R.layout.keyguard_pin_shape_non_hinting_view, null);
159         }
160     }
161 
162     @Override
shouldSendAccessibilityEvent()163     protected boolean shouldSendAccessibilityEvent() {
164         return isFocused() || isSelected() && isShown();
165     }
166 
167     @Override
onDraw(Canvas canvas)168     protected void onDraw(Canvas canvas) {
169         // Do not use legacy draw animations for pin shapes.
170         if (mUsePinShapes) {
171             super.onDraw(canvas);
172             return;
173         }
174 
175         float totalDrawingWidth = getDrawingWidth();
176         float currentDrawPosition;
177         if ((mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.LEFT) {
178             if ((mGravity & Gravity.RELATIVE_LAYOUT_DIRECTION) != 0
179                     && getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
180                 currentDrawPosition = getWidth() - getPaddingRight() - totalDrawingWidth;
181             } else {
182                 currentDrawPosition = getPaddingLeft();
183             }
184         } else {
185             float maxRight = getWidth() - getPaddingRight() - totalDrawingWidth;
186             float center = getWidth() / 2f - totalDrawingWidth / 2f;
187             currentDrawPosition = center > 0 ? center : maxRight;
188         }
189         int length = mTextChars.size();
190         Rect bounds = getCharBounds();
191         int charHeight = (bounds.bottom - bounds.top);
192         float yPosition =
193                 (getHeight() - getPaddingBottom() - getPaddingTop()) / 2 + getPaddingTop();
194         canvas.clipRect(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(),
195                 getHeight() - getPaddingBottom());
196         float charLength = bounds.right - bounds.left;
197         for (int i = 0; i < length; i++) {
198             CharState charState = mTextChars.get(i);
199             float charWidth = charState.draw(canvas, currentDrawPosition, charHeight, yPosition,
200                     charLength);
201             currentDrawPosition += charWidth;
202         }
203     }
204 
205     @Override
onAppend(char c, int newLength)206     protected void onAppend(char c, int newLength) {
207         int visibleChars = mTextChars.size();
208         CharState charState;
209         if (newLength > visibleChars) {
210             charState = obtainCharState(c);
211             mTextChars.add(charState);
212         } else {
213             charState = mTextChars.get(newLength - 1);
214             charState.whichChar = c;
215         }
216         charState.startAppearAnimation();
217 
218         // ensure that the previous element is being swapped
219         if (newLength > 1) {
220             CharState previousState = mTextChars.get(newLength - 2);
221             if (previousState.isDotSwapPending) {
222                 previousState.swapToDotWhenAppearFinished();
223             }
224         }
225     }
226 
227     @Override
onDelete(int index)228     protected void onDelete(int index) {
229         CharState charState = mTextChars.get(index);
230         charState.startRemoveAnimation(0, 0);
231     }
232 
233     @Override
onReset(boolean animated)234     protected void onReset(boolean animated) {
235         if (animated) {
236             int length = mTextChars.size();
237             int middleIndex = (length - 1) / 2;
238             long delayPerElement = RESET_DELAY_PER_ELEMENT;
239             for (int i = 0; i < length; i++) {
240                 CharState charState = mTextChars.get(i);
241                 int delayIndex;
242                 if (i <= middleIndex) {
243                     delayIndex = i * 2;
244                 } else {
245                     int distToMiddle = i - middleIndex;
246                     delayIndex = (length - 1) - (distToMiddle - 1) * 2;
247                 }
248                 long startDelay = delayIndex * delayPerElement;
249                 startDelay = Math.min(startDelay, RESET_MAX_DELAY);
250                 long maxDelay = delayPerElement * (length - 1);
251                 maxDelay = Math.min(maxDelay, RESET_MAX_DELAY) + DISAPPEAR_DURATION;
252                 charState.startRemoveAnimation(startDelay, maxDelay);
253                 charState.removeDotSwapCallbacks();
254             }
255         } else {
256             mTextChars.clear();
257         }
258     }
259 
260     @Override
onUserActivity()261     protected void onUserActivity() {
262         if (!notifyPasswordTextViewUserActivityInBackground()) {
263             mPM.userActivity(SystemClock.uptimeMillis(), false);
264         }
265         super.onUserActivity();
266     }
267 
268     /**
269      * Reload colors from resources.
270      **/
reloadColors()271     public void reloadColors() {
272         mDrawColor = Utils.getColorAttr(getContext(),
273                 android.R.attr.textColorPrimary).getDefaultColor();
274         mDrawPaint.setColor(mDrawColor);
275         if (mPinShapeInput != null) {
276             mPinShapeInput.setDrawColor(mDrawColor);
277         }
278     }
279 
280     @Override
onConfigurationChanged(Configuration newConfig)281     protected void onConfigurationChanged(Configuration newConfig) {
282         mTextHeightRaw = getContext().getResources().getInteger(
283                 R.integer.scaled_password_text_size);
284     }
285 
getCharBounds()286     private Rect getCharBounds() {
287         float textHeight = mTextHeightRaw * getResources().getDisplayMetrics().scaledDensity;
288         mDrawPaint.setTextSize(textHeight);
289         Rect bounds = new Rect();
290         mDrawPaint.getTextBounds("0", 0, 1, bounds);
291         return bounds;
292     }
293 
getDrawingWidth()294     private float getDrawingWidth() {
295         int width = 0;
296         int length = mTextChars.size();
297         Rect bounds = getCharBounds();
298         int charLength = bounds.right - bounds.left;
299         for (int i = 0; i < length; i++) {
300             CharState charState = mTextChars.get(i);
301             if (i != 0) {
302                 width += mCharPadding * charState.currentWidthFactor;
303             }
304             width += charLength * charState.currentWidthFactor;
305         }
306         return width;
307     }
308 
obtainCharState(char c)309     private CharState obtainCharState(char c) {
310         CharState charState = new CharState();
311         charState.whichChar = c;
312         return charState;
313     }
314 
315     @Override
getTransformedText()316     protected CharSequence getTransformedText() {
317         int textLength = mTextChars.size();
318         StringBuilder stringBuilder = new StringBuilder(textLength);
319         for (int i = 0; i < textLength; i++) {
320             CharState charState = mTextChars.get(i);
321             // If the dot is disappearing, the character is disappearing entirely. Consider
322             // it gone.
323             if (charState.dotAnimator != null && !charState.dotAnimationIsGrowing) {
324                 continue;
325             }
326             stringBuilder.append(charState.isCharVisibleForA11y() ? charState.whichChar : DOT);
327         }
328         return stringBuilder;
329     }
330 
331     private class CharState {
332         char whichChar;
333         ValueAnimator textAnimator;
334         boolean textAnimationIsGrowing;
335         Animator dotAnimator;
336         boolean dotAnimationIsGrowing;
337         ValueAnimator widthAnimator;
338         boolean widthAnimationIsGrowing;
339         float currentTextSizeFactor;
340         float currentDotSizeFactor;
341         float currentWidthFactor;
342         boolean isDotSwapPending;
343         float currentTextTranslationY = 1.0f;
344         ValueAnimator textTranslateAnimator;
345 
346         Animator.AnimatorListener removeEndListener = new AnimatorListenerAdapter() {
347             private boolean mCancelled;
348 
349             @Override
350             public void onAnimationCancel(Animator animation) {
351                 mCancelled = true;
352             }
353 
354             @Override
355             public void onAnimationEnd(Animator animation) {
356                 if (!mCancelled) {
357                     mTextChars.remove(CharState.this);
358                     cancelAnimator(textTranslateAnimator);
359                     textTranslateAnimator = null;
360                 }
361             }
362 
363             @Override
364             public void onAnimationStart(Animator animation) {
365                 mCancelled = false;
366             }
367         };
368 
369         Animator.AnimatorListener dotFinishListener = new AnimatorListenerAdapter() {
370             @Override
371             public void onAnimationEnd(Animator animation) {
372                 dotAnimator = null;
373             }
374         };
375 
376         Animator.AnimatorListener textFinishListener = new AnimatorListenerAdapter() {
377             @Override
378             public void onAnimationEnd(Animator animation) {
379                 textAnimator = null;
380             }
381         };
382 
383         Animator.AnimatorListener textTranslateFinishListener = new AnimatorListenerAdapter() {
384             @Override
385             public void onAnimationEnd(Animator animation) {
386                 textTranslateAnimator = null;
387             }
388         };
389 
390         Animator.AnimatorListener widthFinishListener = new AnimatorListenerAdapter() {
391             @Override
392             public void onAnimationEnd(Animator animation) {
393                 widthAnimator = null;
394             }
395         };
396 
397         private ValueAnimator.AnimatorUpdateListener mDotSizeUpdater =
398                 new ValueAnimator.AnimatorUpdateListener() {
399                     @Override
400                     public void onAnimationUpdate(ValueAnimator animation) {
401                         currentDotSizeFactor = (float) animation.getAnimatedValue();
402                         invalidate();
403                     }
404                 };
405 
406         private ValueAnimator.AnimatorUpdateListener mTextSizeUpdater =
407                 new ValueAnimator.AnimatorUpdateListener() {
408                     @Override
409                     public void onAnimationUpdate(ValueAnimator animation) {
410                         boolean textVisibleBefore = isCharVisibleForA11y();
411                         float beforeTextSizeFactor = currentTextSizeFactor;
412                         currentTextSizeFactor = (float) animation.getAnimatedValue();
413                         if (textVisibleBefore != isCharVisibleForA11y()) {
414                             currentTextSizeFactor = beforeTextSizeFactor;
415                             CharSequence beforeText = getTransformedText();
416                             currentTextSizeFactor = (float) animation.getAnimatedValue();
417                             int indexOfThisChar = mTextChars.indexOf(CharState.this);
418                             if (indexOfThisChar >= 0) {
419                                 sendAccessibilityEventTypeViewTextChanged(beforeText,
420                                         indexOfThisChar, 1, 1);
421                             }
422                         }
423                         invalidate();
424                     }
425                 };
426 
427         private ValueAnimator.AnimatorUpdateListener mTextTranslationUpdater =
428                 new ValueAnimator.AnimatorUpdateListener() {
429                     @Override
430                     public void onAnimationUpdate(ValueAnimator animation) {
431                         currentTextTranslationY = (float) animation.getAnimatedValue();
432                         invalidate();
433                     }
434                 };
435 
436         private ValueAnimator.AnimatorUpdateListener mWidthUpdater =
437                 new ValueAnimator.AnimatorUpdateListener() {
438                     @Override
439                     public void onAnimationUpdate(ValueAnimator animation) {
440                         currentWidthFactor = (float) animation.getAnimatedValue();
441                         invalidate();
442                     }
443                 };
444 
445         private Runnable dotSwapperRunnable = new Runnable() {
446             @Override
447             public void run() {
448                 performSwap();
449                 isDotSwapPending = false;
450             }
451         };
452 
startRemoveAnimation(long startDelay, long widthDelay)453         void startRemoveAnimation(long startDelay, long widthDelay) {
454             boolean dotNeedsAnimation =
455                     (currentDotSizeFactor > 0.0f && dotAnimator == null) || (dotAnimator != null
456                             && dotAnimationIsGrowing);
457             boolean textNeedsAnimation =
458                     (currentTextSizeFactor > 0.0f && textAnimator == null) || (textAnimator != null
459                             && textAnimationIsGrowing);
460             boolean widthNeedsAnimation =
461                     (currentWidthFactor > 0.0f && widthAnimator == null) || (widthAnimator != null
462                             && widthAnimationIsGrowing);
463             if (dotNeedsAnimation) {
464                 startDotDisappearAnimation(startDelay);
465             }
466             if (textNeedsAnimation) {
467                 startTextDisappearAnimation(startDelay);
468             }
469             if (widthNeedsAnimation) {
470                 startWidthDisappearAnimation(widthDelay);
471             }
472         }
473 
startAppearAnimation()474         void startAppearAnimation() {
475             boolean dotNeedsAnimation =
476                     !mShowPassword && (dotAnimator == null || !dotAnimationIsGrowing);
477             boolean textNeedsAnimation =
478                     mShowPassword && (textAnimator == null || !textAnimationIsGrowing);
479             boolean widthNeedsAnimation = (widthAnimator == null || !widthAnimationIsGrowing);
480             if (dotNeedsAnimation) {
481                 startDotAppearAnimation(0);
482             }
483             if (textNeedsAnimation) {
484                 startTextAppearAnimation();
485             }
486             if (widthNeedsAnimation) {
487                 startWidthAppearAnimation();
488             }
489             if (mShowPassword) {
490                 postDotSwap(TEXT_VISIBILITY_DURATION);
491             }
492         }
493 
494         /**
495          * Posts a runnable which ensures that the text will be replaced by a dot after {@link
496          * com.android.keyguard.PasswordTextView#TEXT_VISIBILITY_DURATION}.
497          */
postDotSwap(long delay)498         private void postDotSwap(long delay) {
499             removeDotSwapCallbacks();
500             postDelayed(dotSwapperRunnable, delay);
501             isDotSwapPending = true;
502         }
503 
removeDotSwapCallbacks()504         private void removeDotSwapCallbacks() {
505             removeCallbacks(dotSwapperRunnable);
506             isDotSwapPending = false;
507         }
508 
swapToDotWhenAppearFinished()509         void swapToDotWhenAppearFinished() {
510             removeDotSwapCallbacks();
511             if (textAnimator != null) {
512                 long remainingDuration =
513                         textAnimator.getDuration() - textAnimator.getCurrentPlayTime();
514                 postDotSwap(remainingDuration + TEXT_REST_DURATION_AFTER_APPEAR);
515             } else {
516                 performSwap();
517             }
518         }
519 
performSwap()520         private void performSwap() {
521             startTextDisappearAnimation(0);
522             startDotAppearAnimation(
523                     DISAPPEAR_DURATION - DOT_APPEAR_TEXT_DISAPPEAR_OVERLAP_DURATION);
524         }
525 
startWidthDisappearAnimation(long widthDelay)526         private void startWidthDisappearAnimation(long widthDelay) {
527             cancelAnimator(widthAnimator);
528             widthAnimator = ValueAnimator.ofFloat(currentWidthFactor, 0.0f);
529             widthAnimator.addUpdateListener(mWidthUpdater);
530             widthAnimator.addListener(widthFinishListener);
531             widthAnimator.addListener(removeEndListener);
532             widthAnimator.setDuration((long) (DISAPPEAR_DURATION * currentWidthFactor));
533             widthAnimator.setStartDelay(widthDelay);
534             widthAnimator.start();
535             widthAnimationIsGrowing = false;
536         }
537 
startTextDisappearAnimation(long startDelay)538         private void startTextDisappearAnimation(long startDelay) {
539             cancelAnimator(textAnimator);
540             textAnimator = ValueAnimator.ofFloat(currentTextSizeFactor, 0.0f);
541             textAnimator.addUpdateListener(mTextSizeUpdater);
542             textAnimator.addListener(textFinishListener);
543             textAnimator.setInterpolator(mDisappearInterpolator);
544             textAnimator.setDuration((long) (DISAPPEAR_DURATION * currentTextSizeFactor));
545             textAnimator.setStartDelay(startDelay);
546             textAnimator.start();
547             textAnimationIsGrowing = false;
548         }
549 
startDotDisappearAnimation(long startDelay)550         private void startDotDisappearAnimation(long startDelay) {
551             cancelAnimator(dotAnimator);
552             ValueAnimator animator = ValueAnimator.ofFloat(currentDotSizeFactor, 0.0f);
553             animator.addUpdateListener(mDotSizeUpdater);
554             animator.addListener(dotFinishListener);
555             animator.setInterpolator(mDisappearInterpolator);
556             long duration = (long) (DISAPPEAR_DURATION * Math.min(currentDotSizeFactor, 1.0f));
557             animator.setDuration(duration);
558             animator.setStartDelay(startDelay);
559             animator.start();
560             dotAnimator = animator;
561             dotAnimationIsGrowing = false;
562         }
563 
startWidthAppearAnimation()564         private void startWidthAppearAnimation() {
565             cancelAnimator(widthAnimator);
566             widthAnimator = ValueAnimator.ofFloat(currentWidthFactor, 1.0f);
567             widthAnimator.addUpdateListener(mWidthUpdater);
568             widthAnimator.addListener(widthFinishListener);
569             widthAnimator.setDuration((long) (APPEAR_DURATION * (1f - currentWidthFactor)));
570             widthAnimator.start();
571             widthAnimationIsGrowing = true;
572         }
573 
startTextAppearAnimation()574         private void startTextAppearAnimation() {
575             cancelAnimator(textAnimator);
576             textAnimator = ValueAnimator.ofFloat(currentTextSizeFactor, 1.0f);
577             textAnimator.addUpdateListener(mTextSizeUpdater);
578             textAnimator.addListener(textFinishListener);
579             textAnimator.setInterpolator(mAppearInterpolator);
580             textAnimator.setDuration((long) (APPEAR_DURATION * (1f - currentTextSizeFactor)));
581             textAnimator.start();
582             textAnimationIsGrowing = true;
583 
584             // handle translation
585             if (textTranslateAnimator == null) {
586                 textTranslateAnimator = ValueAnimator.ofFloat(1.0f, 0.0f);
587                 textTranslateAnimator.addUpdateListener(mTextTranslationUpdater);
588                 textTranslateAnimator.addListener(textTranslateFinishListener);
589                 textTranslateAnimator.setInterpolator(mAppearInterpolator);
590                 textTranslateAnimator.setDuration(APPEAR_DURATION);
591                 textTranslateAnimator.start();
592             }
593         }
594 
startDotAppearAnimation(long delay)595         private void startDotAppearAnimation(long delay) {
596             cancelAnimator(dotAnimator);
597             if (!mShowPassword) {
598                 // We perform an overshoot animation
599                 ValueAnimator overShootAnimator = ValueAnimator.ofFloat(currentDotSizeFactor,
600                         DOT_OVERSHOOT_FACTOR);
601                 overShootAnimator.addUpdateListener(mDotSizeUpdater);
602                 overShootAnimator.setInterpolator(mAppearInterpolator);
603                 long overShootDuration =
604                         (long) (DOT_APPEAR_DURATION_OVERSHOOT * OVERSHOOT_TIME_POSITION);
605                 overShootAnimator.setDuration(overShootDuration);
606                 ValueAnimator settleBackAnimator = ValueAnimator.ofFloat(DOT_OVERSHOOT_FACTOR,
607                         1.0f);
608                 settleBackAnimator.addUpdateListener(mDotSizeUpdater);
609                 settleBackAnimator.setDuration(DOT_APPEAR_DURATION_OVERSHOOT - overShootDuration);
610                 settleBackAnimator.addListener(dotFinishListener);
611                 AnimatorSet animatorSet = new AnimatorSet();
612                 animatorSet.playSequentially(overShootAnimator, settleBackAnimator);
613                 animatorSet.setStartDelay(delay);
614                 animatorSet.start();
615                 dotAnimator = animatorSet;
616             } else {
617                 ValueAnimator growAnimator = ValueAnimator.ofFloat(currentDotSizeFactor, 1.0f);
618                 growAnimator.addUpdateListener(mDotSizeUpdater);
619                 growAnimator.setDuration((long) (APPEAR_DURATION * (1.0f - currentDotSizeFactor)));
620                 growAnimator.addListener(dotFinishListener);
621                 growAnimator.setStartDelay(delay);
622                 growAnimator.start();
623                 dotAnimator = growAnimator;
624             }
625             dotAnimationIsGrowing = true;
626         }
627 
cancelAnimator(Animator animator)628         private void cancelAnimator(Animator animator) {
629             if (animator != null) {
630                 animator.cancel();
631             }
632         }
633 
634         /**
635          * Draw this char to the canvas.
636          *
637          * @return The width this character contributes, including padding.
638          */
draw(Canvas canvas, float currentDrawPosition, int charHeight, float yPosition, float charLength)639         public float draw(Canvas canvas, float currentDrawPosition, int charHeight, float yPosition,
640                 float charLength) {
641             boolean textVisible = currentTextSizeFactor > 0;
642             boolean dotVisible = currentDotSizeFactor > 0;
643             float charWidth = charLength * currentWidthFactor;
644             if (textVisible) {
645                 float currYPosition = yPosition + charHeight / 2.0f * currentTextSizeFactor
646                         + charHeight * currentTextTranslationY * 0.8f;
647                 canvas.save();
648                 float centerX = currentDrawPosition + charWidth / 2;
649                 canvas.translate(centerX, currYPosition);
650                 canvas.scale(currentTextSizeFactor, currentTextSizeFactor);
651                 canvas.drawText(Character.toString(whichChar), 0, 0, mDrawPaint);
652                 canvas.restore();
653             }
654             if (dotVisible) {
655                 canvas.save();
656                 float centerX = currentDrawPosition + charWidth / 2;
657                 canvas.translate(centerX, yPosition);
658                 canvas.drawCircle(0, 0, mDotSize / 2 * currentDotSizeFactor, mDrawPaint);
659                 canvas.restore();
660             }
661             return charWidth + mCharPadding * currentWidthFactor;
662         }
663 
isCharVisibleForA11y()664         public boolean isCharVisibleForA11y() {
665             // The text has size 0 when it is first added, but we want to count it as visible if
666             // it will become visible presently. Count text as visible if an animator
667             // is configured to make it grow.
668             boolean textIsGrowing = textAnimator != null && textAnimationIsGrowing;
669             return (currentTextSizeFactor > 0) || textIsGrowing;
670         }
671     }
672 }
673