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