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