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