Home | History | Annotate | Download | only in keyguard
      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 
     96     public PasswordTextView(Context context) {
     97         this(context, null);
     98     }
     99 
    100     public PasswordTextView(Context context, AttributeSet attrs) {
    101         this(context, attrs, 0);
    102     }
    103 
    104     public PasswordTextView(Context context, AttributeSet attrs, int defStyleAttr) {
    105         this(context, attrs, defStyleAttr, 0);
    106     }
    107 
    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
    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
    155     public boolean hasOverlappingRendering() {
    156         return false;
    157     }
    158 
    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 
    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 
    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 
    209     private void userActivity() {
    210         mPM.userActivity(SystemClock.uptimeMillis(), false);
    211     }
    212 
    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 
    225     public String getText() {
    226         return mText;
    227     }
    228 
    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 
    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 
    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
    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
    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
    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      */
    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 
    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 
    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 
    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          */
    505         private void postDotSwap(long delay) {
    506             removeDotSwapCallbacks();
    507             postDelayed(dotSwapperRunnable, delay);
    508             isDotSwapPending = true;
    509         }
    510 
    511         private void removeDotSwapCallbacks() {
    512             removeCallbacks(dotSwapperRunnable);
    513             isDotSwapPending = false;
    514         }
    515 
    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 
    527         private void performSwap() {
    528             startTextDisappearAnimation(0);
    529             startDotAppearAnimation(DISAPPEAR_DURATION
    530                     - DOT_APPEAR_TEXT_DISAPPEAR_OVERLAP_DURATION);
    531         }
    532 
    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 
    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 
    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 
    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 
    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 
    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 
    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          */
    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