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