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
17package com.android.keyguard;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.AnimatorSet;
22import android.animation.ValueAnimator;
23import android.content.Context;
24import android.content.res.TypedArray;
25import android.graphics.Canvas;
26import android.graphics.Paint;
27import android.graphics.Rect;
28import android.graphics.Typeface;
29import android.os.PowerManager;
30import android.os.SystemClock;
31import android.os.UserHandle;
32import android.provider.Settings;
33import android.text.InputType;
34import android.text.TextUtils;
35import android.util.AttributeSet;
36import android.view.Gravity;
37import android.view.View;
38import android.view.accessibility.AccessibilityEvent;
39import android.view.accessibility.AccessibilityManager;
40import android.view.accessibility.AccessibilityNodeInfo;
41import android.view.animation.AnimationUtils;
42import android.view.animation.Interpolator;
43
44import java.util.ArrayList;
45import 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 */
51public 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 {
100        void onUserActivity();
101    }
102
103    public PasswordTextView(Context context) {
104        this(context, null);
105    }
106
107    public PasswordTextView(Context context, AttributeSet attrs) {
108        this(context, attrs, 0);
109    }
110
111    public PasswordTextView(Context context, AttributeSet attrs, int defStyleAttr) {
112        this(context, attrs, defStyleAttr, 0);
113    }
114
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
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
178    public boolean hasOverlappingRendering() {
179        return false;
180    }
181
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
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
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
232    public void setUserActivityListener(UserActivityListener userActivitiListener) {
233        mUserActivityListener = userActivitiListener;
234    }
235
236    private void userActivity() {
237        mPM.userActivity(SystemClock.uptimeMillis(), false);
238        if (mUserActivityListener != null) {
239            mUserActivityListener.onUserActivity();
240        }
241    }
242
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
255    public String getText() {
256        return mText;
257    }
258
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
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
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
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
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
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     */
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
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
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
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         */
537        private void postDotSwap(long delay) {
538            removeDotSwapCallbacks();
539            postDelayed(dotSwapperRunnable, delay);
540            isDotSwapPending = true;
541        }
542
543        private void removeDotSwapCallbacks() {
544            removeCallbacks(dotSwapperRunnable);
545            isDotSwapPending = false;
546        }
547
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
559        private void performSwap() {
560            startTextDisappearAnimation(0);
561            startDotAppearAnimation(DISAPPEAR_DURATION
562                    - DOT_APPEAR_TEXT_DISAPPEAR_OVERLAP_DURATION);
563        }
564
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
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
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
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
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
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
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         */
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