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