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