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
17package com.android.systemui.statusbar;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ArgbEvaluator;
22import android.animation.PropertyValuesHolder;
23import android.animation.ValueAnimator;
24import android.annotation.Nullable;
25import android.content.Context;
26import android.content.res.TypedArray;
27import android.graphics.Canvas;
28import android.graphics.CanvasProperty;
29import android.graphics.Color;
30import android.graphics.Paint;
31import android.graphics.PorterDuff;
32import android.graphics.drawable.Drawable;
33import android.util.AttributeSet;
34import android.view.DisplayListCanvas;
35import android.view.RenderNodeAnimator;
36import android.view.View;
37import android.view.ViewAnimationUtils;
38import android.view.animation.Interpolator;
39import android.widget.ImageView;
40
41import com.android.systemui.Interpolators;
42import com.android.systemui.R;
43import 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 */
49public 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
115    public KeyguardAffordanceView(Context context) {
116        this(context, null);
117    }
118
119    public KeyguardAffordanceView(Context context, AttributeSet attrs) {
120        this(context, attrs, 0);
121    }
122
123    public KeyguardAffordanceView(Context context, AttributeSet attrs, int defStyleAttr) {
124        this(context, attrs, defStyleAttr, 0);
125    }
126
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
147    public void setImageDrawable(@Nullable Drawable drawable, boolean tint) {
148        super.setImageDrawable(drawable);
149        mShouldTint = tint;
150        updateIconColor();
151    }
152
153    @Override
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
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
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
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
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
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
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     */
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
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
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
300    private Animator getRtAnimatorToRadius(float circleRadius) {
301        RenderNodeAnimator animator = new RenderNodeAnimator(mHwCircleRadius, circleRadius);
302        animator.setTarget(this);
303        return animator;
304    }
305
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
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
322    public void setCircleRadius(float circleRadius) {
323        setCircleRadius(circleRadius, false, false);
324    }
325
326    public void setCircleRadius(float circleRadius, boolean slowAnimation) {
327        setCircleRadius(circleRadius, slowAnimation, false);
328    }
329
330    public void setCircleRadiusWithoutAnimation(float circleRadius) {
331        cancelAnimator(mCircleAnimator);
332        setCircleRadius(circleRadius, false ,true);
333    }
334
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
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
413    private void cancelAnimator(Animator animator) {
414        if (animator != null) {
415            animator.cancel();
416        }
417    }
418
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     */
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
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
473    public float getRestingAlpha() {
474        return mRestingAlpha;
475    }
476
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     */
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
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
548    public float getCircleRadius() {
549        return mCircleRadius;
550    }
551
552    @Override
553    public boolean performClick() {
554        if (isClickable()) {
555            return super.performClick();
556        } else {
557            return false;
558        }
559    }
560
561    public void setLaunchingAffordance(boolean launchingAffordance) {
562        mLaunchingAffordance = launchingAffordance;
563    }
564}
565