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