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