• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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.navigationbar.buttons;
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.R;
38 import com.android.systemui.animation.Interpolators;
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
setVisible(boolean visible, boolean restart)228     public boolean setVisible(boolean visible, boolean restart) {
229         boolean changed = super.setVisible(visible, restart);
230         if (changed) {
231             // End any existing animations when the visibility changes
232             jumpToCurrentState();
233         }
234         return changed;
235     }
236 
237     @Override
jumpToCurrentState()238     public void jumpToCurrentState() {
239         endAnimations("jumpToCurrentState", false /* cancel */);
240     }
241 
242     @Override
isStateful()243     public boolean isStateful() {
244         return true;
245     }
246 
247     @Override
hasFocusStateSpecified()248     public boolean hasFocusStateSpecified() {
249         return true;
250     }
251 
setPressed(boolean pressed)252     public void setPressed(boolean pressed) {
253         if (mDark != mLastDark && pressed) {
254             mRipplePaint = null;
255             mLastDark = mDark;
256         }
257         if (mSupportHardware) {
258             setPressedHardware(pressed);
259         } else {
260             setPressedSoftware(pressed);
261         }
262     }
263 
264     /**
265      * Abort the ripple while it is delayed and before shown used only when setShouldDelayStartTouch
266      * is enabled.
267      */
abortDelayedRipple()268     public void abortDelayedRipple() {
269         mHandler.removeCallbacksAndMessages(null);
270     }
271 
endAnimations(String reason, boolean cancel)272     private void endAnimations(String reason, boolean cancel) {
273         Trace.beginSection("KeyButtonRipple.endAnim: reason=" + reason + " cancel=" + cancel);
274         Trace.endSection();
275         mVisible = false;
276         mTmpArray.addAll(mRunningAnimations);
277         int size = mTmpArray.size();
278         for (int i = 0; i < size; i++) {
279             Animator a = mTmpArray.get(i);
280             if (cancel) {
281                 a.cancel();
282             } else {
283                 a.end();
284             }
285         }
286         mTmpArray.clear();
287         mRunningAnimations.clear();
288         mHandler.removeCallbacksAndMessages(null);
289     }
290 
setPressedSoftware(boolean pressed)291     private void setPressedSoftware(boolean pressed) {
292         if (pressed) {
293             if (mDelayTouchFeedback) {
294                 if (mRunningAnimations.isEmpty()) {
295                     mHandler.removeCallbacksAndMessages(null);
296                     mHandler.postDelayed(this::enterSoftware, ViewConfiguration.getTapTimeout());
297                 } else if (mVisible) {
298                     enterSoftware();
299                 }
300             } else {
301                 enterSoftware();
302             }
303         } else {
304             exitSoftware();
305         }
306     }
307 
enterSoftware()308     private void enterSoftware() {
309         endAnimations("enterSoftware", true /* cancel */);
310         mVisible = true;
311         mGlowAlpha = getMaxGlowAlpha();
312         ObjectAnimator scaleAnimator = ObjectAnimator.ofFloat(this, "glowScale",
313                 0f, GLOW_MAX_SCALE_FACTOR);
314         scaleAnimator.setInterpolator(mInterpolator);
315         scaleAnimator.setDuration(ANIMATION_DURATION_SCALE);
316         scaleAnimator.addListener(mAnimatorListener);
317         scaleAnimator.start();
318         mRunningAnimations.add(scaleAnimator);
319 
320         // With the delay, it could eventually animate the enter animation with no pressed state,
321         // then immediately show the exit animation. If this is skipped there will be no ripple.
322         if (mDelayTouchFeedback && !mPressed) {
323             exitSoftware();
324         }
325     }
326 
exitSoftware()327     private void exitSoftware() {
328         ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(this, "glowAlpha", mGlowAlpha, 0f);
329         alphaAnimator.setInterpolator(Interpolators.ALPHA_OUT);
330         alphaAnimator.setDuration(ANIMATION_DURATION_FADE);
331         alphaAnimator.addListener(mAnimatorListener);
332         alphaAnimator.start();
333         mRunningAnimations.add(alphaAnimator);
334     }
335 
setPressedHardware(boolean pressed)336     private void setPressedHardware(boolean pressed) {
337         if (pressed) {
338             if (mDelayTouchFeedback) {
339                 if (mRunningAnimations.isEmpty()) {
340                     mHandler.removeCallbacksAndMessages(null);
341                     mHandler.postDelayed(this::enterHardware, ViewConfiguration.getTapTimeout());
342                 } else if (mVisible) {
343                     enterHardware();
344                 }
345             } else {
346                 enterHardware();
347             }
348         } else {
349             exitHardware();
350         }
351     }
352 
353     /**
354      * Sets the left/top property for the round rect to {@code prop} depending on whether we are
355      * horizontal or vertical mode.
356      */
setExtendStart(CanvasProperty<Float> prop)357     private void setExtendStart(CanvasProperty<Float> prop) {
358         if (isHorizontal()) {
359             mLeftProp = prop;
360         } else {
361             mTopProp = prop;
362         }
363     }
364 
getExtendStart()365     private CanvasProperty<Float> getExtendStart() {
366         return isHorizontal() ? mLeftProp : mTopProp;
367     }
368 
369     /**
370      * Sets the right/bottom property for the round rect to {@code prop} depending on whether we are
371      * horizontal or vertical mode.
372      */
setExtendEnd(CanvasProperty<Float> prop)373     private void setExtendEnd(CanvasProperty<Float> prop) {
374         if (isHorizontal()) {
375             mRightProp = prop;
376         } else {
377             mBottomProp = prop;
378         }
379     }
380 
getExtendEnd()381     private CanvasProperty<Float> getExtendEnd() {
382         return isHorizontal() ? mRightProp : mBottomProp;
383     }
384 
getExtendSize()385     private int getExtendSize() {
386         return isHorizontal() ? getBounds().width() : getBounds().height();
387     }
388 
getRippleSize()389     private int getRippleSize() {
390         int size = isHorizontal() ? getBounds().width() : getBounds().height();
391         return Math.min(size, mMaxWidth);
392     }
393 
enterHardware()394     private void enterHardware() {
395         endAnimations("enterHardware", true /* cancel */);
396         mVisible = true;
397         mDrawingHardwareGlow = true;
398         setExtendStart(CanvasProperty.createFloat(getExtendSize() / 2));
399         final RenderNodeAnimator startAnim = new RenderNodeAnimator(getExtendStart(),
400                 getExtendSize()/2 - GLOW_MAX_SCALE_FACTOR * getRippleSize()/2);
401         startAnim.setDuration(ANIMATION_DURATION_SCALE);
402         startAnim.setInterpolator(mInterpolator);
403         startAnim.addListener(mAnimatorListener);
404         startAnim.setTarget(mTargetView);
405 
406         setExtendEnd(CanvasProperty.createFloat(getExtendSize() / 2));
407         final RenderNodeAnimator endAnim = new RenderNodeAnimator(getExtendEnd(),
408                 getExtendSize()/2 + GLOW_MAX_SCALE_FACTOR * getRippleSize()/2);
409         endAnim.setDuration(ANIMATION_DURATION_SCALE);
410         endAnim.setInterpolator(mInterpolator);
411         endAnim.addListener(mAnimatorListener);
412         endAnim.addListener(mEnterHwTraceAnimator);
413         endAnim.setTarget(mTargetView);
414 
415         if (isHorizontal()) {
416             mTopProp = CanvasProperty.createFloat(0f);
417             mBottomProp = CanvasProperty.createFloat(getBounds().height());
418             mRxProp = CanvasProperty.createFloat(getBounds().height()/2);
419             mRyProp = CanvasProperty.createFloat(getBounds().height()/2);
420         } else {
421             mLeftProp = CanvasProperty.createFloat(0f);
422             mRightProp = CanvasProperty.createFloat(getBounds().width());
423             mRxProp = CanvasProperty.createFloat(getBounds().width()/2);
424             mRyProp = CanvasProperty.createFloat(getBounds().width()/2);
425         }
426 
427         mGlowScale = GLOW_MAX_SCALE_FACTOR;
428         mGlowAlpha = getMaxGlowAlpha();
429         mRipplePaint = getRipplePaint();
430         mRipplePaint.setAlpha((int) (mGlowAlpha * 255));
431         mPaintProp = CanvasProperty.createPaint(mRipplePaint);
432 
433         startAnim.start();
434         endAnim.start();
435         mRunningAnimations.add(startAnim);
436         mRunningAnimations.add(endAnim);
437 
438         invalidateSelf();
439 
440         // With the delay, it could eventually animate the enter animation with no pressed state,
441         // then immediately show the exit animation. If this is skipped there will be no ripple.
442         if (mDelayTouchFeedback && !mPressed) {
443             exitHardware();
444         }
445     }
446 
exitHardware()447     private void exitHardware() {
448         mPaintProp = CanvasProperty.createPaint(getRipplePaint());
449         final RenderNodeAnimator opacityAnim = new RenderNodeAnimator(mPaintProp,
450                 RenderNodeAnimator.PAINT_ALPHA, 0);
451         opacityAnim.setDuration(ANIMATION_DURATION_FADE);
452         opacityAnim.setInterpolator(Interpolators.ALPHA_OUT);
453         opacityAnim.addListener(mAnimatorListener);
454         opacityAnim.addListener(mExitHwTraceAnimator);
455         opacityAnim.setTarget(mTargetView);
456 
457         opacityAnim.start();
458         mRunningAnimations.add(opacityAnim);
459 
460         invalidateSelf();
461     }
462 
463     private final AnimatorListenerAdapter mAnimatorListener =
464             new AnimatorListenerAdapter() {
465                 @Override
466                 public void onAnimationEnd(Animator animation) {
467                     mRunningAnimations.remove(animation);
468                     if (mRunningAnimations.isEmpty() && !mPressed) {
469                         mVisible = false;
470                         mDrawingHardwareGlow = false;
471                         invalidateSelf();
472                     }
473                 }
474             };
475 
476     private static final class TraceAnimatorListener extends AnimatorListenerAdapter {
477         private final String mName;
TraceAnimatorListener(String name)478         TraceAnimatorListener(String name) {
479             mName = name;
480         }
481 
482         @Override
onAnimationStart(Animator animation)483         public void onAnimationStart(Animator animation) {
484             Trace.beginSection("KeyButtonRipple.start." + mName);
485             Trace.endSection();
486         }
487 
488         @Override
onAnimationCancel(Animator animation)489         public void onAnimationCancel(Animator animation) {
490             Trace.beginSection("KeyButtonRipple.cancel." + mName);
491             Trace.endSection();
492         }
493 
494         @Override
onAnimationEnd(Animator animation)495         public void onAnimationEnd(Animator animation) {
496             Trace.beginSection("KeyButtonRipple.end." + mName);
497             Trace.endSection();
498         }
499     }
500 
501     /**
502      * Interpolator with a smooth log deceleration
503      */
504     private static final class LogInterpolator implements Interpolator {
505         @Override
getInterpolation(float input)506         public float getInterpolation(float input) {
507             return 1 - (float) Math.pow(400, -input * 1.4);
508         }
509     }
510 }
511