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.systemui.statusbar;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.ArgbEvaluator;
22 import android.animation.PropertyValuesHolder;
23 import android.animation.ValueAnimator;
24 import android.annotation.Nullable;
25 import android.content.Context;
26 import android.content.res.TypedArray;
27 import android.graphics.Canvas;
28 import android.graphics.CanvasProperty;
29 import android.graphics.Color;
30 import android.graphics.Paint;
31 import android.graphics.PorterDuff;
32 import android.graphics.RecordingCanvas;
33 import android.graphics.drawable.Drawable;
34 import android.util.AttributeSet;
35 import android.view.RenderNodeAnimator;
36 import android.view.View;
37 import android.view.ViewAnimationUtils;
38 import android.view.animation.Interpolator;
39 import android.widget.ImageView;
40 
41 import com.android.systemui.Interpolators;
42 import com.android.systemui.R;
43 
44 /**
45  * An ImageView which does not have overlapping renderings commands and therefore does not need a
46  * layer when alpha is changed.
47  */
48 public class KeyguardAffordanceView extends ImageView {
49 
50     private static final long CIRCLE_APPEAR_DURATION = 80;
51     private static final long CIRCLE_DISAPPEAR_MAX_DURATION = 200;
52     private static final long NORMAL_ANIMATION_DURATION = 200;
53     public static final float MAX_ICON_SCALE_AMOUNT = 1.5f;
54     public static final float MIN_ICON_SCALE_AMOUNT = 0.8f;
55 
56     protected final int mDarkIconColor;
57     protected final int mNormalColor;
58     private final int mMinBackgroundRadius;
59     private final Paint mCirclePaint;
60     private final ArgbEvaluator mColorInterpolator;
61     private final FlingAnimationUtils mFlingAnimationUtils;
62     private float mCircleRadius;
63     private int mCenterX;
64     private int mCenterY;
65     private ValueAnimator mCircleAnimator;
66     private ValueAnimator mAlphaAnimator;
67     private ValueAnimator mScaleAnimator;
68     private float mCircleStartValue;
69     private boolean mCircleWillBeHidden;
70     private int[] mTempPoint = new int[2];
71     private float mImageScale = 1f;
72     private int mCircleColor;
73     private boolean mIsLeft;
74     private View mPreviewView;
75     private float mCircleStartRadius;
76     private float mMaxCircleSize;
77     private Animator mPreviewClipper;
78     private float mRestingAlpha = 1f;
79     private boolean mSupportHardware;
80     private boolean mFinishing;
81     private boolean mLaunchingAffordance;
82     private boolean mShouldTint = true;
83 
84     private CanvasProperty<Float> mHwCircleRadius;
85     private CanvasProperty<Float> mHwCenterX;
86     private CanvasProperty<Float> mHwCenterY;
87     private CanvasProperty<Paint> mHwCirclePaint;
88 
89     private AnimatorListenerAdapter mClipEndListener = new AnimatorListenerAdapter() {
90         @Override
91         public void onAnimationEnd(Animator animation) {
92             mPreviewClipper = null;
93         }
94     };
95     private AnimatorListenerAdapter mCircleEndListener = new AnimatorListenerAdapter() {
96         @Override
97         public void onAnimationEnd(Animator animation) {
98             mCircleAnimator = null;
99         }
100     };
101     private AnimatorListenerAdapter mScaleEndListener = new AnimatorListenerAdapter() {
102         @Override
103         public void onAnimationEnd(Animator animation) {
104             mScaleAnimator = null;
105         }
106     };
107     private AnimatorListenerAdapter mAlphaEndListener = new AnimatorListenerAdapter() {
108         @Override
109         public void onAnimationEnd(Animator animation) {
110             mAlphaAnimator = null;
111         }
112     };
113 
KeyguardAffordanceView(Context context)114     public KeyguardAffordanceView(Context context) {
115         this(context, null);
116     }
117 
KeyguardAffordanceView(Context context, AttributeSet attrs)118     public KeyguardAffordanceView(Context context, AttributeSet attrs) {
119         this(context, attrs, 0);
120     }
121 
KeyguardAffordanceView(Context context, AttributeSet attrs, int defStyleAttr)122     public KeyguardAffordanceView(Context context, AttributeSet attrs, int defStyleAttr) {
123         this(context, attrs, defStyleAttr, 0);
124     }
125 
KeyguardAffordanceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)126     public KeyguardAffordanceView(Context context, AttributeSet attrs, int defStyleAttr,
127             int defStyleRes) {
128         super(context, attrs, defStyleAttr, defStyleRes);
129         TypedArray a = context.obtainStyledAttributes(attrs, android.R.styleable.ImageView);
130 
131         mCirclePaint = new Paint();
132         mCirclePaint.setAntiAlias(true);
133         mCircleColor = 0xffffffff;
134         mCirclePaint.setColor(mCircleColor);
135 
136         mNormalColor = a.getColor(android.R.styleable.ImageView_tint, 0xffffffff);
137         mDarkIconColor = 0xff000000;
138         mMinBackgroundRadius = mContext.getResources().getDimensionPixelSize(
139                 R.dimen.keyguard_affordance_min_background_radius);
140         mColorInterpolator = new ArgbEvaluator();
141         mFlingAnimationUtils = new FlingAnimationUtils(mContext.getResources().getDisplayMetrics(),
142                 0.3f);
143 
144         a.recycle();
145     }
146 
setImageDrawable(@ullable Drawable drawable, boolean tint)147     public void setImageDrawable(@Nullable Drawable drawable, boolean tint) {
148         super.setImageDrawable(drawable);
149         mShouldTint = tint;
150         updateIconColor();
151     }
152 
153     /**
154      * If current drawable should be tinted.
155      */
shouldTint()156     public boolean shouldTint() {
157         return mShouldTint;
158     }
159 
160     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)161     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
162         super.onLayout(changed, left, top, right, bottom);
163         mCenterX = getWidth() / 2;
164         mCenterY = getHeight() / 2;
165         mMaxCircleSize = getMaxCircleSize();
166     }
167 
168     @Override
onDraw(Canvas canvas)169     protected void onDraw(Canvas canvas) {
170         mSupportHardware = canvas.isHardwareAccelerated();
171         drawBackgroundCircle(canvas);
172         canvas.save();
173         canvas.scale(mImageScale, mImageScale, getWidth() / 2, getHeight() / 2);
174         super.onDraw(canvas);
175         canvas.restore();
176     }
177 
setPreviewView(View v)178     public void setPreviewView(View v) {
179         if (mPreviewView == v) {
180             return;
181         }
182         View oldPreviewView = mPreviewView;
183         mPreviewView = v;
184         if (mPreviewView != null) {
185             mPreviewView.setVisibility(mLaunchingAffordance
186                     ? oldPreviewView.getVisibility() : INVISIBLE);
187         }
188     }
189 
updateIconColor()190     private void updateIconColor() {
191         if (!mShouldTint) return;
192         Drawable drawable = getDrawable().mutate();
193         float alpha = mCircleRadius / mMinBackgroundRadius;
194         alpha = Math.min(1.0f, alpha);
195         int color = (int) mColorInterpolator.evaluate(alpha, mNormalColor, mDarkIconColor);
196         drawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
197     }
198 
drawBackgroundCircle(Canvas canvas)199     private void drawBackgroundCircle(Canvas canvas) {
200         if (mCircleRadius > 0 || mFinishing) {
201             if (mFinishing && mSupportHardware && mHwCenterX != null) {
202                 // Our hardware drawing proparties can be null if the finishing started but we have
203                 // never drawn before. In that case we are not doing a render thread animation
204                 // anyway, so we need to use the normal drawing.
205                 RecordingCanvas recordingCanvas = (RecordingCanvas) canvas;
206                 recordingCanvas.drawCircle(mHwCenterX, mHwCenterY, mHwCircleRadius,
207                         mHwCirclePaint);
208             } else {
209                 updateCircleColor();
210                 canvas.drawCircle(mCenterX, mCenterY, mCircleRadius, mCirclePaint);
211             }
212         }
213     }
214 
updateCircleColor()215     private void updateCircleColor() {
216         float fraction = 0.5f + 0.5f * Math.max(0.0f, Math.min(1.0f,
217                 (mCircleRadius - mMinBackgroundRadius) / (0.5f * mMinBackgroundRadius)));
218         if (mPreviewView != null && mPreviewView.getVisibility() == VISIBLE) {
219             float finishingFraction = 1 - Math.max(0, mCircleRadius - mCircleStartRadius)
220                     / (mMaxCircleSize - mCircleStartRadius);
221             fraction *= finishingFraction;
222         }
223         int color = Color.argb((int) (Color.alpha(mCircleColor) * fraction),
224                 Color.red(mCircleColor),
225                 Color.green(mCircleColor), Color.blue(mCircleColor));
226         mCirclePaint.setColor(color);
227     }
228 
finishAnimation(float velocity, final Runnable mAnimationEndRunnable)229     public void finishAnimation(float velocity, final Runnable mAnimationEndRunnable) {
230         cancelAnimator(mCircleAnimator);
231         cancelAnimator(mPreviewClipper);
232         mFinishing = true;
233         mCircleStartRadius = mCircleRadius;
234         final float maxCircleSize = getMaxCircleSize();
235         Animator animatorToRadius;
236         if (mSupportHardware) {
237             initHwProperties();
238             animatorToRadius = getRtAnimatorToRadius(maxCircleSize);
239             startRtAlphaFadeIn();
240         } else {
241             animatorToRadius = getAnimatorToRadius(maxCircleSize);
242         }
243         mFlingAnimationUtils.applyDismissing(animatorToRadius, mCircleRadius, maxCircleSize,
244                 velocity, maxCircleSize);
245         animatorToRadius.addListener(new AnimatorListenerAdapter() {
246             @Override
247             public void onAnimationEnd(Animator animation) {
248                 mAnimationEndRunnable.run();
249                 mFinishing = false;
250                 mCircleRadius = maxCircleSize;
251                 invalidate();
252             }
253         });
254         animatorToRadius.start();
255         setImageAlpha(0, true);
256         if (mPreviewView != null) {
257             mPreviewView.setVisibility(View.VISIBLE);
258             mPreviewClipper = ViewAnimationUtils.createCircularReveal(
259                     mPreviewView, getLeft() + mCenterX, getTop() + mCenterY, mCircleRadius,
260                     maxCircleSize);
261             mFlingAnimationUtils.applyDismissing(mPreviewClipper, mCircleRadius, maxCircleSize,
262                     velocity, maxCircleSize);
263             mPreviewClipper.addListener(mClipEndListener);
264             mPreviewClipper.start();
265             if (mSupportHardware) {
266                 startRtCircleFadeOut(animatorToRadius.getDuration());
267             }
268         }
269     }
270 
271     /**
272      * Fades in the Circle on the RenderThread. It's used when finishing the circle when it had
273      * alpha 0 in the beginning.
274      */
startRtAlphaFadeIn()275     private void startRtAlphaFadeIn() {
276         if (mCircleRadius == 0 && mPreviewView == null) {
277             Paint modifiedPaint = new Paint(mCirclePaint);
278             modifiedPaint.setColor(mCircleColor);
279             modifiedPaint.setAlpha(0);
280             mHwCirclePaint = CanvasProperty.createPaint(modifiedPaint);
281             RenderNodeAnimator animator = new RenderNodeAnimator(mHwCirclePaint,
282                     RenderNodeAnimator.PAINT_ALPHA, 255);
283             animator.setTarget(this);
284             animator.setInterpolator(Interpolators.ALPHA_IN);
285             animator.setDuration(250);
286             animator.start();
287         }
288     }
289 
instantFinishAnimation()290     public void instantFinishAnimation() {
291         cancelAnimator(mPreviewClipper);
292         if (mPreviewView != null) {
293             mPreviewView.setClipBounds(null);
294             mPreviewView.setVisibility(View.VISIBLE);
295         }
296         mCircleRadius = getMaxCircleSize();
297         setImageAlpha(0, false);
298         invalidate();
299     }
300 
startRtCircleFadeOut(long duration)301     private void startRtCircleFadeOut(long duration) {
302         RenderNodeAnimator animator = new RenderNodeAnimator(mHwCirclePaint,
303                 RenderNodeAnimator.PAINT_ALPHA, 0);
304         animator.setDuration(duration);
305         animator.setInterpolator(Interpolators.ALPHA_OUT);
306         animator.setTarget(this);
307         animator.start();
308     }
309 
getRtAnimatorToRadius(float circleRadius)310     private Animator getRtAnimatorToRadius(float circleRadius) {
311         RenderNodeAnimator animator = new RenderNodeAnimator(mHwCircleRadius, circleRadius);
312         animator.setTarget(this);
313         return animator;
314     }
315 
initHwProperties()316     private void initHwProperties() {
317         mHwCenterX = CanvasProperty.createFloat(mCenterX);
318         mHwCenterY = CanvasProperty.createFloat(mCenterY);
319         mHwCirclePaint = CanvasProperty.createPaint(mCirclePaint);
320         mHwCircleRadius = CanvasProperty.createFloat(mCircleRadius);
321     }
322 
getMaxCircleSize()323     private float getMaxCircleSize() {
324         getLocationInWindow(mTempPoint);
325         float rootWidth = getRootView().getWidth();
326         float width = mTempPoint[0] + mCenterX;
327         width = Math.max(rootWidth - width, width);
328         float height = mTempPoint[1] + mCenterY;
329         return (float) Math.hypot(width, height);
330     }
331 
setCircleRadius(float circleRadius)332     public void setCircleRadius(float circleRadius) {
333         setCircleRadius(circleRadius, false, false);
334     }
335 
setCircleRadius(float circleRadius, boolean slowAnimation)336     public void setCircleRadius(float circleRadius, boolean slowAnimation) {
337         setCircleRadius(circleRadius, slowAnimation, false);
338     }
339 
setCircleRadiusWithoutAnimation(float circleRadius)340     public void setCircleRadiusWithoutAnimation(float circleRadius) {
341         cancelAnimator(mCircleAnimator);
342         setCircleRadius(circleRadius, false ,true);
343     }
344 
setCircleRadius(float circleRadius, boolean slowAnimation, boolean noAnimation)345     private void setCircleRadius(float circleRadius, boolean slowAnimation, boolean noAnimation) {
346 
347         // Check if we need a new animation
348         boolean radiusHidden = (mCircleAnimator != null && mCircleWillBeHidden)
349                 || (mCircleAnimator == null && mCircleRadius == 0.0f);
350         boolean nowHidden = circleRadius == 0.0f;
351         boolean radiusNeedsAnimation = (radiusHidden != nowHidden) && !noAnimation;
352         if (!radiusNeedsAnimation) {
353             if (mCircleAnimator == null) {
354                 mCircleRadius = circleRadius;
355                 updateIconColor();
356                 invalidate();
357                 if (nowHidden) {
358                     if (mPreviewView != null) {
359                         mPreviewView.setVisibility(View.INVISIBLE);
360                     }
361                 }
362             } else if (!mCircleWillBeHidden) {
363 
364                 // We just update the end value
365                 float diff = circleRadius - mMinBackgroundRadius;
366                 PropertyValuesHolder[] values = mCircleAnimator.getValues();
367                 values[0].setFloatValues(mCircleStartValue + diff, circleRadius);
368                 mCircleAnimator.setCurrentPlayTime(mCircleAnimator.getCurrentPlayTime());
369             }
370         } else {
371             cancelAnimator(mCircleAnimator);
372             cancelAnimator(mPreviewClipper);
373             ValueAnimator animator = getAnimatorToRadius(circleRadius);
374             Interpolator interpolator = circleRadius == 0.0f
375                     ? Interpolators.FAST_OUT_LINEAR_IN
376                     : Interpolators.LINEAR_OUT_SLOW_IN;
377             animator.setInterpolator(interpolator);
378             long duration = 250;
379             if (!slowAnimation) {
380                 float durationFactor = Math.abs(mCircleRadius - circleRadius)
381                         / (float) mMinBackgroundRadius;
382                 duration = (long) (CIRCLE_APPEAR_DURATION * durationFactor);
383                 duration = Math.min(duration, CIRCLE_DISAPPEAR_MAX_DURATION);
384             }
385             animator.setDuration(duration);
386             animator.start();
387             if (mPreviewView != null && mPreviewView.getVisibility() == View.VISIBLE) {
388                 mPreviewView.setVisibility(View.VISIBLE);
389                 mPreviewClipper = ViewAnimationUtils.createCircularReveal(
390                         mPreviewView, getLeft() + mCenterX, getTop() + mCenterY, mCircleRadius,
391                         circleRadius);
392                 mPreviewClipper.setInterpolator(interpolator);
393                 mPreviewClipper.setDuration(duration);
394                 mPreviewClipper.addListener(mClipEndListener);
395                 mPreviewClipper.addListener(new AnimatorListenerAdapter() {
396                     @Override
397                     public void onAnimationEnd(Animator animation) {
398                         mPreviewView.setVisibility(View.INVISIBLE);
399                     }
400                 });
401                 mPreviewClipper.start();
402             }
403         }
404     }
405 
getAnimatorToRadius(float circleRadius)406     private ValueAnimator getAnimatorToRadius(float circleRadius) {
407         ValueAnimator animator = ValueAnimator.ofFloat(mCircleRadius, circleRadius);
408         mCircleAnimator = animator;
409         mCircleStartValue = mCircleRadius;
410         mCircleWillBeHidden = circleRadius == 0.0f;
411         animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
412             @Override
413             public void onAnimationUpdate(ValueAnimator animation) {
414                 mCircleRadius = (float) animation.getAnimatedValue();
415                 updateIconColor();
416                 invalidate();
417             }
418         });
419         animator.addListener(mCircleEndListener);
420         return animator;
421     }
422 
cancelAnimator(Animator animator)423     private void cancelAnimator(Animator animator) {
424         if (animator != null) {
425             animator.cancel();
426         }
427     }
428 
setImageScale(float imageScale, boolean animate)429     public void setImageScale(float imageScale, boolean animate) {
430         setImageScale(imageScale, animate, -1, null);
431     }
432 
433     /**
434      * Sets the scale of the containing image
435      *
436      * @param imageScale The new Scale.
437      * @param animate Should an animation be performed
438      * @param duration If animate, whats the duration? When -1 we take the default duration
439      * @param interpolator If animate, whats the interpolator? When null we take the default
440      *                     interpolator.
441      */
setImageScale(float imageScale, boolean animate, long duration, Interpolator interpolator)442     public void setImageScale(float imageScale, boolean animate, long duration,
443             Interpolator interpolator) {
444         cancelAnimator(mScaleAnimator);
445         if (!animate) {
446             mImageScale = imageScale;
447             invalidate();
448         } else {
449             ValueAnimator animator = ValueAnimator.ofFloat(mImageScale, imageScale);
450             mScaleAnimator = animator;
451             animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
452                 @Override
453                 public void onAnimationUpdate(ValueAnimator animation) {
454                     mImageScale = (float) animation.getAnimatedValue();
455                     invalidate();
456                 }
457             });
458             animator.addListener(mScaleEndListener);
459             if (interpolator == null) {
460                 interpolator = imageScale == 0.0f
461                         ? Interpolators.FAST_OUT_LINEAR_IN
462                         : Interpolators.LINEAR_OUT_SLOW_IN;
463             }
464             animator.setInterpolator(interpolator);
465             if (duration == -1) {
466                 float durationFactor = Math.abs(mImageScale - imageScale)
467                         / (1.0f - MIN_ICON_SCALE_AMOUNT);
468                 durationFactor = Math.min(1.0f, durationFactor);
469                 duration = (long) (NORMAL_ANIMATION_DURATION * durationFactor);
470             }
471             animator.setDuration(duration);
472             animator.start();
473         }
474     }
475 
getRestingAlpha()476     public float getRestingAlpha() {
477         return mRestingAlpha;
478     }
479 
setImageAlpha(float alpha, boolean animate)480     public void setImageAlpha(float alpha, boolean animate) {
481         setImageAlpha(alpha, animate, -1, null, null);
482     }
483 
484     /**
485      * Sets the alpha of the containing image
486      *
487      * @param alpha The new alpha.
488      * @param animate Should an animation be performed
489      * @param duration If animate, whats the duration? When -1 we take the default duration
490      * @param interpolator If animate, whats the interpolator? When null we take the default
491      *                     interpolator.
492      */
setImageAlpha(float alpha, boolean animate, long duration, Interpolator interpolator, Runnable runnable)493     public void setImageAlpha(float alpha, boolean animate, long duration,
494             Interpolator interpolator, Runnable runnable) {
495         cancelAnimator(mAlphaAnimator);
496         alpha = mLaunchingAffordance ? 0 : alpha;
497         int endAlpha = (int) (alpha * 255);
498         final Drawable background = getBackground();
499         if (!animate) {
500             if (background != null) background.mutate().setAlpha(endAlpha);
501             setImageAlpha(endAlpha);
502         } else {
503             int currentAlpha = getImageAlpha();
504             ValueAnimator animator = ValueAnimator.ofInt(currentAlpha, endAlpha);
505             mAlphaAnimator = animator;
506             animator.addUpdateListener(animation -> {
507                 int alpha1 = (int) animation.getAnimatedValue();
508                 if (background != null) background.mutate().setAlpha(alpha1);
509                 setImageAlpha(alpha1);
510             });
511             animator.addListener(mAlphaEndListener);
512             if (interpolator == null) {
513                 interpolator = alpha == 0.0f
514                         ? Interpolators.FAST_OUT_LINEAR_IN
515                         : Interpolators.LINEAR_OUT_SLOW_IN;
516             }
517             animator.setInterpolator(interpolator);
518             if (duration == -1) {
519                 float durationFactor = Math.abs(currentAlpha - endAlpha) / 255f;
520                 durationFactor = Math.min(1.0f, durationFactor);
521                 duration = (long) (NORMAL_ANIMATION_DURATION * durationFactor);
522             }
523             animator.setDuration(duration);
524             if (runnable != null) {
525                 animator.addListener(getEndListener(runnable));
526             }
527             animator.start();
528         }
529     }
530 
isAnimatingAlpha()531     public boolean isAnimatingAlpha() {
532         return mAlphaAnimator != null;
533     }
534 
getEndListener(final Runnable runnable)535     private Animator.AnimatorListener getEndListener(final Runnable runnable) {
536         return new AnimatorListenerAdapter() {
537             boolean mCancelled;
538             @Override
539             public void onAnimationCancel(Animator animation) {
540                 mCancelled = true;
541             }
542 
543             @Override
544             public void onAnimationEnd(Animator animation) {
545                 if (!mCancelled) {
546                     runnable.run();
547                 }
548             }
549         };
550     }
551 
getCircleRadius()552     public float getCircleRadius() {
553         return mCircleRadius;
554     }
555 
556     @Override
performClick()557     public boolean performClick() {
558         if (isClickable()) {
559             return super.performClick();
560         } else {
561             return false;
562         }
563     }
564 
setLaunchingAffordance(boolean launchingAffordance)565     public void setLaunchingAffordance(boolean launchingAffordance) {
566         mLaunchingAffordance = launchingAffordance;
567     }
568 }
569