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