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