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