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