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