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.graphics.Canvas;
27import android.graphics.CanvasProperty;
28import android.graphics.Color;
29import android.graphics.Paint;
30import android.graphics.PorterDuff;
31import android.graphics.drawable.Drawable;
32import android.util.AttributeSet;
33import android.view.DisplayListCanvas;
34import android.view.RenderNodeAnimator;
35import android.view.View;
36import android.view.ViewAnimationUtils;
37import android.view.animation.Interpolator;
38import android.widget.ImageView;
39
40import com.android.systemui.Interpolators;
41import com.android.systemui.R;
42import 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 */
48public 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
114    public KeyguardAffordanceView(Context context) {
115        this(context, null);
116    }
117
118    public KeyguardAffordanceView(Context context, AttributeSet attrs) {
119        this(context, attrs, 0);
120    }
121
122    public KeyguardAffordanceView(Context context, AttributeSet attrs, int defStyleAttr) {
123        this(context, attrs, defStyleAttr, 0);
124    }
125
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
142    public void setImageDrawable(@Nullable Drawable drawable, boolean tint) {
143        super.setImageDrawable(drawable);
144        mShouldTint = tint;
145        updateIconColor();
146    }
147
148    @Override
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
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
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
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
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
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
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     */
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
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
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
295    private Animator getRtAnimatorToRadius(float circleRadius) {
296        RenderNodeAnimator animator = new RenderNodeAnimator(mHwCircleRadius, circleRadius);
297        animator.setTarget(this);
298        return animator;
299    }
300
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
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
317    public void setCircleRadius(float circleRadius) {
318        setCircleRadius(circleRadius, false, false);
319    }
320
321    public void setCircleRadius(float circleRadius, boolean slowAnimation) {
322        setCircleRadius(circleRadius, slowAnimation, false);
323    }
324
325    public void setCircleRadiusWithoutAnimation(float circleRadius) {
326        cancelAnimator(mCircleAnimator);
327        setCircleRadius(circleRadius, false ,true);
328    }
329
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
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
408    private void cancelAnimator(Animator animator) {
409        if (animator != null) {
410            animator.cancel();
411        }
412    }
413
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     */
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
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
468    public float getRestingAlpha() {
469        return mRestingAlpha;
470    }
471
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     */
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
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
543    public float getCircleRadius() {
544        return mCircleRadius;
545    }
546
547    @Override
548    public boolean performClick() {
549        if (isClickable()) {
550            return super.performClick();
551        } else {
552            return false;
553        }
554    }
555
556    public void setLaunchingAffordance(boolean launchingAffordance) {
557        mLaunchingAffordance = launchingAffordance;
558    }
559}
560