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