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.Color; 27 import android.graphics.Paint; 28 import android.graphics.Rect; 29 import android.graphics.Typeface; 30 import android.os.PowerManager; 31 import android.os.SystemClock; 32 import android.provider.Settings; 33 import android.text.InputType; 34 import android.text.TextUtils; 35 import android.util.AttributeSet; 36 import android.view.Gravity; 37 import android.view.View; 38 import android.view.accessibility.AccessibilityEvent; 39 import android.view.accessibility.AccessibilityManager; 40 import android.view.accessibility.AccessibilityNodeInfo; 41 import android.view.animation.AnimationUtils; 42 import android.view.animation.Interpolator; 43 import android.widget.EditText; 44 45 import java.util.ArrayList; 46 import java.util.Stack; 47 48 /** 49 * A View similar to a textView which contains password text and can animate when the text is 50 * changed 51 */ 52 public class PasswordTextView extends View { 53 54 private static final float DOT_OVERSHOOT_FACTOR = 1.5f; 55 private static final long DOT_APPEAR_DURATION_OVERSHOOT = 320; 56 private static final long APPEAR_DURATION = 160; 57 private static final long DISAPPEAR_DURATION = 160; 58 private static final long RESET_DELAY_PER_ELEMENT = 40; 59 private static final long RESET_MAX_DELAY = 200; 60 61 /** 62 * The overlap between the text disappearing and the dot appearing animation 63 */ 64 private static final long DOT_APPEAR_TEXT_DISAPPEAR_OVERLAP_DURATION = 130; 65 66 /** 67 * The duration the text needs to stay there at least before it can morph into a dot 68 */ 69 private static final long TEXT_REST_DURATION_AFTER_APPEAR = 100; 70 71 /** 72 * The duration the text should be visible, starting with the appear animation 73 */ 74 private static final long TEXT_VISIBILITY_DURATION = 1300; 75 76 /** 77 * The position in time from [0,1] where the overshoot should be finished and the settle back 78 * animation of the dot should start 79 */ 80 private static final float OVERSHOOT_TIME_POSITION = 0.5f; 81 82 private static char DOT = '\u2022'; 83 84 /** 85 * The raw text size, will be multiplied by the scaled density when drawn 86 */ 87 private final int mTextHeightRaw; 88 private final int mGravity; 89 private ArrayList<CharState> mTextChars = new ArrayList<>(); 90 private String mText = ""; 91 private Stack<CharState> mCharPool = new Stack<>(); 92 private int mDotSize; 93 private PowerManager mPM; 94 private int mCharPadding; 95 private final Paint mDrawPaint = new Paint(); 96 private Interpolator mAppearInterpolator; 97 private Interpolator mDisappearInterpolator; 98 private Interpolator mFastOutSlowInInterpolator; 99 private boolean mShowPassword; 100 private UserActivityListener mUserActivityListener; 101 102 public interface UserActivityListener { onUserActivity()103 void onUserActivity(); 104 } 105 PasswordTextView(Context context)106 public PasswordTextView(Context context) { 107 this(context, null); 108 } 109 PasswordTextView(Context context, AttributeSet attrs)110 public PasswordTextView(Context context, AttributeSet attrs) { 111 this(context, attrs, 0); 112 } 113 PasswordTextView(Context context, AttributeSet attrs, int defStyleAttr)114 public PasswordTextView(Context context, AttributeSet attrs, int defStyleAttr) { 115 this(context, attrs, defStyleAttr, 0); 116 } 117 PasswordTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)118 public PasswordTextView(Context context, AttributeSet attrs, int defStyleAttr, 119 int defStyleRes) { 120 super(context, attrs, defStyleAttr, defStyleRes); 121 setFocusableInTouchMode(true); 122 setFocusable(true); 123 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PasswordTextView); 124 try { 125 mTextHeightRaw = a.getInt(R.styleable.PasswordTextView_scaledTextSize, 0); 126 mGravity = a.getInt(R.styleable.PasswordTextView_android_gravity, Gravity.CENTER); 127 mDotSize = a.getDimensionPixelSize(R.styleable.PasswordTextView_dotSize, 128 getContext().getResources().getDimensionPixelSize(R.dimen.password_dot_size)); 129 mCharPadding = a.getDimensionPixelSize(R.styleable.PasswordTextView_charPadding, 130 getContext().getResources().getDimensionPixelSize( 131 R.dimen.password_char_padding)); 132 int textColor = a.getColor(R.styleable.PasswordTextView_android_textColor, Color.WHITE); 133 mDrawPaint.setColor(textColor); 134 } finally { 135 a.recycle(); 136 } 137 mDrawPaint.setFlags(Paint.SUBPIXEL_TEXT_FLAG | Paint.ANTI_ALIAS_FLAG); 138 mDrawPaint.setTextAlign(Paint.Align.CENTER); 139 mDrawPaint.setTypeface(Typeface.create( 140 context.getString(com.android.internal.R.string.config_headlineFontFamily), 141 0)); 142 mShowPassword = Settings.System.getInt(mContext.getContentResolver(), 143 Settings.System.TEXT_SHOW_PASSWORD, 1) == 1; 144 mAppearInterpolator = AnimationUtils.loadInterpolator(mContext, 145 android.R.interpolator.linear_out_slow_in); 146 mDisappearInterpolator = AnimationUtils.loadInterpolator(mContext, 147 android.R.interpolator.fast_out_linear_in); 148 mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(mContext, 149 android.R.interpolator.fast_out_slow_in); 150 mPM = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); 151 } 152 153 @Override onDraw(Canvas canvas)154 protected void onDraw(Canvas canvas) { 155 float totalDrawingWidth = getDrawingWidth(); 156 float currentDrawPosition; 157 if ((mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.LEFT) { 158 if ((mGravity & Gravity.RELATIVE_LAYOUT_DIRECTION) != 0 159 && getLayoutDirection() == LAYOUT_DIRECTION_RTL) { 160 currentDrawPosition = getWidth() - getPaddingRight() - totalDrawingWidth; 161 } else { 162 currentDrawPosition = getPaddingLeft(); 163 } 164 } else { 165 currentDrawPosition = getWidth() / 2 - totalDrawingWidth / 2; 166 } 167 int length = mTextChars.size(); 168 Rect bounds = getCharBounds(); 169 int charHeight = (bounds.bottom - bounds.top); 170 float yPosition = 171 (getHeight() - getPaddingBottom() - getPaddingTop()) / 2 + getPaddingTop(); 172 canvas.clipRect(getPaddingLeft(), getPaddingTop(), 173 getWidth() - getPaddingRight(), getHeight() - getPaddingBottom()); 174 float charLength = bounds.right - bounds.left; 175 for (int i = 0; i < length; i++) { 176 CharState charState = mTextChars.get(i); 177 float charWidth = charState.draw(canvas, currentDrawPosition, charHeight, yPosition, 178 charLength); 179 currentDrawPosition += charWidth; 180 } 181 } 182 183 @Override hasOverlappingRendering()184 public boolean hasOverlappingRendering() { 185 return false; 186 } 187 getCharBounds()188 private Rect getCharBounds() { 189 float textHeight = mTextHeightRaw * getResources().getDisplayMetrics().scaledDensity; 190 mDrawPaint.setTextSize(textHeight); 191 Rect bounds = new Rect(); 192 mDrawPaint.getTextBounds("0", 0, 1, bounds); 193 return bounds; 194 } 195 getDrawingWidth()196 private float getDrawingWidth() { 197 int width = 0; 198 int length = mTextChars.size(); 199 Rect bounds = getCharBounds(); 200 int charLength = bounds.right - bounds.left; 201 for (int i = 0; i < length; i++) { 202 CharState charState = mTextChars.get(i); 203 if (i != 0) { 204 width += mCharPadding * charState.currentWidthFactor; 205 } 206 width += charLength * charState.currentWidthFactor; 207 } 208 return width; 209 } 210 211 append(char c)212 public void append(char c) { 213 int visibleChars = mTextChars.size(); 214 CharSequence textbefore = getTransformedText(); 215 mText = mText + c; 216 int newLength = mText.length(); 217 CharState charState; 218 if (newLength > visibleChars) { 219 charState = obtainCharState(c); 220 mTextChars.add(charState); 221 } else { 222 charState = mTextChars.get(newLength - 1); 223 charState.whichChar = c; 224 } 225 charState.startAppearAnimation(); 226 227 // ensure that the previous element is being swapped 228 if (newLength > 1) { 229 CharState previousState = mTextChars.get(newLength - 2); 230 if (previousState.isDotSwapPending) { 231 previousState.swapToDotWhenAppearFinished(); 232 } 233 } 234 userActivity(); 235 sendAccessibilityEventTypeViewTextChanged(textbefore, textbefore.length(), 0, 1); 236 } 237 setUserActivityListener(UserActivityListener userActivitiListener)238 public void setUserActivityListener(UserActivityListener userActivitiListener) { 239 mUserActivityListener = userActivitiListener; 240 } 241 userActivity()242 private void userActivity() { 243 mPM.userActivity(SystemClock.uptimeMillis(), false); 244 if (mUserActivityListener != null) { 245 mUserActivityListener.onUserActivity(); 246 } 247 } 248 deleteLastChar()249 public void deleteLastChar() { 250 int length = mText.length(); 251 CharSequence textbefore = getTransformedText(); 252 if (length > 0) { 253 mText = mText.substring(0, length - 1); 254 CharState charState = mTextChars.get(length - 1); 255 charState.startRemoveAnimation(0, 0); 256 sendAccessibilityEventTypeViewTextChanged(textbefore, textbefore.length() - 1, 1, 0); 257 } 258 userActivity(); 259 } 260 getText()261 public String getText() { 262 return mText; 263 } 264 getTransformedText()265 private CharSequence getTransformedText() { 266 int textLength = mTextChars.size(); 267 StringBuilder stringBuilder = new StringBuilder(textLength); 268 for (int i = 0; i < textLength; i++) { 269 CharState charState = mTextChars.get(i); 270 // If the dot is disappearing, the character is disappearing entirely. Consider 271 // it gone. 272 if (charState.dotAnimator != null && !charState.dotAnimationIsGrowing) { 273 continue; 274 } 275 stringBuilder.append(charState.isCharVisibleForA11y() ? charState.whichChar : DOT); 276 } 277 return stringBuilder; 278 } 279 obtainCharState(char c)280 private CharState obtainCharState(char c) { 281 CharState charState; 282 if(mCharPool.isEmpty()) { 283 charState = new CharState(); 284 } else { 285 charState = mCharPool.pop(); 286 charState.reset(); 287 } 288 charState.whichChar = c; 289 return charState; 290 } 291 reset(boolean animated, boolean announce)292 public void reset(boolean animated, boolean announce) { 293 CharSequence textbefore = getTransformedText(); 294 mText = ""; 295 int length = mTextChars.size(); 296 int middleIndex = (length - 1) / 2; 297 long delayPerElement = RESET_DELAY_PER_ELEMENT; 298 for (int i = 0; i < length; i++) { 299 CharState charState = mTextChars.get(i); 300 if (animated) { 301 int delayIndex; 302 if (i <= middleIndex) { 303 delayIndex = i * 2; 304 } else { 305 int distToMiddle = i - middleIndex; 306 delayIndex = (length - 1) - (distToMiddle - 1) * 2; 307 } 308 long startDelay = delayIndex * delayPerElement; 309 startDelay = Math.min(startDelay, RESET_MAX_DELAY); 310 long maxDelay = delayPerElement * (length - 1); 311 maxDelay = Math.min(maxDelay, RESET_MAX_DELAY) + DISAPPEAR_DURATION; 312 charState.startRemoveAnimation(startDelay, maxDelay); 313 charState.removeDotSwapCallbacks(); 314 } else { 315 mCharPool.push(charState); 316 } 317 } 318 if (!animated) { 319 mTextChars.clear(); 320 } 321 if (announce) { 322 sendAccessibilityEventTypeViewTextChanged(textbefore, 0, textbefore.length(), 0); 323 } 324 } 325 sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText, int fromIndex, int removedCount, int addedCount)326 void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText, int fromIndex, 327 int removedCount, int addedCount) { 328 if (AccessibilityManager.getInstance(mContext).isEnabled() && 329 (isFocused() || isSelected() && isShown())) { 330 AccessibilityEvent event = 331 AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED); 332 event.setFromIndex(fromIndex); 333 event.setRemovedCount(removedCount); 334 event.setAddedCount(addedCount); 335 event.setBeforeText(beforeText); 336 CharSequence transformedText = getTransformedText(); 337 if (!TextUtils.isEmpty(transformedText)) { 338 event.getText().add(transformedText); 339 } 340 event.setPassword(true); 341 sendAccessibilityEventUnchecked(event); 342 } 343 } 344 345 @Override onInitializeAccessibilityEvent(AccessibilityEvent event)346 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 347 super.onInitializeAccessibilityEvent(event); 348 349 event.setClassName(EditText.class.getName()); 350 event.setPassword(true); 351 } 352 353 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)354 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 355 super.onInitializeAccessibilityNodeInfo(info); 356 357 info.setClassName(EditText.class.getName()); 358 info.setPassword(true); 359 info.setText(getTransformedText()); 360 361 info.setEditable(true); 362 363 info.setInputType(InputType.TYPE_NUMBER_VARIATION_PASSWORD); 364 } 365 366 private class CharState { 367 char whichChar; 368 ValueAnimator textAnimator; 369 boolean textAnimationIsGrowing; 370 Animator dotAnimator; 371 boolean dotAnimationIsGrowing; 372 ValueAnimator widthAnimator; 373 boolean widthAnimationIsGrowing; 374 float currentTextSizeFactor; 375 float currentDotSizeFactor; 376 float currentWidthFactor; 377 boolean isDotSwapPending; 378 float currentTextTranslationY = 1.0f; 379 ValueAnimator textTranslateAnimator; 380 381 Animator.AnimatorListener removeEndListener = new AnimatorListenerAdapter() { 382 private boolean mCancelled; 383 @Override 384 public void onAnimationCancel(Animator animation) { 385 mCancelled = true; 386 } 387 388 @Override 389 public void onAnimationEnd(Animator animation) { 390 if (!mCancelled) { 391 mTextChars.remove(CharState.this); 392 mCharPool.push(CharState.this); 393 reset(); 394 cancelAnimator(textTranslateAnimator); 395 textTranslateAnimator = null; 396 } 397 } 398 399 @Override 400 public void onAnimationStart(Animator animation) { 401 mCancelled = false; 402 } 403 }; 404 405 Animator.AnimatorListener dotFinishListener = new AnimatorListenerAdapter() { 406 @Override 407 public void onAnimationEnd(Animator animation) { 408 dotAnimator = null; 409 } 410 }; 411 412 Animator.AnimatorListener textFinishListener = new AnimatorListenerAdapter() { 413 @Override 414 public void onAnimationEnd(Animator animation) { 415 textAnimator = null; 416 } 417 }; 418 419 Animator.AnimatorListener textTranslateFinishListener = new AnimatorListenerAdapter() { 420 @Override 421 public void onAnimationEnd(Animator animation) { 422 textTranslateAnimator = null; 423 } 424 }; 425 426 Animator.AnimatorListener widthFinishListener = new AnimatorListenerAdapter() { 427 @Override 428 public void onAnimationEnd(Animator animation) { 429 widthAnimator = null; 430 } 431 }; 432 433 private ValueAnimator.AnimatorUpdateListener dotSizeUpdater 434 = new ValueAnimator.AnimatorUpdateListener() { 435 @Override 436 public void onAnimationUpdate(ValueAnimator animation) { 437 currentDotSizeFactor = (float) animation.getAnimatedValue(); 438 invalidate(); 439 } 440 }; 441 442 private ValueAnimator.AnimatorUpdateListener textSizeUpdater 443 = new ValueAnimator.AnimatorUpdateListener() { 444 @Override 445 public void onAnimationUpdate(ValueAnimator animation) { 446 boolean textVisibleBefore = isCharVisibleForA11y(); 447 float beforeTextSizeFactor = currentTextSizeFactor; 448 currentTextSizeFactor = (float) animation.getAnimatedValue(); 449 if (textVisibleBefore != isCharVisibleForA11y()) { 450 currentTextSizeFactor = beforeTextSizeFactor; 451 CharSequence beforeText = getTransformedText(); 452 currentTextSizeFactor = (float) animation.getAnimatedValue(); 453 int indexOfThisChar = mTextChars.indexOf(CharState.this); 454 if (indexOfThisChar >= 0) { 455 sendAccessibilityEventTypeViewTextChanged( 456 beforeText, indexOfThisChar, 1, 1); 457 } 458 } 459 invalidate(); 460 } 461 }; 462 463 private ValueAnimator.AnimatorUpdateListener textTranslationUpdater 464 = new ValueAnimator.AnimatorUpdateListener() { 465 @Override 466 public void onAnimationUpdate(ValueAnimator animation) { 467 currentTextTranslationY = (float) animation.getAnimatedValue(); 468 invalidate(); 469 } 470 }; 471 472 private ValueAnimator.AnimatorUpdateListener widthUpdater 473 = new ValueAnimator.AnimatorUpdateListener() { 474 @Override 475 public void onAnimationUpdate(ValueAnimator animation) { 476 currentWidthFactor = (float) animation.getAnimatedValue(); 477 invalidate(); 478 } 479 }; 480 481 private Runnable dotSwapperRunnable = new Runnable() { 482 @Override 483 public void run() { 484 performSwap(); 485 isDotSwapPending = false; 486 } 487 }; 488 reset()489 void reset() { 490 whichChar = 0; 491 currentTextSizeFactor = 0.0f; 492 currentDotSizeFactor = 0.0f; 493 currentWidthFactor = 0.0f; 494 cancelAnimator(textAnimator); 495 textAnimator = null; 496 cancelAnimator(dotAnimator); 497 dotAnimator = null; 498 cancelAnimator(widthAnimator); 499 widthAnimator = null; 500 currentTextTranslationY = 1.0f; 501 removeDotSwapCallbacks(); 502 } 503 startRemoveAnimation(long startDelay, long widthDelay)504 void startRemoveAnimation(long startDelay, long widthDelay) { 505 boolean dotNeedsAnimation = (currentDotSizeFactor > 0.0f && dotAnimator == null) 506 || (dotAnimator != null && dotAnimationIsGrowing); 507 boolean textNeedsAnimation = (currentTextSizeFactor > 0.0f && textAnimator == null) 508 || (textAnimator != null && textAnimationIsGrowing); 509 boolean widthNeedsAnimation = (currentWidthFactor > 0.0f && widthAnimator == null) 510 || (widthAnimator != null && widthAnimationIsGrowing); 511 if (dotNeedsAnimation) { 512 startDotDisappearAnimation(startDelay); 513 } 514 if (textNeedsAnimation) { 515 startTextDisappearAnimation(startDelay); 516 } 517 if (widthNeedsAnimation) { 518 startWidthDisappearAnimation(widthDelay); 519 } 520 } 521 startAppearAnimation()522 void startAppearAnimation() { 523 boolean dotNeedsAnimation = !mShowPassword 524 && (dotAnimator == null || !dotAnimationIsGrowing); 525 boolean textNeedsAnimation = mShowPassword 526 && (textAnimator == null || !textAnimationIsGrowing); 527 boolean widthNeedsAnimation = (widthAnimator == null || !widthAnimationIsGrowing); 528 if (dotNeedsAnimation) { 529 startDotAppearAnimation(0); 530 } 531 if (textNeedsAnimation) { 532 startTextAppearAnimation(); 533 } 534 if (widthNeedsAnimation) { 535 startWidthAppearAnimation(); 536 } 537 if (mShowPassword) { 538 postDotSwap(TEXT_VISIBILITY_DURATION); 539 } 540 } 541 542 /** 543 * Posts a runnable which ensures that the text will be replaced by a dot after {@link 544 * com.android.keyguard.PasswordTextView#TEXT_VISIBILITY_DURATION}. 545 */ postDotSwap(long delay)546 private void postDotSwap(long delay) { 547 removeDotSwapCallbacks(); 548 postDelayed(dotSwapperRunnable, delay); 549 isDotSwapPending = true; 550 } 551 removeDotSwapCallbacks()552 private void removeDotSwapCallbacks() { 553 removeCallbacks(dotSwapperRunnable); 554 isDotSwapPending = false; 555 } 556 swapToDotWhenAppearFinished()557 void swapToDotWhenAppearFinished() { 558 removeDotSwapCallbacks(); 559 if (textAnimator != null) { 560 long remainingDuration = textAnimator.getDuration() 561 - textAnimator.getCurrentPlayTime(); 562 postDotSwap(remainingDuration + TEXT_REST_DURATION_AFTER_APPEAR); 563 } else { 564 performSwap(); 565 } 566 } 567 performSwap()568 private void performSwap() { 569 startTextDisappearAnimation(0); 570 startDotAppearAnimation(DISAPPEAR_DURATION 571 - DOT_APPEAR_TEXT_DISAPPEAR_OVERLAP_DURATION); 572 } 573 startWidthDisappearAnimation(long widthDelay)574 private void startWidthDisappearAnimation(long widthDelay) { 575 cancelAnimator(widthAnimator); 576 widthAnimator = ValueAnimator.ofFloat(currentWidthFactor, 0.0f); 577 widthAnimator.addUpdateListener(widthUpdater); 578 widthAnimator.addListener(widthFinishListener); 579 widthAnimator.addListener(removeEndListener); 580 widthAnimator.setDuration((long) (DISAPPEAR_DURATION * currentWidthFactor)); 581 widthAnimator.setStartDelay(widthDelay); 582 widthAnimator.start(); 583 widthAnimationIsGrowing = false; 584 } 585 startTextDisappearAnimation(long startDelay)586 private void startTextDisappearAnimation(long startDelay) { 587 cancelAnimator(textAnimator); 588 textAnimator = ValueAnimator.ofFloat(currentTextSizeFactor, 0.0f); 589 textAnimator.addUpdateListener(textSizeUpdater); 590 textAnimator.addListener(textFinishListener); 591 textAnimator.setInterpolator(mDisappearInterpolator); 592 textAnimator.setDuration((long) (DISAPPEAR_DURATION * currentTextSizeFactor)); 593 textAnimator.setStartDelay(startDelay); 594 textAnimator.start(); 595 textAnimationIsGrowing = false; 596 } 597 startDotDisappearAnimation(long startDelay)598 private void startDotDisappearAnimation(long startDelay) { 599 cancelAnimator(dotAnimator); 600 ValueAnimator animator = ValueAnimator.ofFloat(currentDotSizeFactor, 0.0f); 601 animator.addUpdateListener(dotSizeUpdater); 602 animator.addListener(dotFinishListener); 603 animator.setInterpolator(mDisappearInterpolator); 604 long duration = (long) (DISAPPEAR_DURATION * Math.min(currentDotSizeFactor, 1.0f)); 605 animator.setDuration(duration); 606 animator.setStartDelay(startDelay); 607 animator.start(); 608 dotAnimator = animator; 609 dotAnimationIsGrowing = false; 610 } 611 startWidthAppearAnimation()612 private void startWidthAppearAnimation() { 613 cancelAnimator(widthAnimator); 614 widthAnimator = ValueAnimator.ofFloat(currentWidthFactor, 1.0f); 615 widthAnimator.addUpdateListener(widthUpdater); 616 widthAnimator.addListener(widthFinishListener); 617 widthAnimator.setDuration((long) (APPEAR_DURATION * (1f - currentWidthFactor))); 618 widthAnimator.start(); 619 widthAnimationIsGrowing = true; 620 } 621 startTextAppearAnimation()622 private void startTextAppearAnimation() { 623 cancelAnimator(textAnimator); 624 textAnimator = ValueAnimator.ofFloat(currentTextSizeFactor, 1.0f); 625 textAnimator.addUpdateListener(textSizeUpdater); 626 textAnimator.addListener(textFinishListener); 627 textAnimator.setInterpolator(mAppearInterpolator); 628 textAnimator.setDuration((long) (APPEAR_DURATION * (1f - currentTextSizeFactor))); 629 textAnimator.start(); 630 textAnimationIsGrowing = true; 631 632 // handle translation 633 if (textTranslateAnimator == null) { 634 textTranslateAnimator = ValueAnimator.ofFloat(1.0f, 0.0f); 635 textTranslateAnimator.addUpdateListener(textTranslationUpdater); 636 textTranslateAnimator.addListener(textTranslateFinishListener); 637 textTranslateAnimator.setInterpolator(mAppearInterpolator); 638 textTranslateAnimator.setDuration(APPEAR_DURATION); 639 textTranslateAnimator.start(); 640 } 641 } 642 startDotAppearAnimation(long delay)643 private void startDotAppearAnimation(long delay) { 644 cancelAnimator(dotAnimator); 645 if (!mShowPassword) { 646 // We perform an overshoot animation 647 ValueAnimator overShootAnimator = ValueAnimator.ofFloat(currentDotSizeFactor, 648 DOT_OVERSHOOT_FACTOR); 649 overShootAnimator.addUpdateListener(dotSizeUpdater); 650 overShootAnimator.setInterpolator(mAppearInterpolator); 651 long overShootDuration = (long) (DOT_APPEAR_DURATION_OVERSHOOT 652 * OVERSHOOT_TIME_POSITION); 653 overShootAnimator.setDuration(overShootDuration); 654 ValueAnimator settleBackAnimator = ValueAnimator.ofFloat(DOT_OVERSHOOT_FACTOR, 655 1.0f); 656 settleBackAnimator.addUpdateListener(dotSizeUpdater); 657 settleBackAnimator.setDuration(DOT_APPEAR_DURATION_OVERSHOOT - overShootDuration); 658 settleBackAnimator.addListener(dotFinishListener); 659 AnimatorSet animatorSet = new AnimatorSet(); 660 animatorSet.playSequentially(overShootAnimator, settleBackAnimator); 661 animatorSet.setStartDelay(delay); 662 animatorSet.start(); 663 dotAnimator = animatorSet; 664 } else { 665 ValueAnimator growAnimator = ValueAnimator.ofFloat(currentDotSizeFactor, 1.0f); 666 growAnimator.addUpdateListener(dotSizeUpdater); 667 growAnimator.setDuration((long) (APPEAR_DURATION * (1.0f - currentDotSizeFactor))); 668 growAnimator.addListener(dotFinishListener); 669 growAnimator.setStartDelay(delay); 670 growAnimator.start(); 671 dotAnimator = growAnimator; 672 } 673 dotAnimationIsGrowing = true; 674 } 675 cancelAnimator(Animator animator)676 private void cancelAnimator(Animator animator) { 677 if (animator != null) { 678 animator.cancel(); 679 } 680 } 681 682 /** 683 * Draw this char to the canvas. 684 * 685 * @return The width this character contributes, including padding. 686 */ draw(Canvas canvas, float currentDrawPosition, int charHeight, float yPosition, float charLength)687 public float draw(Canvas canvas, float currentDrawPosition, int charHeight, float yPosition, 688 float charLength) { 689 boolean textVisible = currentTextSizeFactor > 0; 690 boolean dotVisible = currentDotSizeFactor > 0; 691 float charWidth = charLength * currentWidthFactor; 692 if (textVisible) { 693 float currYPosition = yPosition + charHeight / 2.0f * currentTextSizeFactor 694 + charHeight * currentTextTranslationY * 0.8f; 695 canvas.save(); 696 float centerX = currentDrawPosition + charWidth / 2; 697 canvas.translate(centerX, currYPosition); 698 canvas.scale(currentTextSizeFactor, currentTextSizeFactor); 699 canvas.drawText(Character.toString(whichChar), 0, 0, mDrawPaint); 700 canvas.restore(); 701 } 702 if (dotVisible) { 703 canvas.save(); 704 float centerX = currentDrawPosition + charWidth / 2; 705 canvas.translate(centerX, yPosition); 706 canvas.drawCircle(0, 0, mDotSize / 2 * currentDotSizeFactor, mDrawPaint); 707 canvas.restore(); 708 } 709 return charWidth + mCharPadding * currentWidthFactor; 710 } 711 isCharVisibleForA11y()712 public boolean isCharVisibleForA11y() { 713 // The text has size 0 when it is first added, but we want to count it as visible if 714 // it will become visible presently. Count text as visible if an animator 715 // is configured to make it grow. 716 boolean textIsGrowing = textAnimator != null && textAnimationIsGrowing; 717 return (currentTextSizeFactor > 0) || textIsGrowing; 718 } 719 } 720 } 721