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