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