2 * Copyright (C) 2014 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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
17 package com.android.keyguard;
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;
37 import java.util.ArrayList;
38 import java.util.Stack;
41 * A View similar to a textView which contains password text and can animate when the text is
44 public class PasswordTextView extends View {
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;
54 * The overlap between the text disappearing and the dot appearing animation
56 private static final long DOT_APPEAR_TEXT_DISAPPEAR_OVERLAP_DURATION = 130;
59 * The duration the text needs to stay there at least before it can morph into a dot
61 private static final long TEXT_REST_DURATION_AFTER_APPEAR = 100;
64 * The duration the text should be visible, starting with the appear animation
66 private static final long TEXT_VISIBILITY_DURATION = 1300;
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
72 private static final float OVERSHOOT_TIME_POSITION = 0.5f;
75 * The raw text size, will be multiplied by the scaled density when drawn
77 private final int mTextHeightRaw;
78 private ArrayList<CharState> mTextChars = new ArrayList<>();
79 private String mText = "";
80 private Stack<CharState> mCharPool = new Stack<>();
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;
90 public PasswordTextView(Context context) {
94 public PasswordTextView(Context context, AttributeSet attrs) {
95 this(context, attrs, 0);
98 public PasswordTextView(Context context, AttributeSet attrs, int defStyleAttr) {
99 this(context, attrs, defStyleAttr, 0);
102 public PasswordTextView(Context context, AttributeSet attrs, int defStyleAttr,
104 super(context, attrs, defStyleAttr, defStyleRes);
105 setFocusableInTouchMode(true);
107 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PasswordTextView);
109 mTextHeightRaw = a.getInt(R.styleable.PasswordTextView_scaledTextSize, 0);
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);
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,
144 currentDrawPosition += charWidth;
149 public boolean hasOverlappingRendering() {
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);
161 private float getDrawingWidth() {
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);
169 width += mCharPadding * charState.currentWidthFactor;
171 width += charLength * charState.currentWidthFactor;
177 public void append(char c) {
178 int visibleChars = mTextChars.size();
180 int newLength = mText.length();
182 if (newLength > visibleChars) {
183 charState = obtainCharState(c);
184 mTextChars.add(charState);
186 charState = mTextChars.get(newLength - 1);
187 charState.whichChar = c;
189 charState.startAppearAnimation();
191 // ensure that the previous element is being swapped
193 CharState previousState = mTextChars.get(newLength - 2);
194 if (previousState.isDotSwapPending) {
195 previousState.swapToDotWhenAppearFinished();
201 private void userActivity() {
202 mPM.userActivity(SystemClock.uptimeMillis(), false);
205 public void deleteLastChar() {
206 int length = mText.length();
208 mText = mText.substring(0, length - 1);
209 CharState charState = mTextChars.get(length - 1);
210 charState.startRemoveAnimation(0, 0);
215 public String getText() {
219 private CharState obtainCharState(char c) {
221 if(mCharPool.isEmpty()) {
222 charState = new CharState();
224 charState = mCharPool.pop();
227 charState.whichChar = c;
231 public void reset(boolean animated) {
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);
240 if (i <= middleIndex) {
243 int distToMiddle = i - middleIndex;
244 delayIndex = (length - 1) - (distToMiddle - 1) * 2;
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();
253 mCharPool.push(charState);
261 private class CharState {
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;
276 Animator.AnimatorListener removeEndListener = new AnimatorListenerAdapter() {
277 private boolean mCancelled;
279 public void onAnimationCancel(Animator animation) {
284 public void onAnimationEnd(Animator animation) {
286 mTextChars.remove(CharState.this);
287 mCharPool.push(CharState.this);
289 cancelAnimator(textTranslateAnimator);
290 textTranslateAnimator = null;
295 public void onAnimationStart(Animator animation) {
300 Animator.AnimatorListener dotFinishListener = new AnimatorListenerAdapter() {
302 public void onAnimationEnd(Animator animation) {
307 Animator.AnimatorListener textFinishListener = new AnimatorListenerAdapter() {
309 public void onAnimationEnd(Animator animation) {
314 Animator.AnimatorListener textTranslateFinishListener = new AnimatorListenerAdapter() {
316 public void onAnimationEnd(Animator animation) {
317 textTranslateAnimator = null;
321 Animator.AnimatorListener widthFinishListener = new AnimatorListenerAdapter() {
323 public void onAnimationEnd(Animator animation) {
324 widthAnimator = null;
328 private ValueAnimator.AnimatorUpdateListener dotSizeUpdater
329 = new ValueAnimator.AnimatorUpdateListener() {
331 public void onAnimationUpdate(ValueAnimator animation) {
332 currentDotSizeFactor = (float) animation.getAnimatedValue();
337 private ValueAnimator.AnimatorUpdateListener textSizeUpdater
338 = new ValueAnimator.AnimatorUpdateListener() {
340 public void onAnimationUpdate(ValueAnimator animation) {
341 currentTextSizeFactor = (float) animation.getAnimatedValue();
346 private ValueAnimator.AnimatorUpdateListener textTranslationUpdater
347 = new ValueAnimator.AnimatorUpdateListener() {
349 public void onAnimationUpdate(ValueAnimator animation) {
350 currentTextTranslationY = (float) animation.getAnimatedValue();
355 private ValueAnimator.AnimatorUpdateListener widthUpdater
356 = new ValueAnimator.AnimatorUpdateListener() {
358 public void onAnimationUpdate(ValueAnimator animation) {
359 currentWidthFactor = (float) animation.getAnimatedValue();
364 private Runnable dotSwapperRunnable = new Runnable() {
368 isDotSwapPending = false;
374 currentTextSizeFactor = 0.0f;
375 currentDotSizeFactor = 0.0f;
376 currentWidthFactor = 0.0f;
377 cancelAnimator(textAnimator);
379 cancelAnimator(dotAnimator);
381 cancelAnimator(widthAnimator);
382 widthAnimator = null;
383 currentTextTranslationY = 1.0f;
384 removeDotSwapCallbacks();
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);
397 if (textNeedsAnimation) {
398 startTextDisappearAnimation(startDelay);
400 if (widthNeedsAnimation) {
401 startWidthDisappearAnimation(widthDelay);
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);
414 if (textNeedsAnimation) {
415 startTextAppearAnimation();
417 if (widthNeedsAnimation) {
418 startWidthAppearAnimation();
421 postDotSwap(TEXT_VISIBILITY_DURATION);
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}.
429 private void postDotSwap(long delay) {
430 removeDotSwapCallbacks();
431 postDelayed(dotSwapperRunnable, delay);
432 isDotSwapPending = true;
435 private void removeDotSwapCallbacks() {
436 removeCallbacks(dotSwapperRunnable);
437 isDotSwapPending = false;
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);
451 private void performSwap() {
452 startTextDisappearAnimation(0);
453 startDotAppearAnimation(DISAPPEAR_DURATION
454 - DOT_APPEAR_TEXT_DISAPPEAR_OVERLAP_DURATION);
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;
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;
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);
491 dotAnimator = animator;
492 dotAnimationIsGrowing = false;
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;
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;
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();
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,
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);
546 dotAnimator = animatorSet;
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;
556 dotAnimationIsGrowing = true;
559 private void cancelAnimator(Animator animator) {
560 if (animator != null) {
566 * Draw this char to the canvas.
568 * @return The width this character contributes, including padding.
570 public float draw(Canvas canvas, float currentDrawPosition, int charHeight, float yPosition,
572 boolean textVisible = currentTextSizeFactor > 0;
573 boolean dotVisible = currentDotSizeFactor > 0;
574 float charWidth = charLength * currentWidthFactor;
576 float currYPosition = yPosition + charHeight / 2.0f * currentTextSizeFactor
577 + charHeight * currentTextTranslationY * 0.8f;
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);
587 float centerX = currentDrawPosition + charWidth / 2;
588 canvas.translate(centerX, yPosition);
589 canvas.drawCircle(0, 0, mDotSize / 2 * currentDotSizeFactor, mDrawPaint);
592 return charWidth + mCharPadding * currentWidthFactor;