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 com.android.systemui.R;
46 
47 import java.util.ArrayList;
48 import 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  */
54 public 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 
84     private static char DOT = '\u2022';
85 
86     /**
87      * The raw text size, will be multiplied by the scaled density when drawn
88      */
89     private final int mTextHeightRaw;
90     private final int mGravity;
91     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;
102     private UserActivityListener mUserActivityListener;
103 
104     public interface UserActivityListener {
onUserActivity()105         void onUserActivity();
106     }
107 
PasswordTextView(Context context)108     public PasswordTextView(Context context) {
109         this(context, null);
110     }
111 
PasswordTextView(Context context, AttributeSet attrs)112     public PasswordTextView(Context context, AttributeSet attrs) {
113         this(context, attrs, 0);
114     }
115 
PasswordTextView(Context context, AttributeSet attrs, int defStyleAttr)116     public PasswordTextView(Context context, AttributeSet attrs, int defStyleAttr) {
117         this(context, attrs, defStyleAttr, 0);
118     }
119 
PasswordTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)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);
128             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));
134             int textColor = a.getColor(R.styleable.PasswordTextView_android_textColor, Color.WHITE);
135             mDrawPaint.setColor(textColor);
136         } finally {
137             a.recycle();
138         }
139         mDrawPaint.setFlags(Paint.SUBPIXEL_TEXT_FLAG | Paint.ANTI_ALIAS_FLAG);
140         mDrawPaint.setTextAlign(Paint.Align.CENTER);
141         mDrawPaint.setTypeface(Typeface.create(
142                 context.getString(com.android.internal.R.string.config_headlineFontFamily),
143                 0));
144         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
onDraw(Canvas canvas)156     protected void onDraw(Canvas canvas) {
157         float totalDrawingWidth = getDrawingWidth();
158         float currentDrawPosition;
159         if ((mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.LEFT) {
160             if ((mGravity & Gravity.RELATIVE_LAYOUT_DIRECTION) != 0
161                     && getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
162                 currentDrawPosition = getWidth() - getPaddingRight() - totalDrawingWidth;
163             } else {
164                 currentDrawPosition = getPaddingLeft();
165             }
166         } else {
167             currentDrawPosition = getWidth() / 2 - totalDrawingWidth / 2;
168         }
169         int length = mTextChars.size();
170         Rect bounds = getCharBounds();
171         int charHeight = (bounds.bottom - bounds.top);
172         float yPosition =
173                 (getHeight() - getPaddingBottom() - getPaddingTop()) / 2 + getPaddingTop();
174         canvas.clipRect(getPaddingLeft(), getPaddingTop(),
175                 getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
176         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 
185     @Override
hasOverlappingRendering()186     public boolean hasOverlappingRendering() {
187         return false;
188     }
189 
getCharBounds()190     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 
getDrawingWidth()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 
append(char c)214     public void append(char c) {
215         int visibleChars = mTextChars.size();
216         CharSequence textbefore = getTransformedText();
217         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();
237         sendAccessibilityEventTypeViewTextChanged(textbefore, textbefore.length(), 0, 1);
238     }
239 
setUserActivityListener(UserActivityListener userActivitiListener)240     public void setUserActivityListener(UserActivityListener userActivitiListener) {
241         mUserActivityListener = userActivitiListener;
242     }
243 
userActivity()244     private void userActivity() {
245         mPM.userActivity(SystemClock.uptimeMillis(), false);
246         if (mUserActivityListener != null) {
247             mUserActivityListener.onUserActivity();
248         }
249     }
250 
deleteLastChar()251     public void deleteLastChar() {
252         int length = mText.length();
253         CharSequence textbefore = getTransformedText();
254         if (length > 0) {
255             mText = mText.substring(0, length - 1);
256             CharState charState = mTextChars.get(length - 1);
257             charState.startRemoveAnimation(0, 0);
258             sendAccessibilityEventTypeViewTextChanged(textbefore, textbefore.length() - 1, 1, 0);
259         }
260         userActivity();
261     }
262 
getText()263     public String getText() {
264         return mText;
265     }
266 
getTransformedText()267     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 
obtainCharState(char c)282     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 
reset(boolean animated, boolean announce)294     public void reset(boolean animated, boolean announce) {
295         CharSequence textbefore = getTransformedText();
296         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         }
323         if (announce) {
324             sendAccessibilityEventTypeViewTextChanged(textbefore, 0, textbefore.length(), 0);
325         }
326     }
327 
sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText, int fromIndex, int removedCount, int addedCount)328     void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText, int fromIndex,
329                                                    int removedCount, int addedCount) {
330         if (AccessibilityManager.getInstance(mContext).isEnabled() &&
331                 (isFocused() || isSelected() && isShown())) {
332             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);
338             CharSequence transformedText = getTransformedText();
339             if (!TextUtils.isEmpty(transformedText)) {
340                 event.getText().add(transformedText);
341             }
342             event.setPassword(true);
343             sendAccessibilityEventUnchecked(event);
344         }
345     }
346 
347     @Override
onInitializeAccessibilityEvent(AccessibilityEvent event)348     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
349         super.onInitializeAccessibilityEvent(event);
350 
351         event.setClassName(EditText.class.getName());
352         event.setPassword(true);
353     }
354 
355     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)356     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
357         super.onInitializeAccessibilityNodeInfo(info);
358 
359         info.setClassName(EditText.class.getName());
360         info.setPassword(true);
361         info.setText(getTransformedText());
362 
363         info.setEditable(true);
364 
365         info.setInputType(InputType.TYPE_NUMBER_VARIATION_PASSWORD);
366     }
367 
368     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) {
448                 boolean textVisibleBefore = isCharVisibleForA11y();
449                 float beforeTextSizeFactor = currentTextSizeFactor;
450                 currentTextSizeFactor = (float) animation.getAnimatedValue();
451                 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                 }
461                 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 
reset()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 
startRemoveAnimation(long startDelay, long widthDelay)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 
startAppearAnimation()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          */
postDotSwap(long delay)548         private void postDotSwap(long delay) {
549             removeDotSwapCallbacks();
550             postDelayed(dotSwapperRunnable, delay);
551             isDotSwapPending = true;
552         }
553 
removeDotSwapCallbacks()554         private void removeDotSwapCallbacks() {
555             removeCallbacks(dotSwapperRunnable);
556             isDotSwapPending = false;
557         }
558 
swapToDotWhenAppearFinished()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 
performSwap()570         private void performSwap() {
571             startTextDisappearAnimation(0);
572             startDotAppearAnimation(DISAPPEAR_DURATION
573                     - DOT_APPEAR_TEXT_DISAPPEAR_OVERLAP_DURATION);
574         }
575 
startWidthDisappearAnimation(long widthDelay)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 
startTextDisappearAnimation(long startDelay)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 
startDotDisappearAnimation(long startDelay)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 
startWidthAppearAnimation()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 
startTextAppearAnimation()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 
startDotAppearAnimation(long delay)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 
cancelAnimator(Animator animator)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          */
draw(Canvas canvas, float currentDrawPosition, int charHeight, float yPosition, float charLength)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         }
713 
isCharVisibleForA11y()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         }
721     }
722 }
723