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