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