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