blob: b21bcc98ae68386c389b1a89f3ed32d454ee76ee [file] [log] [blame]
Selim Cinek4e8b9ed2014-06-20 16:37:04 -07001/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package com.android.keyguard;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.AnimatorSet;
22import android.animation.ValueAnimator;
23import android.content.Context;
24import android.content.res.TypedArray;
25import android.graphics.Canvas;
Lucas Dupin987f1932017-05-13 21:02:52 -070026import android.graphics.Color;
Selim Cinek4e8b9ed2014-06-20 16:37:04 -070027import android.graphics.Paint;
28import android.graphics.Rect;
29import android.graphics.Typeface;
30import android.os.PowerManager;
31import android.os.SystemClock;
32import android.provider.Settings;
Adrian Roosa48caee2015-01-15 19:57:51 +010033import android.text.InputType;
Phil Weaverc355c7a2017-12-20 10:54:13 -080034import android.text.TextUtils;
Selim Cinek4e8b9ed2014-06-20 16:37:04 -070035import android.util.AttributeSet;
Evan Rosky90427502016-04-01 16:04:23 -070036import android.view.Gravity;
Selim Cinek4e8b9ed2014-06-20 16:37:04 -070037import android.view.View;
Adrian Roosa48caee2015-01-15 19:57:51 +010038import android.view.accessibility.AccessibilityEvent;
39import android.view.accessibility.AccessibilityManager;
40import android.view.accessibility.AccessibilityNodeInfo;
Selim Cinek4e8b9ed2014-06-20 16:37:04 -070041import android.view.animation.AnimationUtils;
42import android.view.animation.Interpolator;
Phil Weaver385912e2017-02-10 10:06:56 -080043import android.widget.EditText;
Selim Cinek4e8b9ed2014-06-20 16:37:04 -070044
45import java.util.ArrayList;
46import java.util.Stack;
47
48/**
49 * A View similar to a textView which contains password text and can animate when the text is
50 * changed
51 */
52public class PasswordTextView extends View {
53
54 private static final float DOT_OVERSHOOT_FACTOR = 1.5f;
55 private static final long DOT_APPEAR_DURATION_OVERSHOOT = 320;
56 private static final long APPEAR_DURATION = 160;
57 private static final long DISAPPEAR_DURATION = 160;
58 private static final long RESET_DELAY_PER_ELEMENT = 40;
59 private static final long RESET_MAX_DELAY = 200;
60
61 /**
62 * The overlap between the text disappearing and the dot appearing animation
63 */
64 private static final long DOT_APPEAR_TEXT_DISAPPEAR_OVERLAP_DURATION = 130;
65
66 /**
67 * The duration the text needs to stay there at least before it can morph into a dot
68 */
69 private static final long TEXT_REST_DURATION_AFTER_APPEAR = 100;
70
71 /**
72 * The duration the text should be visible, starting with the appear animation
73 */
74 private static final long TEXT_VISIBILITY_DURATION = 1300;
75
76 /**
77 * The position in time from [0,1] where the overshoot should be finished and the settle back
78 * animation of the dot should start
79 */
80 private static final float OVERSHOOT_TIME_POSITION = 0.5f;
81
Phil Weaverc355c7a2017-12-20 10:54:13 -080082 private static char DOT = '\u2022';
83
Selim Cinek4e8b9ed2014-06-20 16:37:04 -070084 /**
85 * The raw text size, will be multiplied by the scaled density when drawn
86 */
87 private final int mTextHeightRaw;
Evan Rosky90427502016-04-01 16:04:23 -070088 private final int mGravity;
Selim Cinek4e8b9ed2014-06-20 16:37:04 -070089 private ArrayList<CharState> mTextChars = new ArrayList<>();
90 private String mText = "";
91 private Stack<CharState> mCharPool = new Stack<>();
92 private int mDotSize;
93 private PowerManager mPM;
94 private int mCharPadding;
95 private final Paint mDrawPaint = new Paint();
96 private Interpolator mAppearInterpolator;
97 private Interpolator mDisappearInterpolator;
98 private Interpolator mFastOutSlowInInterpolator;
99 private boolean mShowPassword;
Xiyuan Xia09eb0332015-05-13 15:29:42 -0700100 private UserActivityListener mUserActivityListener;
101
102 public interface UserActivityListener {
103 void onUserActivity();
104 }
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700105
106 public PasswordTextView(Context context) {
107 this(context, null);
108 }
109
110 public PasswordTextView(Context context, AttributeSet attrs) {
111 this(context, attrs, 0);
112 }
113
114 public PasswordTextView(Context context, AttributeSet attrs, int defStyleAttr) {
115 this(context, attrs, defStyleAttr, 0);
116 }
117
118 public PasswordTextView(Context context, AttributeSet attrs, int defStyleAttr,
119 int defStyleRes) {
120 super(context, attrs, defStyleAttr, defStyleRes);
121 setFocusableInTouchMode(true);
122 setFocusable(true);
123 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PasswordTextView);
124 try {
125 mTextHeightRaw = a.getInt(R.styleable.PasswordTextView_scaledTextSize, 0);
Evan Rosky90427502016-04-01 16:04:23 -0700126 mGravity = a.getInt(R.styleable.PasswordTextView_android_gravity, Gravity.CENTER);
127 mDotSize = a.getDimensionPixelSize(R.styleable.PasswordTextView_dotSize,
128 getContext().getResources().getDimensionPixelSize(R.dimen.password_dot_size));
129 mCharPadding = a.getDimensionPixelSize(R.styleable.PasswordTextView_charPadding,
130 getContext().getResources().getDimensionPixelSize(
131 R.dimen.password_char_padding));
Lucas Dupin987f1932017-05-13 21:02:52 -0700132 int textColor = a.getColor(R.styleable.PasswordTextView_android_textColor, Color.WHITE);
133 mDrawPaint.setColor(textColor);
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700134 } finally {
135 a.recycle();
136 }
137 mDrawPaint.setFlags(Paint.SUBPIXEL_TEXT_FLAG | Paint.ANTI_ALIAS_FLAG);
138 mDrawPaint.setTextAlign(Paint.Align.CENTER);
Andrew Sappersteinfec80922017-06-13 18:36:28 -0700139 mDrawPaint.setTypeface(Typeface.create(
Fabian Kozynski8a7a3342018-12-13 17:11:57 -0500140 context.getString(com.android.internal.R.string.config_headlineFontFamily),
Andrew Sappersteinfec80922017-06-13 18:36:28 -0700141 0));
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700142 mShowPassword = Settings.System.getInt(mContext.getContentResolver(),
143 Settings.System.TEXT_SHOW_PASSWORD, 1) == 1;
144 mAppearInterpolator = AnimationUtils.loadInterpolator(mContext,
145 android.R.interpolator.linear_out_slow_in);
146 mDisappearInterpolator = AnimationUtils.loadInterpolator(mContext,
147 android.R.interpolator.fast_out_linear_in);
148 mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(mContext,
149 android.R.interpolator.fast_out_slow_in);
150 mPM = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
151 }
152
153 @Override
154 protected void onDraw(Canvas canvas) {
155 float totalDrawingWidth = getDrawingWidth();
Evan Rosky90427502016-04-01 16:04:23 -0700156 float currentDrawPosition;
Evan Rosky22d12012016-04-07 14:17:13 -0700157 if ((mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.LEFT) {
158 if ((mGravity & Gravity.RELATIVE_LAYOUT_DIRECTION) != 0
159 && getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
Evan Rosky90427502016-04-01 16:04:23 -0700160 currentDrawPosition = getWidth() - getPaddingRight() - totalDrawingWidth;
161 } else {
162 currentDrawPosition = getPaddingLeft();
163 }
164 } else {
165 currentDrawPosition = getWidth() / 2 - totalDrawingWidth / 2;
166 }
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700167 int length = mTextChars.size();
168 Rect bounds = getCharBounds();
169 int charHeight = (bounds.bottom - bounds.top);
Evan Rosky90427502016-04-01 16:04:23 -0700170 float yPosition =
171 (getHeight() - getPaddingBottom() - getPaddingTop()) / 2 + getPaddingTop();
172 canvas.clipRect(getPaddingLeft(), getPaddingTop(),
Evan Rosky22d12012016-04-07 14:17:13 -0700173 getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700174 float charLength = bounds.right - bounds.left;
175 for (int i = 0; i < length; i++) {
176 CharState charState = mTextChars.get(i);
177 float charWidth = charState.draw(canvas, currentDrawPosition, charHeight, yPosition,
178 charLength);
179 currentDrawPosition += charWidth;
180 }
181 }
182
Selim Cinek63804822014-09-10 16:39:01 +0200183 @Override
184 public boolean hasOverlappingRendering() {
185 return false;
186 }
187
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700188 private Rect getCharBounds() {
189 float textHeight = mTextHeightRaw * getResources().getDisplayMetrics().scaledDensity;
190 mDrawPaint.setTextSize(textHeight);
191 Rect bounds = new Rect();
192 mDrawPaint.getTextBounds("0", 0, 1, bounds);
193 return bounds;
194 }
195
196 private float getDrawingWidth() {
197 int width = 0;
198 int length = mTextChars.size();
199 Rect bounds = getCharBounds();
200 int charLength = bounds.right - bounds.left;
201 for (int i = 0; i < length; i++) {
202 CharState charState = mTextChars.get(i);
203 if (i != 0) {
204 width += mCharPadding * charState.currentWidthFactor;
205 }
206 width += charLength * charState.currentWidthFactor;
207 }
208 return width;
209 }
210
211
212 public void append(char c) {
213 int visibleChars = mTextChars.size();
Phil Weaverc355c7a2017-12-20 10:54:13 -0800214 CharSequence textbefore = getTransformedText();
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700215 mText = mText + c;
216 int newLength = mText.length();
217 CharState charState;
218 if (newLength > visibleChars) {
219 charState = obtainCharState(c);
220 mTextChars.add(charState);
221 } else {
222 charState = mTextChars.get(newLength - 1);
223 charState.whichChar = c;
224 }
225 charState.startAppearAnimation();
226
227 // ensure that the previous element is being swapped
228 if (newLength > 1) {
229 CharState previousState = mTextChars.get(newLength - 2);
230 if (previousState.isDotSwapPending) {
231 previousState.swapToDotWhenAppearFinished();
232 }
233 }
234 userActivity();
Adrian Roosa48caee2015-01-15 19:57:51 +0100235 sendAccessibilityEventTypeViewTextChanged(textbefore, textbefore.length(), 0, 1);
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700236 }
237
Xiyuan Xia09eb0332015-05-13 15:29:42 -0700238 public void setUserActivityListener(UserActivityListener userActivitiListener) {
239 mUserActivityListener = userActivitiListener;
240 }
241
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700242 private void userActivity() {
243 mPM.userActivity(SystemClock.uptimeMillis(), false);
Xiyuan Xia09eb0332015-05-13 15:29:42 -0700244 if (mUserActivityListener != null) {
245 mUserActivityListener.onUserActivity();
246 }
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700247 }
248
249 public void deleteLastChar() {
250 int length = mText.length();
Phil Weaverc355c7a2017-12-20 10:54:13 -0800251 CharSequence textbefore = getTransformedText();
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700252 if (length > 0) {
253 mText = mText.substring(0, length - 1);
254 CharState charState = mTextChars.get(length - 1);
255 charState.startRemoveAnimation(0, 0);
Selim Cinek8a72b062017-06-22 15:24:11 -0700256 sendAccessibilityEventTypeViewTextChanged(textbefore, textbefore.length() - 1, 1, 0);
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700257 }
258 userActivity();
259 }
260
261 public String getText() {
262 return mText;
263 }
264
Phil Weaverc355c7a2017-12-20 10:54:13 -0800265 private CharSequence getTransformedText() {
266 int textLength = mTextChars.size();
267 StringBuilder stringBuilder = new StringBuilder(textLength);
268 for (int i = 0; i < textLength; i++) {
269 CharState charState = mTextChars.get(i);
270 // If the dot is disappearing, the character is disappearing entirely. Consider
271 // it gone.
272 if (charState.dotAnimator != null && !charState.dotAnimationIsGrowing) {
273 continue;
274 }
275 stringBuilder.append(charState.isCharVisibleForA11y() ? charState.whichChar : DOT);
276 }
277 return stringBuilder;
278 }
279
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700280 private CharState obtainCharState(char c) {
281 CharState charState;
282 if(mCharPool.isEmpty()) {
283 charState = new CharState();
284 } else {
285 charState = mCharPool.pop();
286 charState.reset();
287 }
288 charState.whichChar = c;
289 return charState;
290 }
291
Jim Miller4db942c2016-05-16 18:06:50 -0700292 public void reset(boolean animated, boolean announce) {
Phil Weaverc355c7a2017-12-20 10:54:13 -0800293 CharSequence textbefore = getTransformedText();
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700294 mText = "";
295 int length = mTextChars.size();
296 int middleIndex = (length - 1) / 2;
297 long delayPerElement = RESET_DELAY_PER_ELEMENT;
298 for (int i = 0; i < length; i++) {
299 CharState charState = mTextChars.get(i);
300 if (animated) {
301 int delayIndex;
302 if (i <= middleIndex) {
303 delayIndex = i * 2;
304 } else {
305 int distToMiddle = i - middleIndex;
306 delayIndex = (length - 1) - (distToMiddle - 1) * 2;
307 }
308 long startDelay = delayIndex * delayPerElement;
309 startDelay = Math.min(startDelay, RESET_MAX_DELAY);
310 long maxDelay = delayPerElement * (length - 1);
311 maxDelay = Math.min(maxDelay, RESET_MAX_DELAY) + DISAPPEAR_DURATION;
312 charState.startRemoveAnimation(startDelay, maxDelay);
313 charState.removeDotSwapCallbacks();
314 } else {
315 mCharPool.push(charState);
316 }
317 }
318 if (!animated) {
319 mTextChars.clear();
320 }
Jim Miller4db942c2016-05-16 18:06:50 -0700321 if (announce) {
322 sendAccessibilityEventTypeViewTextChanged(textbefore, 0, textbefore.length(), 0);
323 }
Adrian Roosa48caee2015-01-15 19:57:51 +0100324 }
325
Phil Weaverc355c7a2017-12-20 10:54:13 -0800326 void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText, int fromIndex,
Adrian Roosa48caee2015-01-15 19:57:51 +0100327 int removedCount, int addedCount) {
Eugene Suslad4128ec2017-12-04 19:48:41 +0000328 if (AccessibilityManager.getInstance(mContext).isEnabled() &&
329 (isFocused() || isSelected() && isShown())) {
Adrian Roosa48caee2015-01-15 19:57:51 +0100330 AccessibilityEvent event =
331 AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
332 event.setFromIndex(fromIndex);
333 event.setRemovedCount(removedCount);
334 event.setAddedCount(addedCount);
335 event.setBeforeText(beforeText);
Phil Weaverc355c7a2017-12-20 10:54:13 -0800336 CharSequence transformedText = getTransformedText();
337 if (!TextUtils.isEmpty(transformedText)) {
338 event.getText().add(transformedText);
339 }
Adrian Roosa48caee2015-01-15 19:57:51 +0100340 event.setPassword(true);
341 sendAccessibilityEventUnchecked(event);
342 }
343 }
344
345 @Override
346 public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
347 super.onInitializeAccessibilityEvent(event);
348
Phil Weaver385912e2017-02-10 10:06:56 -0800349 event.setClassName(EditText.class.getName());
Adrian Roosa48caee2015-01-15 19:57:51 +0100350 event.setPassword(true);
351 }
352
353 @Override
Adrian Roosa48caee2015-01-15 19:57:51 +0100354 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
355 super.onInitializeAccessibilityNodeInfo(info);
356
Phil Weaverc355c7a2017-12-20 10:54:13 -0800357 info.setClassName(EditText.class.getName());
Adrian Roosa48caee2015-01-15 19:57:51 +0100358 info.setPassword(true);
Phil Weaverc355c7a2017-12-20 10:54:13 -0800359 info.setText(getTransformedText());
Adrian Roosa48caee2015-01-15 19:57:51 +0100360
Adrian Roosa48caee2015-01-15 19:57:51 +0100361 info.setEditable(true);
362
363 info.setInputType(InputType.TYPE_NUMBER_VARIATION_PASSWORD);
364 }
365
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700366 private class CharState {
367 char whichChar;
368 ValueAnimator textAnimator;
369 boolean textAnimationIsGrowing;
370 Animator dotAnimator;
371 boolean dotAnimationIsGrowing;
372 ValueAnimator widthAnimator;
373 boolean widthAnimationIsGrowing;
374 float currentTextSizeFactor;
375 float currentDotSizeFactor;
376 float currentWidthFactor;
377 boolean isDotSwapPending;
378 float currentTextTranslationY = 1.0f;
379 ValueAnimator textTranslateAnimator;
380
381 Animator.AnimatorListener removeEndListener = new AnimatorListenerAdapter() {
382 private boolean mCancelled;
383 @Override
384 public void onAnimationCancel(Animator animation) {
385 mCancelled = true;
386 }
387
388 @Override
389 public void onAnimationEnd(Animator animation) {
390 if (!mCancelled) {
391 mTextChars.remove(CharState.this);
392 mCharPool.push(CharState.this);
393 reset();
394 cancelAnimator(textTranslateAnimator);
395 textTranslateAnimator = null;
396 }
397 }
398
399 @Override
400 public void onAnimationStart(Animator animation) {
401 mCancelled = false;
402 }
403 };
404
405 Animator.AnimatorListener dotFinishListener = new AnimatorListenerAdapter() {
406 @Override
407 public void onAnimationEnd(Animator animation) {
408 dotAnimator = null;
409 }
410 };
411
412 Animator.AnimatorListener textFinishListener = new AnimatorListenerAdapter() {
413 @Override
414 public void onAnimationEnd(Animator animation) {
415 textAnimator = null;
416 }
417 };
418
419 Animator.AnimatorListener textTranslateFinishListener = new AnimatorListenerAdapter() {
420 @Override
421 public void onAnimationEnd(Animator animation) {
422 textTranslateAnimator = null;
423 }
424 };
425
426 Animator.AnimatorListener widthFinishListener = new AnimatorListenerAdapter() {
427 @Override
428 public void onAnimationEnd(Animator animation) {
429 widthAnimator = null;
430 }
431 };
432
433 private ValueAnimator.AnimatorUpdateListener dotSizeUpdater
434 = new ValueAnimator.AnimatorUpdateListener() {
435 @Override
436 public void onAnimationUpdate(ValueAnimator animation) {
437 currentDotSizeFactor = (float) animation.getAnimatedValue();
438 invalidate();
439 }
440 };
441
442 private ValueAnimator.AnimatorUpdateListener textSizeUpdater
443 = new ValueAnimator.AnimatorUpdateListener() {
444 @Override
445 public void onAnimationUpdate(ValueAnimator animation) {
Phil Weaverc355c7a2017-12-20 10:54:13 -0800446 boolean textVisibleBefore = isCharVisibleForA11y();
447 float beforeTextSizeFactor = currentTextSizeFactor;
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700448 currentTextSizeFactor = (float) animation.getAnimatedValue();
Phil Weaverc355c7a2017-12-20 10:54:13 -0800449 if (textVisibleBefore != isCharVisibleForA11y()) {
450 currentTextSizeFactor = beforeTextSizeFactor;
451 CharSequence beforeText = getTransformedText();
452 currentTextSizeFactor = (float) animation.getAnimatedValue();
453 int indexOfThisChar = mTextChars.indexOf(CharState.this);
454 if (indexOfThisChar >= 0) {
455 sendAccessibilityEventTypeViewTextChanged(
456 beforeText, indexOfThisChar, 1, 1);
457 }
458 }
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700459 invalidate();
460 }
461 };
462
463 private ValueAnimator.AnimatorUpdateListener textTranslationUpdater
464 = new ValueAnimator.AnimatorUpdateListener() {
465 @Override
466 public void onAnimationUpdate(ValueAnimator animation) {
467 currentTextTranslationY = (float) animation.getAnimatedValue();
468 invalidate();
469 }
470 };
471
472 private ValueAnimator.AnimatorUpdateListener widthUpdater
473 = new ValueAnimator.AnimatorUpdateListener() {
474 @Override
475 public void onAnimationUpdate(ValueAnimator animation) {
476 currentWidthFactor = (float) animation.getAnimatedValue();
477 invalidate();
478 }
479 };
480
481 private Runnable dotSwapperRunnable = new Runnable() {
482 @Override
483 public void run() {
484 performSwap();
485 isDotSwapPending = false;
486 }
487 };
488
489 void reset() {
490 whichChar = 0;
491 currentTextSizeFactor = 0.0f;
492 currentDotSizeFactor = 0.0f;
493 currentWidthFactor = 0.0f;
494 cancelAnimator(textAnimator);
495 textAnimator = null;
496 cancelAnimator(dotAnimator);
497 dotAnimator = null;
498 cancelAnimator(widthAnimator);
499 widthAnimator = null;
500 currentTextTranslationY = 1.0f;
501 removeDotSwapCallbacks();
502 }
503
504 void startRemoveAnimation(long startDelay, long widthDelay) {
505 boolean dotNeedsAnimation = (currentDotSizeFactor > 0.0f && dotAnimator == null)
506 || (dotAnimator != null && dotAnimationIsGrowing);
507 boolean textNeedsAnimation = (currentTextSizeFactor > 0.0f && textAnimator == null)
508 || (textAnimator != null && textAnimationIsGrowing);
509 boolean widthNeedsAnimation = (currentWidthFactor > 0.0f && widthAnimator == null)
510 || (widthAnimator != null && widthAnimationIsGrowing);
511 if (dotNeedsAnimation) {
512 startDotDisappearAnimation(startDelay);
513 }
514 if (textNeedsAnimation) {
515 startTextDisappearAnimation(startDelay);
516 }
517 if (widthNeedsAnimation) {
518 startWidthDisappearAnimation(widthDelay);
519 }
520 }
521
522 void startAppearAnimation() {
523 boolean dotNeedsAnimation = !mShowPassword
524 && (dotAnimator == null || !dotAnimationIsGrowing);
525 boolean textNeedsAnimation = mShowPassword
526 && (textAnimator == null || !textAnimationIsGrowing);
527 boolean widthNeedsAnimation = (widthAnimator == null || !widthAnimationIsGrowing);
528 if (dotNeedsAnimation) {
529 startDotAppearAnimation(0);
530 }
531 if (textNeedsAnimation) {
532 startTextAppearAnimation();
533 }
534 if (widthNeedsAnimation) {
535 startWidthAppearAnimation();
536 }
537 if (mShowPassword) {
538 postDotSwap(TEXT_VISIBILITY_DURATION);
539 }
540 }
541
542 /**
543 * Posts a runnable which ensures that the text will be replaced by a dot after {@link
544 * com.android.keyguard.PasswordTextView#TEXT_VISIBILITY_DURATION}.
545 */
546 private void postDotSwap(long delay) {
547 removeDotSwapCallbacks();
548 postDelayed(dotSwapperRunnable, delay);
549 isDotSwapPending = true;
550 }
551
552 private void removeDotSwapCallbacks() {
553 removeCallbacks(dotSwapperRunnable);
554 isDotSwapPending = false;
555 }
556
557 void swapToDotWhenAppearFinished() {
558 removeDotSwapCallbacks();
559 if (textAnimator != null) {
560 long remainingDuration = textAnimator.getDuration()
561 - textAnimator.getCurrentPlayTime();
562 postDotSwap(remainingDuration + TEXT_REST_DURATION_AFTER_APPEAR);
563 } else {
564 performSwap();
565 }
566 }
567
568 private void performSwap() {
569 startTextDisappearAnimation(0);
570 startDotAppearAnimation(DISAPPEAR_DURATION
571 - DOT_APPEAR_TEXT_DISAPPEAR_OVERLAP_DURATION);
572 }
573
574 private void startWidthDisappearAnimation(long widthDelay) {
575 cancelAnimator(widthAnimator);
576 widthAnimator = ValueAnimator.ofFloat(currentWidthFactor, 0.0f);
577 widthAnimator.addUpdateListener(widthUpdater);
578 widthAnimator.addListener(widthFinishListener);
579 widthAnimator.addListener(removeEndListener);
580 widthAnimator.setDuration((long) (DISAPPEAR_DURATION * currentWidthFactor));
581 widthAnimator.setStartDelay(widthDelay);
582 widthAnimator.start();
583 widthAnimationIsGrowing = false;
584 }
585
586 private void startTextDisappearAnimation(long startDelay) {
587 cancelAnimator(textAnimator);
588 textAnimator = ValueAnimator.ofFloat(currentTextSizeFactor, 0.0f);
589 textAnimator.addUpdateListener(textSizeUpdater);
590 textAnimator.addListener(textFinishListener);
591 textAnimator.setInterpolator(mDisappearInterpolator);
592 textAnimator.setDuration((long) (DISAPPEAR_DURATION * currentTextSizeFactor));
593 textAnimator.setStartDelay(startDelay);
594 textAnimator.start();
595 textAnimationIsGrowing = false;
596 }
597
598 private void startDotDisappearAnimation(long startDelay) {
599 cancelAnimator(dotAnimator);
600 ValueAnimator animator = ValueAnimator.ofFloat(currentDotSizeFactor, 0.0f);
601 animator.addUpdateListener(dotSizeUpdater);
602 animator.addListener(dotFinishListener);
603 animator.setInterpolator(mDisappearInterpolator);
604 long duration = (long) (DISAPPEAR_DURATION * Math.min(currentDotSizeFactor, 1.0f));
605 animator.setDuration(duration);
606 animator.setStartDelay(startDelay);
607 animator.start();
608 dotAnimator = animator;
609 dotAnimationIsGrowing = false;
610 }
611
612 private void startWidthAppearAnimation() {
613 cancelAnimator(widthAnimator);
614 widthAnimator = ValueAnimator.ofFloat(currentWidthFactor, 1.0f);
615 widthAnimator.addUpdateListener(widthUpdater);
616 widthAnimator.addListener(widthFinishListener);
617 widthAnimator.setDuration((long) (APPEAR_DURATION * (1f - currentWidthFactor)));
618 widthAnimator.start();
619 widthAnimationIsGrowing = true;
620 }
621
622 private void startTextAppearAnimation() {
623 cancelAnimator(textAnimator);
624 textAnimator = ValueAnimator.ofFloat(currentTextSizeFactor, 1.0f);
625 textAnimator.addUpdateListener(textSizeUpdater);
626 textAnimator.addListener(textFinishListener);
627 textAnimator.setInterpolator(mAppearInterpolator);
628 textAnimator.setDuration((long) (APPEAR_DURATION * (1f - currentTextSizeFactor)));
629 textAnimator.start();
630 textAnimationIsGrowing = true;
631
632 // handle translation
633 if (textTranslateAnimator == null) {
634 textTranslateAnimator = ValueAnimator.ofFloat(1.0f, 0.0f);
635 textTranslateAnimator.addUpdateListener(textTranslationUpdater);
636 textTranslateAnimator.addListener(textTranslateFinishListener);
637 textTranslateAnimator.setInterpolator(mAppearInterpolator);
638 textTranslateAnimator.setDuration(APPEAR_DURATION);
639 textTranslateAnimator.start();
640 }
641 }
642
643 private void startDotAppearAnimation(long delay) {
644 cancelAnimator(dotAnimator);
645 if (!mShowPassword) {
646 // We perform an overshoot animation
647 ValueAnimator overShootAnimator = ValueAnimator.ofFloat(currentDotSizeFactor,
648 DOT_OVERSHOOT_FACTOR);
649 overShootAnimator.addUpdateListener(dotSizeUpdater);
650 overShootAnimator.setInterpolator(mAppearInterpolator);
651 long overShootDuration = (long) (DOT_APPEAR_DURATION_OVERSHOOT
652 * OVERSHOOT_TIME_POSITION);
653 overShootAnimator.setDuration(overShootDuration);
654 ValueAnimator settleBackAnimator = ValueAnimator.ofFloat(DOT_OVERSHOOT_FACTOR,
655 1.0f);
656 settleBackAnimator.addUpdateListener(dotSizeUpdater);
657 settleBackAnimator.setDuration(DOT_APPEAR_DURATION_OVERSHOOT - overShootDuration);
658 settleBackAnimator.addListener(dotFinishListener);
659 AnimatorSet animatorSet = new AnimatorSet();
660 animatorSet.playSequentially(overShootAnimator, settleBackAnimator);
661 animatorSet.setStartDelay(delay);
662 animatorSet.start();
663 dotAnimator = animatorSet;
664 } else {
665 ValueAnimator growAnimator = ValueAnimator.ofFloat(currentDotSizeFactor, 1.0f);
666 growAnimator.addUpdateListener(dotSizeUpdater);
667 growAnimator.setDuration((long) (APPEAR_DURATION * (1.0f - currentDotSizeFactor)));
668 growAnimator.addListener(dotFinishListener);
669 growAnimator.setStartDelay(delay);
670 growAnimator.start();
671 dotAnimator = growAnimator;
672 }
673 dotAnimationIsGrowing = true;
674 }
675
676 private void cancelAnimator(Animator animator) {
677 if (animator != null) {
678 animator.cancel();
679 }
680 }
681
682 /**
683 * Draw this char to the canvas.
684 *
685 * @return The width this character contributes, including padding.
686 */
687 public float draw(Canvas canvas, float currentDrawPosition, int charHeight, float yPosition,
688 float charLength) {
689 boolean textVisible = currentTextSizeFactor > 0;
690 boolean dotVisible = currentDotSizeFactor > 0;
691 float charWidth = charLength * currentWidthFactor;
692 if (textVisible) {
693 float currYPosition = yPosition + charHeight / 2.0f * currentTextSizeFactor
694 + charHeight * currentTextTranslationY * 0.8f;
695 canvas.save();
696 float centerX = currentDrawPosition + charWidth / 2;
697 canvas.translate(centerX, currYPosition);
698 canvas.scale(currentTextSizeFactor, currentTextSizeFactor);
699 canvas.drawText(Character.toString(whichChar), 0, 0, mDrawPaint);
700 canvas.restore();
701 }
702 if (dotVisible) {
703 canvas.save();
704 float centerX = currentDrawPosition + charWidth / 2;
705 canvas.translate(centerX, yPosition);
706 canvas.drawCircle(0, 0, mDotSize / 2 * currentDotSizeFactor, mDrawPaint);
707 canvas.restore();
708 }
709 return charWidth + mCharPadding * currentWidthFactor;
710 }
Phil Weaverc355c7a2017-12-20 10:54:13 -0800711
712 public boolean isCharVisibleForA11y() {
713 // The text has size 0 when it is first added, but we want to count it as visible if
714 // it will become visible presently. Count text as visible if an animator
715 // is configured to make it grow.
716 boolean textIsGrowing = textAnimator != null && textAnimationIsGrowing;
717 return (currentTextSizeFactor > 0) || textIsGrowing;
718 }
Selim Cinek4e8b9ed2014-06-20 16:37:04 -0700719 }
720}