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