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.RecordingCanvas;
29 import android.graphics.drawable.Drawable;
30 import android.os.Handler;
31 import android.os.Trace;
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 
77     private final TraceAnimatorListener mExitHwTraceAnimator =
78             new TraceAnimatorListener("exitHardware");
79     private final TraceAnimatorListener mEnterHwTraceAnimator =
80             new TraceAnimatorListener("enterHardware");
81 
82     public enum Type {
83         OVAL,
84         ROUNDED_RECT
85     }
86 
87     private Type mType = Type.ROUNDED_RECT;
88 
KeyButtonRipple(Context ctx, View targetView)89     public KeyButtonRipple(Context ctx, View targetView) {
90         mMaxWidth =  ctx.getResources().getDimensionPixelSize(R.dimen.key_button_ripple_max_width);
91         mTargetView = targetView;
92     }
93 
setDarkIntensity(float darkIntensity)94     public void setDarkIntensity(float darkIntensity) {
95         mDark = darkIntensity >= 0.5f;
96     }
97 
setDelayTouchFeedback(boolean delay)98     public void setDelayTouchFeedback(boolean delay) {
99         mDelayTouchFeedback = delay;
100     }
101 
setType(Type type)102     public void setType(Type type) {
103         mType = type;
104     }
105 
getRipplePaint()106     private Paint getRipplePaint() {
107         if (mRipplePaint == null) {
108             mRipplePaint = new Paint();
109             mRipplePaint.setAntiAlias(true);
110             mRipplePaint.setColor(mLastDark ? 0xff000000 : 0xffffffff);
111         }
112         return mRipplePaint;
113     }
114 
drawSoftware(Canvas canvas)115     private void drawSoftware(Canvas canvas) {
116         if (mGlowAlpha > 0f) {
117             final Paint p = getRipplePaint();
118             p.setAlpha((int)(mGlowAlpha * 255f));
119 
120             final float w = getBounds().width();
121             final float h = getBounds().height();
122             final boolean horizontal = w > h;
123             final float diameter = getRippleSize() * mGlowScale;
124             final float radius = diameter * .5f;
125             final float cx = w * .5f;
126             final float cy = h * .5f;
127             final float rx = horizontal ? radius : cx;
128             final float ry = horizontal ? cy : radius;
129             final float corner = horizontal ? cy : cx;
130 
131             if (mType == Type.ROUNDED_RECT) {
132                 canvas.drawRoundRect(cx - rx, cy - ry, cx + rx, cy + ry, corner, corner, p);
133             } else {
134                 canvas.save();
135                 canvas.translate(cx, cy);
136                 float r = Math.min(rx, ry);
137                 canvas.drawOval(-r, -r, r, r, p);
138                 canvas.restore();
139             }
140         }
141     }
142 
143     @Override
draw(Canvas canvas)144     public void draw(Canvas canvas) {
145         mSupportHardware = canvas.isHardwareAccelerated();
146         if (mSupportHardware) {
147             drawHardware((RecordingCanvas) canvas);
148         } else {
149             drawSoftware(canvas);
150         }
151     }
152 
153     @Override
setAlpha(int alpha)154     public void setAlpha(int alpha) {
155         // Not supported.
156     }
157 
158     @Override
setColorFilter(ColorFilter colorFilter)159     public void setColorFilter(ColorFilter colorFilter) {
160         // Not supported.
161     }
162 
163     @Override
getOpacity()164     public int getOpacity() {
165         return PixelFormat.TRANSLUCENT;
166     }
167 
isHorizontal()168     private boolean isHorizontal() {
169         return getBounds().width() > getBounds().height();
170     }
171 
drawHardware(RecordingCanvas c)172     private void drawHardware(RecordingCanvas c) {
173         if (mDrawingHardwareGlow) {
174             if (mType == Type.ROUNDED_RECT) {
175                 c.drawRoundRect(mLeftProp, mTopProp, mRightProp, mBottomProp, mRxProp, mRyProp,
176                         mPaintProp);
177             } else {
178                 CanvasProperty<Float> cx = CanvasProperty.createFloat(getBounds().width() / 2);
179                 CanvasProperty<Float> cy = CanvasProperty.createFloat(getBounds().height() / 2);
180                 int d = Math.min(getBounds().width(), getBounds().height());
181                 CanvasProperty<Float> r = CanvasProperty.createFloat(1.0f * d / 2);
182                 c.drawCircle(cx, cy, r, mPaintProp);
183             }
184         }
185     }
186 
getGlowAlpha()187     public float getGlowAlpha() {
188         return mGlowAlpha;
189     }
190 
setGlowAlpha(float x)191     public void setGlowAlpha(float x) {
192         mGlowAlpha = x;
193         invalidateSelf();
194     }
195 
getGlowScale()196     public float getGlowScale() {
197         return mGlowScale;
198     }
199 
setGlowScale(float x)200     public void setGlowScale(float x) {
201         mGlowScale = x;
202         invalidateSelf();
203     }
204 
getMaxGlowAlpha()205     private float getMaxGlowAlpha() {
206         return mLastDark ? GLOW_MAX_ALPHA_DARK : GLOW_MAX_ALPHA;
207     }
208 
209     @Override
onStateChange(int[] state)210     protected boolean onStateChange(int[] state) {
211         boolean pressed = false;
212         for (int i = 0; i < state.length; i++) {
213             if (state[i] == android.R.attr.state_pressed) {
214                 pressed = true;
215                 break;
216             }
217         }
218         if (pressed != mPressed) {
219             setPressed(pressed);
220             mPressed = pressed;
221             return true;
222         } else {
223             return false;
224         }
225     }
226 
227     @Override
jumpToCurrentState()228     public void jumpToCurrentState() {
229         endAnimations("jumpToCurrentState", false /* cancel */);
230     }
231 
232     @Override
isStateful()233     public boolean isStateful() {
234         return true;
235     }
236 
237     @Override
hasFocusStateSpecified()238     public boolean hasFocusStateSpecified() {
239         return true;
240     }
241 
setPressed(boolean pressed)242     public void setPressed(boolean pressed) {
243         if (mDark != mLastDark && pressed) {
244             mRipplePaint = null;
245             mLastDark = mDark;
246         }
247         if (mSupportHardware) {
248             setPressedHardware(pressed);
249         } else {
250             setPressedSoftware(pressed);
251         }
252     }
253 
254     /**
255      * Abort the ripple while it is delayed and before shown used only when setShouldDelayStartTouch
256      * is enabled.
257      */
abortDelayedRipple()258     public void abortDelayedRipple() {
259         mHandler.removeCallbacksAndMessages(null);
260     }
261 
endAnimations(String reason, boolean cancel)262     private void endAnimations(String reason, boolean cancel) {
263         Trace.beginSection("KeyButtonRipple.endAnim: reason=" + reason + " cancel=" + cancel);
264         Trace.endSection();
265         mVisible = false;
266         mTmpArray.addAll(mRunningAnimations);
267         int size = mTmpArray.size();
268         for (int i = 0; i < size; i++) {
269             Animator a = mTmpArray.get(i);
270             if (cancel) {
271                 a.cancel();
272             } else {
273                 a.end();
274             }
275         }
276         mTmpArray.clear();
277         mRunningAnimations.clear();
278         mHandler.removeCallbacksAndMessages(null);
279     }
280 
setPressedSoftware(boolean pressed)281     private void setPressedSoftware(boolean pressed) {
282         if (pressed) {
283             if (mDelayTouchFeedback) {
284                 if (mRunningAnimations.isEmpty()) {
285                     mHandler.removeCallbacksAndMessages(null);
286                     mHandler.postDelayed(this::enterSoftware, ViewConfiguration.getTapTimeout());
287                 } else if (mVisible) {
288                     enterSoftware();
289                 }
290             } else {
291                 enterSoftware();
292             }
293         } else {
294             exitSoftware();
295         }
296     }
297 
enterSoftware()298     private void enterSoftware() {
299         endAnimations("enterSoftware", true /* cancel */);
300         mVisible = true;
301         mGlowAlpha = getMaxGlowAlpha();
302         ObjectAnimator scaleAnimator = ObjectAnimator.ofFloat(this, "glowScale",
303                 0f, GLOW_MAX_SCALE_FACTOR);
304         scaleAnimator.setInterpolator(mInterpolator);
305         scaleAnimator.setDuration(ANIMATION_DURATION_SCALE);
306         scaleAnimator.addListener(mAnimatorListener);
307         scaleAnimator.start();
308         mRunningAnimations.add(scaleAnimator);
309 
310         // With the delay, it could eventually animate the enter animation with no pressed state,
311         // then immediately show the exit animation. If this is skipped there will be no ripple.
312         if (mDelayTouchFeedback && !mPressed) {
313             exitSoftware();
314         }
315     }
316 
exitSoftware()317     private void exitSoftware() {
318         ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(this, "glowAlpha", mGlowAlpha, 0f);
319         alphaAnimator.setInterpolator(Interpolators.ALPHA_OUT);
320         alphaAnimator.setDuration(ANIMATION_DURATION_FADE);
321         alphaAnimator.addListener(mAnimatorListener);
322         alphaAnimator.start();
323         mRunningAnimations.add(alphaAnimator);
324     }
325 
setPressedHardware(boolean pressed)326     private void setPressedHardware(boolean pressed) {
327         if (pressed) {
328             if (mDelayTouchFeedback) {
329                 if (mRunningAnimations.isEmpty()) {
330                     mHandler.removeCallbacksAndMessages(null);
331                     mHandler.postDelayed(this::enterHardware, ViewConfiguration.getTapTimeout());
332                 } else if (mVisible) {
333                     enterHardware();
334                 }
335             } else {
336                 enterHardware();
337             }
338         } else {
339             exitHardware();
340         }
341     }
342 
343     /**
344      * Sets the left/top property for the round rect to {@code prop} depending on whether we are
345      * horizontal or vertical mode.
346      */
setExtendStart(CanvasProperty<Float> prop)347     private void setExtendStart(CanvasProperty<Float> prop) {
348         if (isHorizontal()) {
349             mLeftProp = prop;
350         } else {
351             mTopProp = prop;
352         }
353     }
354 
getExtendStart()355     private CanvasProperty<Float> getExtendStart() {
356         return isHorizontal() ? mLeftProp : mTopProp;
357     }
358 
359     /**
360      * Sets the right/bottom property for the round rect to {@code prop} depending on whether we are
361      * horizontal or vertical mode.
362      */
setExtendEnd(CanvasProperty<Float> prop)363     private void setExtendEnd(CanvasProperty<Float> prop) {
364         if (isHorizontal()) {
365             mRightProp = prop;
366         } else {
367             mBottomProp = prop;
368         }
369     }
370 
getExtendEnd()371     private CanvasProperty<Float> getExtendEnd() {
372         return isHorizontal() ? mRightProp : mBottomProp;
373     }
374 
getExtendSize()375     private int getExtendSize() {
376         return isHorizontal() ? getBounds().width() : getBounds().height();
377     }
378 
getRippleSize()379     private int getRippleSize() {
380         int size = isHorizontal() ? getBounds().width() : getBounds().height();
381         return Math.min(size, mMaxWidth);
382     }
383 
enterHardware()384     private void enterHardware() {
385         endAnimations("enterHardware", true /* cancel */);
386         mVisible = true;
387         mDrawingHardwareGlow = true;
388         setExtendStart(CanvasProperty.createFloat(getExtendSize() / 2));
389         final RenderNodeAnimator startAnim = new RenderNodeAnimator(getExtendStart(),
390                 getExtendSize()/2 - GLOW_MAX_SCALE_FACTOR * getRippleSize()/2);
391         startAnim.setDuration(ANIMATION_DURATION_SCALE);
392         startAnim.setInterpolator(mInterpolator);
393         startAnim.addListener(mAnimatorListener);
394         startAnim.setTarget(mTargetView);
395 
396         setExtendEnd(CanvasProperty.createFloat(getExtendSize() / 2));
397         final RenderNodeAnimator endAnim = new RenderNodeAnimator(getExtendEnd(),
398                 getExtendSize()/2 + GLOW_MAX_SCALE_FACTOR * getRippleSize()/2);
399         endAnim.setDuration(ANIMATION_DURATION_SCALE);
400         endAnim.setInterpolator(mInterpolator);
401         endAnim.addListener(mAnimatorListener);
402         endAnim.addListener(mEnterHwTraceAnimator);
403         endAnim.setTarget(mTargetView);
404 
405         if (isHorizontal()) {
406             mTopProp = CanvasProperty.createFloat(0f);
407             mBottomProp = CanvasProperty.createFloat(getBounds().height());
408             mRxProp = CanvasProperty.createFloat(getBounds().height()/2);
409             mRyProp = CanvasProperty.createFloat(getBounds().height()/2);
410         } else {
411             mLeftProp = CanvasProperty.createFloat(0f);
412             mRightProp = CanvasProperty.createFloat(getBounds().width());
413             mRxProp = CanvasProperty.createFloat(getBounds().width()/2);
414             mRyProp = CanvasProperty.createFloat(getBounds().width()/2);
415         }
416 
417         mGlowScale = GLOW_MAX_SCALE_FACTOR;
418         mGlowAlpha = getMaxGlowAlpha();
419         mRipplePaint = getRipplePaint();
420         mRipplePaint.setAlpha((int) (mGlowAlpha * 255));
421         mPaintProp = CanvasProperty.createPaint(mRipplePaint);
422 
423         startAnim.start();
424         endAnim.start();
425         mRunningAnimations.add(startAnim);
426         mRunningAnimations.add(endAnim);
427 
428         invalidateSelf();
429 
430         // With the delay, it could eventually animate the enter animation with no pressed state,
431         // then immediately show the exit animation. If this is skipped there will be no ripple.
432         if (mDelayTouchFeedback && !mPressed) {
433             exitHardware();
434         }
435     }
436 
exitHardware()437     private void exitHardware() {
438         mPaintProp = CanvasProperty.createPaint(getRipplePaint());
439         final RenderNodeAnimator opacityAnim = new RenderNodeAnimator(mPaintProp,
440                 RenderNodeAnimator.PAINT_ALPHA, 0);
441         opacityAnim.setDuration(ANIMATION_DURATION_FADE);
442         opacityAnim.setInterpolator(Interpolators.ALPHA_OUT);
443         opacityAnim.addListener(mAnimatorListener);
444         opacityAnim.addListener(mExitHwTraceAnimator);
445         opacityAnim.setTarget(mTargetView);
446 
447         opacityAnim.start();
448         mRunningAnimations.add(opacityAnim);
449 
450         invalidateSelf();
451     }
452 
453     private final AnimatorListenerAdapter mAnimatorListener =
454             new AnimatorListenerAdapter() {
455                 @Override
456                 public void onAnimationEnd(Animator animation) {
457                     mRunningAnimations.remove(animation);
458                     if (mRunningAnimations.isEmpty() && !mPressed) {
459                         mVisible = false;
460                         mDrawingHardwareGlow = false;
461                         invalidateSelf();
462                     }
463                 }
464             };
465 
466     private static final class TraceAnimatorListener extends AnimatorListenerAdapter {
467         private final String mName;
TraceAnimatorListener(String name)468         TraceAnimatorListener(String name) {
469             mName = name;
470         }
471 
472         @Override
onAnimationStart(Animator animation)473         public void onAnimationStart(Animator animation) {
474             Trace.beginSection("KeyButtonRipple.start." + mName);
475             Trace.endSection();
476         }
477 
478         @Override
onAnimationCancel(Animator animation)479         public void onAnimationCancel(Animator animation) {
480             Trace.beginSection("KeyButtonRipple.cancel." + mName);
481             Trace.endSection();
482         }
483 
484         @Override
onAnimationEnd(Animator animation)485         public void onAnimationEnd(Animator animation) {
486             Trace.beginSection("KeyButtonRipple.end." + mName);
487             Trace.endSection();
488         }
489     }
490 
491     /**
492      * Interpolator with a smooth log deceleration
493      */
494     private static final class LogInterpolator implements Interpolator {
495         @Override
getInterpolation(float input)496         public float getInterpolation(float input) {
497             return 1 - (float) Math.pow(400, -input * 1.4);
498         }
499     }
500 }
501