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