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