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 float maxRight = getWidth() - getPaddingRight() - totalDrawingWidth; 168 float center = getWidth() / 2f - totalDrawingWidth / 2f; 169 currentDrawPosition = center > 0 ? center : maxRight; 170 } 171 int length = mTextChars.size(); 172 Rect bounds = getCharBounds(); 173 int charHeight = (bounds.bottom - bounds.top); 174 float yPosition = 175 (getHeight() - getPaddingBottom() - getPaddingTop()) / 2 + getPaddingTop(); 176 canvas.clipRect(getPaddingLeft(), getPaddingTop(), 177 getWidth() - getPaddingRight(), getHeight() - getPaddingBottom()); 178 float charLength = bounds.right - bounds.left; 179 for (int i = 0; i < length; i++) { 180 CharState charState = mTextChars.get(i); 181 float charWidth = charState.draw(canvas, currentDrawPosition, charHeight, yPosition, 182 charLength); 183 currentDrawPosition += charWidth; 184 } 185 } 186 187 @Override hasOverlappingRendering()188 public boolean hasOverlappingRendering() { 189 return false; 190 } 191 getCharBounds()192 private Rect getCharBounds() { 193 float textHeight = mTextHeightRaw * getResources().getDisplayMetrics().scaledDensity; 194 mDrawPaint.setTextSize(textHeight); 195 Rect bounds = new Rect(); 196 mDrawPaint.getTextBounds("0", 0, 1, bounds); 197 return bounds; 198 } 199 getDrawingWidth()200 private float getDrawingWidth() { 201 int width = 0; 202 int length = mTextChars.size(); 203 Rect bounds = getCharBounds(); 204 int charLength = bounds.right - bounds.left; 205 for (int i = 0; i < length; i++) { 206 CharState charState = mTextChars.get(i); 207 if (i != 0) { 208 width += mCharPadding * charState.currentWidthFactor; 209 } 210 width += charLength * charState.currentWidthFactor; 211 } 212 return width; 213 } 214 215 append(char c)216 public void append(char c) { 217 int visibleChars = mTextChars.size(); 218 CharSequence textbefore = getTransformedText(); 219 mText = mText + c; 220 int newLength = mText.length(); 221 CharState charState; 222 if (newLength > visibleChars) { 223 charState = obtainCharState(c); 224 mTextChars.add(charState); 225 } else { 226 charState = mTextChars.get(newLength - 1); 227 charState.whichChar = c; 228 } 229 charState.startAppearAnimation(); 230 231 // ensure that the previous element is being swapped 232 if (newLength > 1) { 233 CharState previousState = mTextChars.get(newLength - 2); 234 if (previousState.isDotSwapPending) { 235 previousState.swapToDotWhenAppearFinished(); 236 } 237 } 238 userActivity(); 239 sendAccessibilityEventTypeViewTextChanged(textbefore, textbefore.length(), 0, 1); 240 } 241 setUserActivityListener(UserActivityListener userActivitiListener)242 public void setUserActivityListener(UserActivityListener userActivitiListener) { 243 mUserActivityListener = userActivitiListener; 244 } 245 userActivity()246 private void userActivity() { 247 mPM.userActivity(SystemClock.uptimeMillis(), false); 248 if (mUserActivityListener != null) { 249 mUserActivityListener.onUserActivity(); 250 } 251 } 252 deleteLastChar()253 public void deleteLastChar() { 254 int length = mText.length(); 255 CharSequence textbefore = getTransformedText(); 256 if (length > 0) { 257 mText = mText.substring(0, length - 1); 258 CharState charState = mTextChars.get(length - 1); 259 charState.startRemoveAnimation(0, 0); 260 sendAccessibilityEventTypeViewTextChanged(textbefore, textbefore.length() - 1, 1, 0); 261 } 262 userActivity(); 263 } 264 getText()265 public String getText() { 266 return mText; 267 } 268 getTransformedText()269 private CharSequence getTransformedText() { 270 int textLength = mTextChars.size(); 271 StringBuilder stringBuilder = new StringBuilder(textLength); 272 for (int i = 0; i < textLength; i++) { 273 CharState charState = mTextChars.get(i); 274 // If the dot is disappearing, the character is disappearing entirely. Consider 275 // it gone. 276 if (charState.dotAnimator != null && !charState.dotAnimationIsGrowing) { 277 continue; 278 } 279 stringBuilder.append(charState.isCharVisibleForA11y() ? charState.whichChar : DOT); 280 } 281 return stringBuilder; 282 } 283 obtainCharState(char c)284 private CharState obtainCharState(char c) { 285 CharState charState; 286 if(mCharPool.isEmpty()) { 287 charState = new CharState(); 288 } else { 289 charState = mCharPool.pop(); 290 charState.reset(); 291 } 292 charState.whichChar = c; 293 return charState; 294 } 295 reset(boolean animated, boolean announce)296 public void reset(boolean animated, boolean announce) { 297 CharSequence textbefore = getTransformedText(); 298 mText = ""; 299 int length = mTextChars.size(); 300 int middleIndex = (length - 1) / 2; 301 long delayPerElement = RESET_DELAY_PER_ELEMENT; 302 for (int i = 0; i < length; i++) { 303 CharState charState = mTextChars.get(i); 304 if (animated) { 305 int delayIndex; 306 if (i <= middleIndex) { 307 delayIndex = i * 2; 308 } else { 309 int distToMiddle = i - middleIndex; 310 delayIndex = (length - 1) - (distToMiddle - 1) * 2; 311 } 312 long startDelay = delayIndex * delayPerElement; 313 startDelay = Math.min(startDelay, RESET_MAX_DELAY); 314 long maxDelay = delayPerElement * (length - 1); 315 maxDelay = Math.min(maxDelay, RESET_MAX_DELAY) + DISAPPEAR_DURATION; 316 charState.startRemoveAnimation(startDelay, maxDelay); 317 charState.removeDotSwapCallbacks(); 318 } else { 319 mCharPool.push(charState); 320 } 321 } 322 if (!animated) { 323 mTextChars.clear(); 324 } 325 if (announce) { 326 sendAccessibilityEventTypeViewTextChanged(textbefore, 0, textbefore.length(), 0); 327 } 328 } 329 sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText, int fromIndex, int removedCount, int addedCount)330 void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText, int fromIndex, 331 int removedCount, int addedCount) { 332 if (AccessibilityManager.getInstance(mContext).isEnabled() && 333 (isFocused() || isSelected() && isShown())) { 334 AccessibilityEvent event = 335 AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED); 336 event.setFromIndex(fromIndex); 337 event.setRemovedCount(removedCount); 338 event.setAddedCount(addedCount); 339 event.setBeforeText(beforeText); 340 CharSequence transformedText = getTransformedText(); 341 if (!TextUtils.isEmpty(transformedText)) { 342 event.getText().add(transformedText); 343 } 344 event.setPassword(true); 345 sendAccessibilityEventUnchecked(event); 346 } 347 } 348 349 @Override onInitializeAccessibilityEvent(AccessibilityEvent event)350 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 351 super.onInitializeAccessibilityEvent(event); 352 353 event.setClassName(EditText.class.getName()); 354 event.setPassword(true); 355 } 356 357 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)358 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 359 super.onInitializeAccessibilityNodeInfo(info); 360 361 info.setClassName(EditText.class.getName()); 362 info.setPassword(true); 363 info.setText(getTransformedText()); 364 365 info.setEditable(true); 366 367 info.setInputType(InputType.TYPE_NUMBER_VARIATION_PASSWORD); 368 } 369 370 private class CharState { 371 char whichChar; 372 ValueAnimator textAnimator; 373 boolean textAnimationIsGrowing; 374 Animator dotAnimator; 375 boolean dotAnimationIsGrowing; 376 ValueAnimator widthAnimator; 377 boolean widthAnimationIsGrowing; 378 float currentTextSizeFactor; 379 float currentDotSizeFactor; 380 float currentWidthFactor; 381 boolean isDotSwapPending; 382 float currentTextTranslationY = 1.0f; 383 ValueAnimator textTranslateAnimator; 384 385 Animator.AnimatorListener removeEndListener = new AnimatorListenerAdapter() { 386 private boolean mCancelled; 387 @Override 388 public void onAnimationCancel(Animator animation) { 389 mCancelled = true; 390 } 391 392 @Override 393 public void onAnimationEnd(Animator animation) { 394 if (!mCancelled) { 395 mTextChars.remove(CharState.this); 396 mCharPool.push(CharState.this); 397 reset(); 398 cancelAnimator(textTranslateAnimator); 399 textTranslateAnimator = null; 400 } 401 } 402 403 @Override 404 public void onAnimationStart(Animator animation) { 405 mCancelled = false; 406 } 407 }; 408 409 Animator.AnimatorListener dotFinishListener = new AnimatorListenerAdapter() { 410 @Override 411 public void onAnimationEnd(Animator animation) { 412 dotAnimator = null; 413 } 414 }; 415 416 Animator.AnimatorListener textFinishListener = new AnimatorListenerAdapter() { 417 @Override 418 public void onAnimationEnd(Animator animation) { 419 textAnimator = null; 420 } 421 }; 422 423 Animator.AnimatorListener textTranslateFinishListener = new AnimatorListenerAdapter() { 424 @Override 425 public void onAnimationEnd(Animator animation) { 426 textTranslateAnimator = null; 427 } 428 }; 429 430 Animator.AnimatorListener widthFinishListener = new AnimatorListenerAdapter() { 431 @Override 432 public void onAnimationEnd(Animator animation) { 433 widthAnimator = null; 434 } 435 }; 436 437 private ValueAnimator.AnimatorUpdateListener dotSizeUpdater 438 = new ValueAnimator.AnimatorUpdateListener() { 439 @Override 440 public void onAnimationUpdate(ValueAnimator animation) { 441 currentDotSizeFactor = (float) animation.getAnimatedValue(); 442 invalidate(); 443 } 444 }; 445 446 private ValueAnimator.AnimatorUpdateListener textSizeUpdater 447 = new ValueAnimator.AnimatorUpdateListener() { 448 @Override 449 public void onAnimationUpdate(ValueAnimator animation) { 450 boolean textVisibleBefore = isCharVisibleForA11y(); 451 float beforeTextSizeFactor = currentTextSizeFactor; 452 currentTextSizeFactor = (float) animation.getAnimatedValue(); 453 if (textVisibleBefore != isCharVisibleForA11y()) { 454 currentTextSizeFactor = beforeTextSizeFactor; 455 CharSequence beforeText = getTransformedText(); 456 currentTextSizeFactor = (float) animation.getAnimatedValue(); 457 int indexOfThisChar = mTextChars.indexOf(CharState.this); 458 if (indexOfThisChar >= 0) { 459 sendAccessibilityEventTypeViewTextChanged( 460 beforeText, indexOfThisChar, 1, 1); 461 } 462 } 463 invalidate(); 464 } 465 }; 466 467 private ValueAnimator.AnimatorUpdateListener textTranslationUpdater 468 = new ValueAnimator.AnimatorUpdateListener() { 469 @Override 470 public void onAnimationUpdate(ValueAnimator animation) { 471 currentTextTranslationY = (float) animation.getAnimatedValue(); 472 invalidate(); 473 } 474 }; 475 476 private ValueAnimator.AnimatorUpdateListener widthUpdater 477 = new ValueAnimator.AnimatorUpdateListener() { 478 @Override 479 public void onAnimationUpdate(ValueAnimator animation) { 480 currentWidthFactor = (float) animation.getAnimatedValue(); 481 invalidate(); 482 } 483 }; 484 485 private Runnable dotSwapperRunnable = new Runnable() { 486 @Override 487 public void run() { 488 performSwap(); 489 isDotSwapPending = false; 490 } 491 }; 492 reset()493 void reset() { 494 whichChar = 0; 495 currentTextSizeFactor = 0.0f; 496 currentDotSizeFactor = 0.0f; 497 currentWidthFactor = 0.0f; 498 cancelAnimator(textAnimator); 499 textAnimator = null; 500 cancelAnimator(dotAnimator); 501 dotAnimator = null; 502 cancelAnimator(widthAnimator); 503 widthAnimator = null; 504 currentTextTranslationY = 1.0f; 505 removeDotSwapCallbacks(); 506 } 507 startRemoveAnimation(long startDelay, long widthDelay)508 void startRemoveAnimation(long startDelay, long widthDelay) { 509 boolean dotNeedsAnimation = (currentDotSizeFactor > 0.0f && dotAnimator == null) 510 || (dotAnimator != null && dotAnimationIsGrowing); 511 boolean textNeedsAnimation = (currentTextSizeFactor > 0.0f && textAnimator == null) 512 || (textAnimator != null && textAnimationIsGrowing); 513 boolean widthNeedsAnimation = (currentWidthFactor > 0.0f && widthAnimator == null) 514 || (widthAnimator != null && widthAnimationIsGrowing); 515 if (dotNeedsAnimation) { 516 startDotDisappearAnimation(startDelay); 517 } 518 if (textNeedsAnimation) { 519 startTextDisappearAnimation(startDelay); 520 } 521 if (widthNeedsAnimation) { 522 startWidthDisappearAnimation(widthDelay); 523 } 524 } 525 startAppearAnimation()526 void startAppearAnimation() { 527 boolean dotNeedsAnimation = !mShowPassword 528 && (dotAnimator == null || !dotAnimationIsGrowing); 529 boolean textNeedsAnimation = mShowPassword 530 && (textAnimator == null || !textAnimationIsGrowing); 531 boolean widthNeedsAnimation = (widthAnimator == null || !widthAnimationIsGrowing); 532 if (dotNeedsAnimation) { 533 startDotAppearAnimation(0); 534 } 535 if (textNeedsAnimation) { 536 startTextAppearAnimation(); 537 } 538 if (widthNeedsAnimation) { 539 startWidthAppearAnimation(); 540 } 541 if (mShowPassword) { 542 postDotSwap(TEXT_VISIBILITY_DURATION); 543 } 544 } 545 546 /** 547 * Posts a runnable which ensures that the text will be replaced by a dot after {@link 548 * com.android.keyguard.PasswordTextView#TEXT_VISIBILITY_DURATION}. 549 */ postDotSwap(long delay)550 private void postDotSwap(long delay) { 551 removeDotSwapCallbacks(); 552 postDelayed(dotSwapperRunnable, delay); 553 isDotSwapPending = true; 554 } 555 removeDotSwapCallbacks()556 private void removeDotSwapCallbacks() { 557 removeCallbacks(dotSwapperRunnable); 558 isDotSwapPending = false; 559 } 560 swapToDotWhenAppearFinished()561 void swapToDotWhenAppearFinished() { 562 removeDotSwapCallbacks(); 563 if (textAnimator != null) { 564 long remainingDuration = textAnimator.getDuration() 565 - textAnimator.getCurrentPlayTime(); 566 postDotSwap(remainingDuration + TEXT_REST_DURATION_AFTER_APPEAR); 567 } else { 568 performSwap(); 569 } 570 } 571 performSwap()572 private void performSwap() { 573 startTextDisappearAnimation(0); 574 startDotAppearAnimation(DISAPPEAR_DURATION 575 - DOT_APPEAR_TEXT_DISAPPEAR_OVERLAP_DURATION); 576 } 577 startWidthDisappearAnimation(long widthDelay)578 private void startWidthDisappearAnimation(long widthDelay) { 579 cancelAnimator(widthAnimator); 580 widthAnimator = ValueAnimator.ofFloat(currentWidthFactor, 0.0f); 581 widthAnimator.addUpdateListener(widthUpdater); 582 widthAnimator.addListener(widthFinishListener); 583 widthAnimator.addListener(removeEndListener); 584 widthAnimator.setDuration((long) (DISAPPEAR_DURATION * currentWidthFactor)); 585 widthAnimator.setStartDelay(widthDelay); 586 widthAnimator.start(); 587 widthAnimationIsGrowing = false; 588 } 589 startTextDisappearAnimation(long startDelay)590 private void startTextDisappearAnimation(long startDelay) { 591 cancelAnimator(textAnimator); 592 textAnimator = ValueAnimator.ofFloat(currentTextSizeFactor, 0.0f); 593 textAnimator.addUpdateListener(textSizeUpdater); 594 textAnimator.addListener(textFinishListener); 595 textAnimator.setInterpolator(mDisappearInterpolator); 596 textAnimator.setDuration((long) (DISAPPEAR_DURATION * currentTextSizeFactor)); 597 textAnimator.setStartDelay(startDelay); 598 textAnimator.start(); 599 textAnimationIsGrowing = false; 600 } 601 startDotDisappearAnimation(long startDelay)602 private void startDotDisappearAnimation(long startDelay) { 603 cancelAnimator(dotAnimator); 604 ValueAnimator animator = ValueAnimator.ofFloat(currentDotSizeFactor, 0.0f); 605 animator.addUpdateListener(dotSizeUpdater); 606 animator.addListener(dotFinishListener); 607 animator.setInterpolator(mDisappearInterpolator); 608 long duration = (long) (DISAPPEAR_DURATION * Math.min(currentDotSizeFactor, 1.0f)); 609 animator.setDuration(duration); 610 animator.setStartDelay(startDelay); 611 animator.start(); 612 dotAnimator = animator; 613 dotAnimationIsGrowing = false; 614 } 615 startWidthAppearAnimation()616 private void startWidthAppearAnimation() { 617 cancelAnimator(widthAnimator); 618 widthAnimator = ValueAnimator.ofFloat(currentWidthFactor, 1.0f); 619 widthAnimator.addUpdateListener(widthUpdater); 620 widthAnimator.addListener(widthFinishListener); 621 widthAnimator.setDuration((long) (APPEAR_DURATION * (1f - currentWidthFactor))); 622 widthAnimator.start(); 623 widthAnimationIsGrowing = true; 624 } 625 startTextAppearAnimation()626 private void startTextAppearAnimation() { 627 cancelAnimator(textAnimator); 628 textAnimator = ValueAnimator.ofFloat(currentTextSizeFactor, 1.0f); 629 textAnimator.addUpdateListener(textSizeUpdater); 630 textAnimator.addListener(textFinishListener); 631 textAnimator.setInterpolator(mAppearInterpolator); 632 textAnimator.setDuration((long) (APPEAR_DURATION * (1f - currentTextSizeFactor))); 633 textAnimator.start(); 634 textAnimationIsGrowing = true; 635 636 // handle translation 637 if (textTranslateAnimator == null) { 638 textTranslateAnimator = ValueAnimator.ofFloat(1.0f, 0.0f); 639 textTranslateAnimator.addUpdateListener(textTranslationUpdater); 640 textTranslateAnimator.addListener(textTranslateFinishListener); 641 textTranslateAnimator.setInterpolator(mAppearInterpolator); 642 textTranslateAnimator.setDuration(APPEAR_DURATION); 643 textTranslateAnimator.start(); 644 } 645 } 646 startDotAppearAnimation(long delay)647 private void startDotAppearAnimation(long delay) { 648 cancelAnimator(dotAnimator); 649 if (!mShowPassword) { 650 // We perform an overshoot animation 651 ValueAnimator overShootAnimator = ValueAnimator.ofFloat(currentDotSizeFactor, 652 DOT_OVERSHOOT_FACTOR); 653 overShootAnimator.addUpdateListener(dotSizeUpdater); 654 overShootAnimator.setInterpolator(mAppearInterpolator); 655 long overShootDuration = (long) (DOT_APPEAR_DURATION_OVERSHOOT 656 * OVERSHOOT_TIME_POSITION); 657 overShootAnimator.setDuration(overShootDuration); 658 ValueAnimator settleBackAnimator = ValueAnimator.ofFloat(DOT_OVERSHOOT_FACTOR, 659 1.0f); 660 settleBackAnimator.addUpdateListener(dotSizeUpdater); 661 settleBackAnimator.setDuration(DOT_APPEAR_DURATION_OVERSHOOT - overShootDuration); 662 settleBackAnimator.addListener(dotFinishListener); 663 AnimatorSet animatorSet = new AnimatorSet(); 664 animatorSet.playSequentially(overShootAnimator, settleBackAnimator); 665 animatorSet.setStartDelay(delay); 666 animatorSet.start(); 667 dotAnimator = animatorSet; 668 } else { 669 ValueAnimator growAnimator = ValueAnimator.ofFloat(currentDotSizeFactor, 1.0f); 670 growAnimator.addUpdateListener(dotSizeUpdater); 671 growAnimator.setDuration((long) (APPEAR_DURATION * (1.0f - currentDotSizeFactor))); 672 growAnimator.addListener(dotFinishListener); 673 growAnimator.setStartDelay(delay); 674 growAnimator.start(); 675 dotAnimator = growAnimator; 676 } 677 dotAnimationIsGrowing = true; 678 } 679 cancelAnimator(Animator animator)680 private void cancelAnimator(Animator animator) { 681 if (animator != null) { 682 animator.cancel(); 683 } 684 } 685 686 /** 687 * Draw this char to the canvas. 688 * 689 * @return The width this character contributes, including padding. 690 */ draw(Canvas canvas, float currentDrawPosition, int charHeight, float yPosition, float charLength)691 public float draw(Canvas canvas, float currentDrawPosition, int charHeight, float yPosition, 692 float charLength) { 693 boolean textVisible = currentTextSizeFactor > 0; 694 boolean dotVisible = currentDotSizeFactor > 0; 695 float charWidth = charLength * currentWidthFactor; 696 if (textVisible) { 697 float currYPosition = yPosition + charHeight / 2.0f * currentTextSizeFactor 698 + charHeight * currentTextTranslationY * 0.8f; 699 canvas.save(); 700 float centerX = currentDrawPosition + charWidth / 2; 701 canvas.translate(centerX, currYPosition); 702 canvas.scale(currentTextSizeFactor, currentTextSizeFactor); 703 canvas.drawText(Character.toString(whichChar), 0, 0, mDrawPaint); 704 canvas.restore(); 705 } 706 if (dotVisible) { 707 canvas.save(); 708 float centerX = currentDrawPosition + charWidth / 2; 709 canvas.translate(centerX, yPosition); 710 canvas.drawCircle(0, 0, mDotSize / 2 * currentDotSizeFactor, mDrawPaint); 711 canvas.restore(); 712 } 713 return charWidth + mCharPadding * currentWidthFactor; 714 } 715 isCharVisibleForA11y()716 public boolean isCharVisibleForA11y() { 717 // The text has size 0 when it is first added, but we want to count it as visible if 718 // it will become visible presently. Count text as visible if an animator 719 // is configured to make it grow. 720 boolean textIsGrowing = textAnimator != null && textAnimationIsGrowing; 721 return (currentTextSizeFactor > 0) || textIsGrowing; 722 } 723 } 724 } 725