• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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             float maxRight = getWidth() - getPaddingRight() - totalDrawingWidth;
168             float center = getWidth() / 2f - totalDrawingWidth / 2f;
169             currentDrawPosition = center > 0 ? center : maxRight;
170         }
171         int length = mTextChars.size();
172         Rect bounds = getCharBounds();
173         int charHeight = (bounds.bottom - bounds.top);
174         float yPosition =
175                 (getHeight() - getPaddingBottom() - getPaddingTop()) / 2 + getPaddingTop();
176         canvas.clipRect(getPaddingLeft(), getPaddingTop(),
177                 getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
178         float charLength = bounds.right - bounds.left;
179         for (int i = 0; i < length; i++) {
180             CharState charState = mTextChars.get(i);
181             float charWidth = charState.draw(canvas, currentDrawPosition, charHeight, yPosition,
182                     charLength);
183             currentDrawPosition += charWidth;
184         }
185     }
186 
187     @Override
hasOverlappingRendering()188     public boolean hasOverlappingRendering() {
189         return false;
190     }
191 
getCharBounds()192     private Rect getCharBounds() {
193         float textHeight = mTextHeightRaw * getResources().getDisplayMetrics().scaledDensity;
194         mDrawPaint.setTextSize(textHeight);
195         Rect bounds = new Rect();
196         mDrawPaint.getTextBounds("0", 0, 1, bounds);
197         return bounds;
198     }
199 
getDrawingWidth()200     private float getDrawingWidth() {
201         int width = 0;
202         int length = mTextChars.size();
203         Rect bounds = getCharBounds();
204         int charLength = bounds.right - bounds.left;
205         for (int i = 0; i < length; i++) {
206             CharState charState = mTextChars.get(i);
207             if (i != 0) {
208                 width += mCharPadding * charState.currentWidthFactor;
209             }
210             width += charLength * charState.currentWidthFactor;
211         }
212         return width;
213     }
214 
215 
append(char c)216     public void append(char c) {
217         int visibleChars = mTextChars.size();
218         CharSequence textbefore = getTransformedText();
219         mText = mText + c;
220         int newLength = mText.length();
221         CharState charState;
222         if (newLength > visibleChars) {
223             charState = obtainCharState(c);
224             mTextChars.add(charState);
225         } else {
226             charState = mTextChars.get(newLength - 1);
227             charState.whichChar = c;
228         }
229         charState.startAppearAnimation();
230 
231         // ensure that the previous element is being swapped
232         if (newLength > 1) {
233             CharState previousState = mTextChars.get(newLength - 2);
234             if (previousState.isDotSwapPending) {
235                 previousState.swapToDotWhenAppearFinished();
236             }
237         }
238         userActivity();
239         sendAccessibilityEventTypeViewTextChanged(textbefore, textbefore.length(), 0, 1);
240     }
241 
setUserActivityListener(UserActivityListener userActivitiListener)242     public void setUserActivityListener(UserActivityListener userActivitiListener) {
243         mUserActivityListener = userActivitiListener;
244     }
245 
userActivity()246     private void userActivity() {
247         mPM.userActivity(SystemClock.uptimeMillis(), false);
248         if (mUserActivityListener != null) {
249             mUserActivityListener.onUserActivity();
250         }
251     }
252 
deleteLastChar()253     public void deleteLastChar() {
254         int length = mText.length();
255         CharSequence textbefore = getTransformedText();
256         if (length > 0) {
257             mText = mText.substring(0, length - 1);
258             CharState charState = mTextChars.get(length - 1);
259             charState.startRemoveAnimation(0, 0);
260             sendAccessibilityEventTypeViewTextChanged(textbefore, textbefore.length() - 1, 1, 0);
261         }
262         userActivity();
263     }
264 
getText()265     public String getText() {
266         return mText;
267     }
268 
getTransformedText()269     private CharSequence getTransformedText() {
270         int textLength = mTextChars.size();
271         StringBuilder stringBuilder = new StringBuilder(textLength);
272         for (int i = 0; i < textLength; i++) {
273             CharState charState = mTextChars.get(i);
274             // If the dot is disappearing, the character is disappearing entirely. Consider
275             // it gone.
276             if (charState.dotAnimator != null && !charState.dotAnimationIsGrowing) {
277                 continue;
278             }
279             stringBuilder.append(charState.isCharVisibleForA11y() ? charState.whichChar : DOT);
280         }
281         return stringBuilder;
282     }
283 
obtainCharState(char c)284     private CharState obtainCharState(char c) {
285         CharState charState;
286         if(mCharPool.isEmpty()) {
287             charState = new CharState();
288         } else {
289             charState = mCharPool.pop();
290             charState.reset();
291         }
292         charState.whichChar = c;
293         return charState;
294     }
295 
reset(boolean animated, boolean announce)296     public void reset(boolean animated, boolean announce) {
297         CharSequence textbefore = getTransformedText();
298         mText = "";
299         int length = mTextChars.size();
300         int middleIndex = (length - 1) / 2;
301         long delayPerElement = RESET_DELAY_PER_ELEMENT;
302         for (int i = 0; i < length; i++) {
303             CharState charState = mTextChars.get(i);
304             if (animated) {
305                 int delayIndex;
306                 if (i <= middleIndex) {
307                     delayIndex = i * 2;
308                 } else {
309                     int distToMiddle = i - middleIndex;
310                     delayIndex = (length - 1) - (distToMiddle - 1) * 2;
311                 }
312                 long startDelay = delayIndex * delayPerElement;
313                 startDelay = Math.min(startDelay, RESET_MAX_DELAY);
314                 long maxDelay = delayPerElement * (length - 1);
315                 maxDelay = Math.min(maxDelay, RESET_MAX_DELAY) + DISAPPEAR_DURATION;
316                 charState.startRemoveAnimation(startDelay, maxDelay);
317                 charState.removeDotSwapCallbacks();
318             } else {
319                 mCharPool.push(charState);
320             }
321         }
322         if (!animated) {
323             mTextChars.clear();
324         }
325         if (announce) {
326             sendAccessibilityEventTypeViewTextChanged(textbefore, 0, textbefore.length(), 0);
327         }
328     }
329 
sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText, int fromIndex, int removedCount, int addedCount)330     void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText, int fromIndex,
331                                                    int removedCount, int addedCount) {
332         if (AccessibilityManager.getInstance(mContext).isEnabled() &&
333                 (isFocused() || isSelected() && isShown())) {
334             AccessibilityEvent event =
335                     AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
336             event.setFromIndex(fromIndex);
337             event.setRemovedCount(removedCount);
338             event.setAddedCount(addedCount);
339             event.setBeforeText(beforeText);
340             CharSequence transformedText = getTransformedText();
341             if (!TextUtils.isEmpty(transformedText)) {
342                 event.getText().add(transformedText);
343             }
344             event.setPassword(true);
345             sendAccessibilityEventUnchecked(event);
346         }
347     }
348 
349     @Override
onInitializeAccessibilityEvent(AccessibilityEvent event)350     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
351         super.onInitializeAccessibilityEvent(event);
352 
353         event.setClassName(EditText.class.getName());
354         event.setPassword(true);
355     }
356 
357     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)358     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
359         super.onInitializeAccessibilityNodeInfo(info);
360 
361         info.setClassName(EditText.class.getName());
362         info.setPassword(true);
363         info.setText(getTransformedText());
364 
365         info.setEditable(true);
366 
367         info.setInputType(InputType.TYPE_NUMBER_VARIATION_PASSWORD);
368     }
369 
370     private class CharState {
371         char whichChar;
372         ValueAnimator textAnimator;
373         boolean textAnimationIsGrowing;
374         Animator dotAnimator;
375         boolean dotAnimationIsGrowing;
376         ValueAnimator widthAnimator;
377         boolean widthAnimationIsGrowing;
378         float currentTextSizeFactor;
379         float currentDotSizeFactor;
380         float currentWidthFactor;
381         boolean isDotSwapPending;
382         float currentTextTranslationY = 1.0f;
383         ValueAnimator textTranslateAnimator;
384 
385         Animator.AnimatorListener removeEndListener = new AnimatorListenerAdapter() {
386             private boolean mCancelled;
387             @Override
388             public void onAnimationCancel(Animator animation) {
389                 mCancelled = true;
390             }
391 
392             @Override
393             public void onAnimationEnd(Animator animation) {
394                 if (!mCancelled) {
395                     mTextChars.remove(CharState.this);
396                     mCharPool.push(CharState.this);
397                     reset();
398                     cancelAnimator(textTranslateAnimator);
399                     textTranslateAnimator = null;
400                 }
401             }
402 
403             @Override
404             public void onAnimationStart(Animator animation) {
405                 mCancelled = false;
406             }
407         };
408 
409         Animator.AnimatorListener dotFinishListener = new AnimatorListenerAdapter() {
410             @Override
411             public void onAnimationEnd(Animator animation) {
412                 dotAnimator = null;
413             }
414         };
415 
416         Animator.AnimatorListener textFinishListener = new AnimatorListenerAdapter() {
417             @Override
418             public void onAnimationEnd(Animator animation) {
419                 textAnimator = null;
420             }
421         };
422 
423         Animator.AnimatorListener textTranslateFinishListener = new AnimatorListenerAdapter() {
424             @Override
425             public void onAnimationEnd(Animator animation) {
426                 textTranslateAnimator = null;
427             }
428         };
429 
430         Animator.AnimatorListener widthFinishListener = new AnimatorListenerAdapter() {
431             @Override
432             public void onAnimationEnd(Animator animation) {
433                 widthAnimator = null;
434             }
435         };
436 
437         private ValueAnimator.AnimatorUpdateListener dotSizeUpdater
438                 = new ValueAnimator.AnimatorUpdateListener() {
439             @Override
440             public void onAnimationUpdate(ValueAnimator animation) {
441                 currentDotSizeFactor = (float) animation.getAnimatedValue();
442                 invalidate();
443             }
444         };
445 
446         private ValueAnimator.AnimatorUpdateListener textSizeUpdater
447                 = new ValueAnimator.AnimatorUpdateListener() {
448             @Override
449             public void onAnimationUpdate(ValueAnimator animation) {
450                 boolean textVisibleBefore = isCharVisibleForA11y();
451                 float beforeTextSizeFactor = currentTextSizeFactor;
452                 currentTextSizeFactor = (float) animation.getAnimatedValue();
453                 if (textVisibleBefore != isCharVisibleForA11y()) {
454                     currentTextSizeFactor = beforeTextSizeFactor;
455                     CharSequence beforeText = getTransformedText();
456                     currentTextSizeFactor = (float) animation.getAnimatedValue();
457                     int indexOfThisChar = mTextChars.indexOf(CharState.this);
458                     if (indexOfThisChar >= 0) {
459                         sendAccessibilityEventTypeViewTextChanged(
460                                 beforeText, indexOfThisChar, 1, 1);
461                     }
462                 }
463                 invalidate();
464             }
465         };
466 
467         private ValueAnimator.AnimatorUpdateListener textTranslationUpdater
468                 = new ValueAnimator.AnimatorUpdateListener() {
469             @Override
470             public void onAnimationUpdate(ValueAnimator animation) {
471                 currentTextTranslationY = (float) animation.getAnimatedValue();
472                 invalidate();
473             }
474         };
475 
476         private ValueAnimator.AnimatorUpdateListener widthUpdater
477                 = new ValueAnimator.AnimatorUpdateListener() {
478             @Override
479             public void onAnimationUpdate(ValueAnimator animation) {
480                 currentWidthFactor = (float) animation.getAnimatedValue();
481                 invalidate();
482             }
483         };
484 
485         private Runnable dotSwapperRunnable = new Runnable() {
486             @Override
487             public void run() {
488                 performSwap();
489                 isDotSwapPending = false;
490             }
491         };
492 
reset()493         void reset() {
494             whichChar = 0;
495             currentTextSizeFactor = 0.0f;
496             currentDotSizeFactor = 0.0f;
497             currentWidthFactor = 0.0f;
498             cancelAnimator(textAnimator);
499             textAnimator = null;
500             cancelAnimator(dotAnimator);
501             dotAnimator = null;
502             cancelAnimator(widthAnimator);
503             widthAnimator = null;
504             currentTextTranslationY = 1.0f;
505             removeDotSwapCallbacks();
506         }
507 
startRemoveAnimation(long startDelay, long widthDelay)508         void startRemoveAnimation(long startDelay, long widthDelay) {
509             boolean dotNeedsAnimation = (currentDotSizeFactor > 0.0f && dotAnimator == null)
510                     || (dotAnimator != null && dotAnimationIsGrowing);
511             boolean textNeedsAnimation = (currentTextSizeFactor > 0.0f && textAnimator == null)
512                     || (textAnimator != null && textAnimationIsGrowing);
513             boolean widthNeedsAnimation = (currentWidthFactor > 0.0f && widthAnimator == null)
514                     || (widthAnimator != null && widthAnimationIsGrowing);
515             if (dotNeedsAnimation) {
516                 startDotDisappearAnimation(startDelay);
517             }
518             if (textNeedsAnimation) {
519                 startTextDisappearAnimation(startDelay);
520             }
521             if (widthNeedsAnimation) {
522                 startWidthDisappearAnimation(widthDelay);
523             }
524         }
525 
startAppearAnimation()526         void startAppearAnimation() {
527             boolean dotNeedsAnimation = !mShowPassword
528                     && (dotAnimator == null || !dotAnimationIsGrowing);
529             boolean textNeedsAnimation = mShowPassword
530                     && (textAnimator == null || !textAnimationIsGrowing);
531             boolean widthNeedsAnimation = (widthAnimator == null || !widthAnimationIsGrowing);
532             if (dotNeedsAnimation) {
533                 startDotAppearAnimation(0);
534             }
535             if (textNeedsAnimation) {
536                 startTextAppearAnimation();
537             }
538             if (widthNeedsAnimation) {
539                 startWidthAppearAnimation();
540             }
541             if (mShowPassword) {
542                 postDotSwap(TEXT_VISIBILITY_DURATION);
543             }
544         }
545 
546         /**
547          * Posts a runnable which ensures that the text will be replaced by a dot after {@link
548          * com.android.keyguard.PasswordTextView#TEXT_VISIBILITY_DURATION}.
549          */
postDotSwap(long delay)550         private void postDotSwap(long delay) {
551             removeDotSwapCallbacks();
552             postDelayed(dotSwapperRunnable, delay);
553             isDotSwapPending = true;
554         }
555 
removeDotSwapCallbacks()556         private void removeDotSwapCallbacks() {
557             removeCallbacks(dotSwapperRunnable);
558             isDotSwapPending = false;
559         }
560 
swapToDotWhenAppearFinished()561         void swapToDotWhenAppearFinished() {
562             removeDotSwapCallbacks();
563             if (textAnimator != null) {
564                 long remainingDuration = textAnimator.getDuration()
565                         - textAnimator.getCurrentPlayTime();
566                 postDotSwap(remainingDuration + TEXT_REST_DURATION_AFTER_APPEAR);
567             } else {
568                 performSwap();
569             }
570         }
571 
performSwap()572         private void performSwap() {
573             startTextDisappearAnimation(0);
574             startDotAppearAnimation(DISAPPEAR_DURATION
575                     - DOT_APPEAR_TEXT_DISAPPEAR_OVERLAP_DURATION);
576         }
577 
startWidthDisappearAnimation(long widthDelay)578         private void startWidthDisappearAnimation(long widthDelay) {
579             cancelAnimator(widthAnimator);
580             widthAnimator = ValueAnimator.ofFloat(currentWidthFactor, 0.0f);
581             widthAnimator.addUpdateListener(widthUpdater);
582             widthAnimator.addListener(widthFinishListener);
583             widthAnimator.addListener(removeEndListener);
584             widthAnimator.setDuration((long) (DISAPPEAR_DURATION * currentWidthFactor));
585             widthAnimator.setStartDelay(widthDelay);
586             widthAnimator.start();
587             widthAnimationIsGrowing = false;
588         }
589 
startTextDisappearAnimation(long startDelay)590         private void startTextDisappearAnimation(long startDelay) {
591             cancelAnimator(textAnimator);
592             textAnimator = ValueAnimator.ofFloat(currentTextSizeFactor, 0.0f);
593             textAnimator.addUpdateListener(textSizeUpdater);
594             textAnimator.addListener(textFinishListener);
595             textAnimator.setInterpolator(mDisappearInterpolator);
596             textAnimator.setDuration((long) (DISAPPEAR_DURATION * currentTextSizeFactor));
597             textAnimator.setStartDelay(startDelay);
598             textAnimator.start();
599             textAnimationIsGrowing = false;
600         }
601 
startDotDisappearAnimation(long startDelay)602         private void startDotDisappearAnimation(long startDelay) {
603             cancelAnimator(dotAnimator);
604             ValueAnimator animator = ValueAnimator.ofFloat(currentDotSizeFactor, 0.0f);
605             animator.addUpdateListener(dotSizeUpdater);
606             animator.addListener(dotFinishListener);
607             animator.setInterpolator(mDisappearInterpolator);
608             long duration = (long) (DISAPPEAR_DURATION * Math.min(currentDotSizeFactor, 1.0f));
609             animator.setDuration(duration);
610             animator.setStartDelay(startDelay);
611             animator.start();
612             dotAnimator = animator;
613             dotAnimationIsGrowing = false;
614         }
615 
startWidthAppearAnimation()616         private void startWidthAppearAnimation() {
617             cancelAnimator(widthAnimator);
618             widthAnimator = ValueAnimator.ofFloat(currentWidthFactor, 1.0f);
619             widthAnimator.addUpdateListener(widthUpdater);
620             widthAnimator.addListener(widthFinishListener);
621             widthAnimator.setDuration((long) (APPEAR_DURATION * (1f - currentWidthFactor)));
622             widthAnimator.start();
623             widthAnimationIsGrowing = true;
624         }
625 
startTextAppearAnimation()626         private void startTextAppearAnimation() {
627             cancelAnimator(textAnimator);
628             textAnimator = ValueAnimator.ofFloat(currentTextSizeFactor, 1.0f);
629             textAnimator.addUpdateListener(textSizeUpdater);
630             textAnimator.addListener(textFinishListener);
631             textAnimator.setInterpolator(mAppearInterpolator);
632             textAnimator.setDuration((long) (APPEAR_DURATION * (1f - currentTextSizeFactor)));
633             textAnimator.start();
634             textAnimationIsGrowing = true;
635 
636             // handle translation
637             if (textTranslateAnimator == null) {
638                 textTranslateAnimator = ValueAnimator.ofFloat(1.0f, 0.0f);
639                 textTranslateAnimator.addUpdateListener(textTranslationUpdater);
640                 textTranslateAnimator.addListener(textTranslateFinishListener);
641                 textTranslateAnimator.setInterpolator(mAppearInterpolator);
642                 textTranslateAnimator.setDuration(APPEAR_DURATION);
643                 textTranslateAnimator.start();
644             }
645         }
646 
startDotAppearAnimation(long delay)647         private void startDotAppearAnimation(long delay) {
648             cancelAnimator(dotAnimator);
649             if (!mShowPassword) {
650                 // We perform an overshoot animation
651                 ValueAnimator overShootAnimator = ValueAnimator.ofFloat(currentDotSizeFactor,
652                         DOT_OVERSHOOT_FACTOR);
653                 overShootAnimator.addUpdateListener(dotSizeUpdater);
654                 overShootAnimator.setInterpolator(mAppearInterpolator);
655                 long overShootDuration = (long) (DOT_APPEAR_DURATION_OVERSHOOT
656                         * OVERSHOOT_TIME_POSITION);
657                 overShootAnimator.setDuration(overShootDuration);
658                 ValueAnimator settleBackAnimator = ValueAnimator.ofFloat(DOT_OVERSHOOT_FACTOR,
659                         1.0f);
660                 settleBackAnimator.addUpdateListener(dotSizeUpdater);
661                 settleBackAnimator.setDuration(DOT_APPEAR_DURATION_OVERSHOOT - overShootDuration);
662                 settleBackAnimator.addListener(dotFinishListener);
663                 AnimatorSet animatorSet = new AnimatorSet();
664                 animatorSet.playSequentially(overShootAnimator, settleBackAnimator);
665                 animatorSet.setStartDelay(delay);
666                 animatorSet.start();
667                 dotAnimator = animatorSet;
668             } else {
669                 ValueAnimator growAnimator = ValueAnimator.ofFloat(currentDotSizeFactor, 1.0f);
670                 growAnimator.addUpdateListener(dotSizeUpdater);
671                 growAnimator.setDuration((long) (APPEAR_DURATION * (1.0f - currentDotSizeFactor)));
672                 growAnimator.addListener(dotFinishListener);
673                 growAnimator.setStartDelay(delay);
674                 growAnimator.start();
675                 dotAnimator = growAnimator;
676             }
677             dotAnimationIsGrowing = true;
678         }
679 
cancelAnimator(Animator animator)680         private void cancelAnimator(Animator animator) {
681             if (animator != null) {
682                 animator.cancel();
683             }
684         }
685 
686         /**
687          * Draw this char to the canvas.
688          *
689          * @return The width this character contributes, including padding.
690          */
draw(Canvas canvas, float currentDrawPosition, int charHeight, float yPosition, float charLength)691         public float draw(Canvas canvas, float currentDrawPosition, int charHeight, float yPosition,
692                 float charLength) {
693             boolean textVisible = currentTextSizeFactor > 0;
694             boolean dotVisible = currentDotSizeFactor > 0;
695             float charWidth = charLength * currentWidthFactor;
696             if (textVisible) {
697                 float currYPosition = yPosition + charHeight / 2.0f * currentTextSizeFactor
698                         + charHeight * currentTextTranslationY * 0.8f;
699                 canvas.save();
700                 float centerX = currentDrawPosition + charWidth / 2;
701                 canvas.translate(centerX, currYPosition);
702                 canvas.scale(currentTextSizeFactor, currentTextSizeFactor);
703                 canvas.drawText(Character.toString(whichChar), 0, 0, mDrawPaint);
704                 canvas.restore();
705             }
706             if (dotVisible) {
707                 canvas.save();
708                 float centerX = currentDrawPosition + charWidth / 2;
709                 canvas.translate(centerX, yPosition);
710                 canvas.drawCircle(0, 0, mDotSize / 2 * currentDotSizeFactor, mDrawPaint);
711                 canvas.restore();
712             }
713             return charWidth + mCharPadding * currentWidthFactor;
714         }
715 
isCharVisibleForA11y()716         public boolean isCharVisibleForA11y() {
717             // The text has size 0 when it is first added, but we want to count it as visible if
718             // it will become visible presently. Count text as visible if an animator
719             // is configured to make it grow.
720             boolean textIsGrowing = textAnimator != null && textAnimationIsGrowing;
721             return (currentTextSizeFactor > 0) || textIsGrowing;
722         }
723     }
724 }
725