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