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