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