KeyButtonRipple.java revision 40db029cfeb263b5b672ca687c347e58ed2ad2ae
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.policy;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ObjectAnimator;
22import android.content.Context;
23import android.graphics.Canvas;
24import android.graphics.CanvasProperty;
25import android.graphics.ColorFilter;
26import android.graphics.Paint;
27import android.graphics.PixelFormat;
28import android.graphics.drawable.Drawable;
29import android.view.DisplayListCanvas;
30import android.view.RenderNodeAnimator;
31import android.view.View;
32import android.view.animation.Interpolator;
33
34import com.android.systemui.Interpolators;
35import com.android.systemui.R;
36
37import java.util.ArrayList;
38import java.util.HashSet;
39
40public class KeyButtonRipple extends Drawable {
41
42    private static final float GLOW_MAX_SCALE_FACTOR = 1.35f;
43    private static final float GLOW_MAX_ALPHA = 0.2f;
44    private static final float GLOW_MAX_ALPHA_DARK = 0.1f;
45    private static final int ANIMATION_DURATION_SCALE = 350;
46    private static final int ANIMATION_DURATION_FADE = 450;
47
48    private Paint mRipplePaint;
49    private CanvasProperty<Float> mLeftProp;
50    private CanvasProperty<Float> mTopProp;
51    private CanvasProperty<Float> mRightProp;
52    private CanvasProperty<Float> mBottomProp;
53    private CanvasProperty<Float> mRxProp;
54    private CanvasProperty<Float> mRyProp;
55    private CanvasProperty<Paint> mPaintProp;
56    private float mGlowAlpha = 0f;
57    private float mGlowScale = 1f;
58    private boolean mPressed;
59    private boolean mDrawingHardwareGlow;
60    private int mMaxWidth;
61    private boolean mLastDark;
62    private boolean mDark;
63
64    private final Interpolator mInterpolator = new LogInterpolator();
65    private boolean mSupportHardware;
66    private final View mTargetView;
67
68    private final HashSet<Animator> mRunningAnimations = new HashSet<>();
69    private final ArrayList<Animator> mTmpArray = new ArrayList<>();
70
71    public KeyButtonRipple(Context ctx, View targetView) {
72        mMaxWidth =  ctx.getResources().getDimensionPixelSize(R.dimen.key_button_ripple_max_width);
73        mTargetView = targetView;
74    }
75
76    public void setDarkIntensity(float darkIntensity) {
77        mDark = darkIntensity >= 0.5f;
78    }
79
80    private Paint getRipplePaint() {
81        if (mRipplePaint == null) {
82            mRipplePaint = new Paint();
83            mRipplePaint.setAntiAlias(true);
84            mRipplePaint.setColor(mLastDark ? 0xff000000 : 0xffffffff);
85        }
86        return mRipplePaint;
87    }
88
89    private void drawSoftware(Canvas canvas) {
90        if (mGlowAlpha > 0f) {
91            final Paint p = getRipplePaint();
92            p.setAlpha((int)(mGlowAlpha * 255f));
93
94            final float w = getBounds().width();
95            final float h = getBounds().height();
96            final boolean horizontal = w > h;
97            final float diameter = getRippleSize() * mGlowScale;
98            final float radius = diameter * .5f;
99            final float cx = w * .5f;
100            final float cy = h * .5f;
101            final float rx = horizontal ? radius : cx;
102            final float ry = horizontal ? cy : radius;
103            final float corner = horizontal ? cy : cx;
104
105            canvas.drawRoundRect(cx - rx, cy - ry,
106                    cx + rx, cy + ry,
107                    corner, corner, p);
108        }
109    }
110
111    @Override
112    public void draw(Canvas canvas) {
113        mSupportHardware = canvas.isHardwareAccelerated();
114        if (mSupportHardware) {
115            drawHardware((DisplayListCanvas) canvas);
116        } else {
117            drawSoftware(canvas);
118        }
119    }
120
121    @Override
122    public void setAlpha(int alpha) {
123        // Not supported.
124    }
125
126    @Override
127    public void setColorFilter(ColorFilter colorFilter) {
128        // Not supported.
129    }
130
131    @Override
132    public int getOpacity() {
133        return PixelFormat.TRANSLUCENT;
134    }
135
136    private boolean isHorizontal() {
137        return getBounds().width() > getBounds().height();
138    }
139
140    private void drawHardware(DisplayListCanvas c) {
141        if (mDrawingHardwareGlow) {
142            c.drawRoundRect(mLeftProp, mTopProp, mRightProp, mBottomProp, mRxProp, mRyProp,
143                    mPaintProp);
144        }
145    }
146
147    public float getGlowAlpha() {
148        return mGlowAlpha;
149    }
150
151    public void setGlowAlpha(float x) {
152        mGlowAlpha = x;
153        invalidateSelf();
154    }
155
156    public float getGlowScale() {
157        return mGlowScale;
158    }
159
160    public void setGlowScale(float x) {
161        mGlowScale = x;
162        invalidateSelf();
163    }
164
165    private float getMaxGlowAlpha() {
166        return mLastDark ? GLOW_MAX_ALPHA_DARK : GLOW_MAX_ALPHA;
167    }
168
169    @Override
170    protected boolean onStateChange(int[] state) {
171        boolean pressed = false;
172        for (int i = 0; i < state.length; i++) {
173            if (state[i] == android.R.attr.state_pressed) {
174                pressed = true;
175                break;
176            }
177        }
178        if (pressed != mPressed) {
179            setPressed(pressed);
180            mPressed = pressed;
181            return true;
182        } else {
183            return false;
184        }
185    }
186
187    @Override
188    public void jumpToCurrentState() {
189        cancelAnimations();
190    }
191
192    @Override
193    public boolean isStateful() {
194        return true;
195    }
196
197    public void setPressed(boolean pressed) {
198        if (mDark != mLastDark && pressed) {
199            mRipplePaint = null;
200            mLastDark = mDark;
201        }
202        if (mSupportHardware) {
203            setPressedHardware(pressed);
204        } else {
205            setPressedSoftware(pressed);
206        }
207    }
208
209    private void cancelAnimations() {
210        mTmpArray.addAll(mRunningAnimations);
211        int size = mTmpArray.size();
212        for (int i = 0; i < size; i++) {
213            Animator a = mTmpArray.get(i);
214            a.cancel();
215        }
216        mTmpArray.clear();
217        mRunningAnimations.clear();
218    }
219
220    private void setPressedSoftware(boolean pressed) {
221        if (pressed) {
222            enterSoftware();
223        } else {
224            exitSoftware();
225        }
226    }
227
228    private void enterSoftware() {
229        cancelAnimations();
230        mGlowAlpha = getMaxGlowAlpha();
231        ObjectAnimator scaleAnimator = ObjectAnimator.ofFloat(this, "glowScale",
232                0f, GLOW_MAX_SCALE_FACTOR);
233        scaleAnimator.setInterpolator(mInterpolator);
234        scaleAnimator.setDuration(ANIMATION_DURATION_SCALE);
235        scaleAnimator.addListener(mAnimatorListener);
236        scaleAnimator.start();
237        mRunningAnimations.add(scaleAnimator);
238    }
239
240    private void exitSoftware() {
241        ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(this, "glowAlpha", mGlowAlpha, 0f);
242        alphaAnimator.setInterpolator(Interpolators.ALPHA_OUT);
243        alphaAnimator.setDuration(ANIMATION_DURATION_FADE);
244        alphaAnimator.addListener(mAnimatorListener);
245        alphaAnimator.start();
246        mRunningAnimations.add(alphaAnimator);
247    }
248
249    private void setPressedHardware(boolean pressed) {
250        if (pressed) {
251            enterHardware();
252        } else {
253            exitHardware();
254        }
255    }
256
257    /**
258     * Sets the left/top property for the round rect to {@code prop} depending on whether we are
259     * horizontal or vertical mode.
260     */
261    private void setExtendStart(CanvasProperty<Float> prop) {
262        if (isHorizontal()) {
263            mLeftProp = prop;
264        } else {
265            mTopProp = prop;
266        }
267    }
268
269    private CanvasProperty<Float> getExtendStart() {
270        return isHorizontal() ? mLeftProp : mTopProp;
271    }
272
273    /**
274     * Sets the right/bottom property for the round rect to {@code prop} depending on whether we are
275     * horizontal or vertical mode.
276     */
277    private void setExtendEnd(CanvasProperty<Float> prop) {
278        if (isHorizontal()) {
279            mRightProp = prop;
280        } else {
281            mBottomProp = prop;
282        }
283    }
284
285    private CanvasProperty<Float> getExtendEnd() {
286        return isHorizontal() ? mRightProp : mBottomProp;
287    }
288
289    private int getExtendSize() {
290        return isHorizontal() ? getBounds().width() : getBounds().height();
291    }
292
293    private int getRippleSize() {
294        int size = isHorizontal() ? getBounds().width() : getBounds().height();
295        return Math.min(size, mMaxWidth);
296    }
297
298    private void enterHardware() {
299        cancelAnimations();
300        mDrawingHardwareGlow = true;
301        setExtendStart(CanvasProperty.createFloat(getExtendSize() / 2));
302        final RenderNodeAnimator startAnim = new RenderNodeAnimator(getExtendStart(),
303                getExtendSize()/2 - GLOW_MAX_SCALE_FACTOR * getRippleSize()/2);
304        startAnim.setDuration(ANIMATION_DURATION_SCALE);
305        startAnim.setInterpolator(mInterpolator);
306        startAnim.addListener(mAnimatorListener);
307        startAnim.setTarget(mTargetView);
308
309        setExtendEnd(CanvasProperty.createFloat(getExtendSize() / 2));
310        final RenderNodeAnimator endAnim = new RenderNodeAnimator(getExtendEnd(),
311                getExtendSize()/2 + GLOW_MAX_SCALE_FACTOR * getRippleSize()/2);
312        endAnim.setDuration(ANIMATION_DURATION_SCALE);
313        endAnim.setInterpolator(mInterpolator);
314        endAnim.addListener(mAnimatorListener);
315        endAnim.setTarget(mTargetView);
316
317        if (isHorizontal()) {
318            mTopProp = CanvasProperty.createFloat(0f);
319            mBottomProp = CanvasProperty.createFloat(getBounds().height());
320            mRxProp = CanvasProperty.createFloat(getBounds().height()/2);
321            mRyProp = CanvasProperty.createFloat(getBounds().height()/2);
322        } else {
323            mLeftProp = CanvasProperty.createFloat(0f);
324            mRightProp = CanvasProperty.createFloat(getBounds().width());
325            mRxProp = CanvasProperty.createFloat(getBounds().width()/2);
326            mRyProp = CanvasProperty.createFloat(getBounds().width()/2);
327        }
328
329        mGlowScale = GLOW_MAX_SCALE_FACTOR;
330        mGlowAlpha = getMaxGlowAlpha();
331        mRipplePaint = getRipplePaint();
332        mRipplePaint.setAlpha((int) (mGlowAlpha * 255));
333        mPaintProp = CanvasProperty.createPaint(mRipplePaint);
334
335        startAnim.start();
336        endAnim.start();
337        mRunningAnimations.add(startAnim);
338        mRunningAnimations.add(endAnim);
339
340        invalidateSelf();
341    }
342
343    private void exitHardware() {
344        mPaintProp = CanvasProperty.createPaint(getRipplePaint());
345        final RenderNodeAnimator opacityAnim = new RenderNodeAnimator(mPaintProp,
346                RenderNodeAnimator.PAINT_ALPHA, 0);
347        opacityAnim.setDuration(ANIMATION_DURATION_FADE);
348        opacityAnim.setInterpolator(Interpolators.ALPHA_OUT);
349        opacityAnim.addListener(mAnimatorListener);
350        opacityAnim.setTarget(mTargetView);
351
352        opacityAnim.start();
353        mRunningAnimations.add(opacityAnim);
354
355        invalidateSelf();
356    }
357
358    private final AnimatorListenerAdapter mAnimatorListener =
359            new AnimatorListenerAdapter() {
360        @Override
361        public void onAnimationEnd(Animator animation) {
362            mRunningAnimations.remove(animation);
363            if (mRunningAnimations.isEmpty() && !mPressed) {
364                mDrawingHardwareGlow = false;
365                invalidateSelf();
366            }
367        }
368    };
369
370    /**
371     * Interpolator with a smooth log deceleration
372     */
373    private static final class LogInterpolator implements Interpolator {
374        @Override
375        public float getInterpolation(float input) {
376            return 1 - (float) Math.pow(400, -input * 1.4);
377        }
378    }
379}
380