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