OSDN Git Service

Fixed unnecessary layer creations in the pin unlock method
[android-x86/frameworks-base.git] / packages / Keyguard / src / com / android / keyguard / PasswordTextView.java
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 }