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.Gravity;
     37 import android.view.View;
     38 import android.view.accessibility.AccessibilityEvent;
     39 import android.view.accessibility.AccessibilityManager;
     40 import android.view.accessibility.AccessibilityNodeInfo;
     41 import android.view.animation.AnimationUtils;
     42 import android.view.animation.Interpolator;
     43 
     44 import java.util.ArrayList;
     45 import 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  */
     51 public 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