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