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