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