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