1 /*
2  * Copyright (C) 2015 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 android.graphics.drawable;
18 
19 import android.animation.Animator;
20 import android.graphics.Canvas;
21 import android.graphics.Paint;
22 import android.graphics.Rect;
23 import android.util.DisplayMetrics;
24 import android.view.DisplayListCanvas;
25 import android.view.RenderNodeAnimator;
26 
27 import java.util.ArrayList;
28 
29 /**
30  * Abstract class that handles hardware/software hand-off and lifecycle for
31  * animated ripple foreground and background components.
32  */
33 abstract class RippleComponent {
34     private final RippleDrawable mOwner;
35 
36     /** Bounds used for computing max radius. May be modified by the owner. */
37     protected final Rect mBounds;
38 
39     /** Whether we can use hardware acceleration for the exit animation. */
40     private boolean mHasDisplayListCanvas;
41 
42     private boolean mHasPendingHardwareAnimator;
43     private RenderNodeAnimatorSet mHardwareAnimator;
44 
45     private Animator mSoftwareAnimator;
46 
47     /** Whether we have an explicit maximum radius. */
48     private boolean mHasMaxRadius;
49 
50     /** How big this ripple should be when fully entered. */
51     protected float mTargetRadius;
52 
53     /** Screen density used to adjust pixel-based constants. */
54     protected float mDensityScale;
55 
56     /**
57      * If set, force all ripple animations to not run on RenderThread, even if it would be
58      * available.
59      */
60     private final boolean mForceSoftware;
61 
RippleComponent(RippleDrawable owner, Rect bounds, boolean forceSoftware)62     public RippleComponent(RippleDrawable owner, Rect bounds, boolean forceSoftware) {
63         mOwner = owner;
64         mBounds = bounds;
65         mForceSoftware = forceSoftware;
66     }
67 
onBoundsChange()68     public void onBoundsChange() {
69         if (!mHasMaxRadius) {
70             mTargetRadius = getTargetRadius(mBounds);
71             onTargetRadiusChanged(mTargetRadius);
72         }
73     }
74 
setup(float maxRadius, int densityDpi)75     public final void setup(float maxRadius, int densityDpi) {
76         if (maxRadius >= 0) {
77             mHasMaxRadius = true;
78             mTargetRadius = maxRadius;
79         } else {
80             mTargetRadius = getTargetRadius(mBounds);
81         }
82 
83         mDensityScale = densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE;
84 
85         onTargetRadiusChanged(mTargetRadius);
86     }
87 
getTargetRadius(Rect bounds)88     private static float getTargetRadius(Rect bounds) {
89         final float halfWidth = bounds.width() / 2.0f;
90         final float halfHeight = bounds.height() / 2.0f;
91         return (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
92     }
93 
94     /**
95      * Starts a ripple enter animation.
96      *
97      * @param fast whether the ripple should enter quickly
98      */
enter(boolean fast)99     public final void enter(boolean fast) {
100         cancel();
101 
102         mSoftwareAnimator = createSoftwareEnter(fast);
103 
104         if (mSoftwareAnimator != null) {
105             mSoftwareAnimator.start();
106         }
107     }
108 
109     /**
110      * Starts a ripple exit animation.
111      */
exit()112     public final void exit() {
113         cancel();
114 
115         if (mHasDisplayListCanvas) {
116             // We don't have access to a canvas here, but we expect one on the
117             // next frame. We'll start the render thread animation then.
118             mHasPendingHardwareAnimator = true;
119 
120             // Request another frame.
121             invalidateSelf();
122         } else {
123             mSoftwareAnimator = createSoftwareExit();
124             mSoftwareAnimator.start();
125         }
126     }
127 
128     /**
129      * Cancels all animations. Software animation values are left in the
130      * current state, while hardware animation values jump to the end state.
131      */
cancel()132     public void cancel() {
133         cancelSoftwareAnimations();
134         endHardwareAnimations();
135     }
136 
137     /**
138      * Ends all animations, jumping values to the end state.
139      */
end()140     public void end() {
141         endSoftwareAnimations();
142         endHardwareAnimations();
143     }
144 
145     /**
146      * Draws the ripple to the canvas, inheriting the paint's color and alpha
147      * properties.
148      *
149      * @param c the canvas to which the ripple should be drawn
150      * @param p the paint used to draw the ripple
151      * @return {@code true} if something was drawn, {@code false} otherwise
152      */
draw(Canvas c, Paint p)153     public boolean draw(Canvas c, Paint p) {
154         final boolean hasDisplayListCanvas = !mForceSoftware && c.isHardwareAccelerated()
155                 && c instanceof DisplayListCanvas;
156         if (mHasDisplayListCanvas != hasDisplayListCanvas) {
157             mHasDisplayListCanvas = hasDisplayListCanvas;
158 
159             if (!hasDisplayListCanvas) {
160                 // We've switched from hardware to non-hardware mode. Panic.
161                 endHardwareAnimations();
162             }
163         }
164 
165         if (hasDisplayListCanvas) {
166             final DisplayListCanvas hw = (DisplayListCanvas) c;
167             startPendingAnimation(hw, p);
168 
169             if (mHardwareAnimator != null) {
170                 return drawHardware(hw);
171             }
172         }
173 
174         return drawSoftware(c, p);
175     }
176 
177     /**
178      * Populates {@code bounds} with the maximum drawing bounds of the ripple
179      * relative to its center. The resulting bounds should be translated into
180      * parent drawable coordinates before use.
181      *
182      * @param bounds the rect to populate with drawing bounds
183      */
getBounds(Rect bounds)184     public void getBounds(Rect bounds) {
185         final int r = (int) Math.ceil(mTargetRadius);
186         bounds.set(-r, -r, r, r);
187     }
188 
189     /**
190      * Starts the pending hardware animation, if available.
191      *
192      * @param hw hardware canvas on which the animation should draw
193      * @param p paint whose properties the hardware canvas should use
194      */
startPendingAnimation(DisplayListCanvas hw, Paint p)195     private void startPendingAnimation(DisplayListCanvas hw, Paint p) {
196         if (mHasPendingHardwareAnimator) {
197             mHasPendingHardwareAnimator = false;
198 
199             mHardwareAnimator = createHardwareExit(new Paint(p));
200             mHardwareAnimator.start(hw);
201 
202             // Preemptively jump the software values to the end state now that
203             // the hardware exit has read whatever values it needs.
204             jumpValuesToExit();
205         }
206     }
207 
208     /**
209      * Cancels any current software animations, leaving the values in their
210      * current state.
211      */
cancelSoftwareAnimations()212     private void cancelSoftwareAnimations() {
213         if (mSoftwareAnimator != null) {
214             mSoftwareAnimator.cancel();
215             mSoftwareAnimator = null;
216         }
217     }
218 
219     /**
220      * Ends any current software animations, jumping the values to their end
221      * state.
222      */
endSoftwareAnimations()223     private void endSoftwareAnimations() {
224         if (mSoftwareAnimator != null) {
225             mSoftwareAnimator.end();
226             mSoftwareAnimator = null;
227         }
228     }
229 
230     /**
231      * Ends any pending or current hardware animations.
232      * <p>
233      * Hardware animations can't synchronize values back to the software
234      * thread, so there is no "cancel" equivalent.
235      */
endHardwareAnimations()236     private void endHardwareAnimations() {
237         if (mHardwareAnimator != null) {
238             mHardwareAnimator.end();
239             mHardwareAnimator = null;
240         }
241 
242         if (mHasPendingHardwareAnimator) {
243             mHasPendingHardwareAnimator = false;
244 
245             // Manually jump values to their exited state. Normally we'd do that
246             // later when starting the hardware exit, but we're aborting early.
247             jumpValuesToExit();
248         }
249     }
250 
invalidateSelf()251     protected final void invalidateSelf() {
252         mOwner.invalidateSelf(false);
253     }
254 
isHardwareAnimating()255     protected final boolean isHardwareAnimating() {
256         return mHardwareAnimator != null && mHardwareAnimator.isRunning()
257                 || mHasPendingHardwareAnimator;
258     }
259 
onHotspotBoundsChanged()260     protected final void onHotspotBoundsChanged() {
261         if (!mHasMaxRadius) {
262             final float halfWidth = mBounds.width() / 2.0f;
263             final float halfHeight = mBounds.height() / 2.0f;
264             final float targetRadius = (float) Math.sqrt(halfWidth * halfWidth
265                     + halfHeight * halfHeight);
266 
267             onTargetRadiusChanged(targetRadius);
268         }
269     }
270 
271     /**
272      * Called when the target radius changes.
273      *
274      * @param targetRadius the new target radius
275      */
onTargetRadiusChanged(float targetRadius)276     protected void onTargetRadiusChanged(float targetRadius) {
277         // Stub.
278     }
279 
createSoftwareEnter(boolean fast)280     protected abstract Animator createSoftwareEnter(boolean fast);
281 
createSoftwareExit()282     protected abstract Animator createSoftwareExit();
283 
createHardwareExit(Paint p)284     protected abstract RenderNodeAnimatorSet createHardwareExit(Paint p);
285 
drawHardware(DisplayListCanvas c)286     protected abstract boolean drawHardware(DisplayListCanvas c);
287 
drawSoftware(Canvas c, Paint p)288     protected abstract boolean drawSoftware(Canvas c, Paint p);
289 
290     /**
291      * Called when the hardware exit is cancelled. Jumps software values to end
292      * state to ensure that software and hardware values are synchronized.
293      */
jumpValuesToExit()294     protected abstract void jumpValuesToExit();
295 
296     public static class RenderNodeAnimatorSet {
297         private final ArrayList<RenderNodeAnimator> mAnimators = new ArrayList<>();
298 
add(RenderNodeAnimator anim)299         public void add(RenderNodeAnimator anim) {
300             mAnimators.add(anim);
301         }
302 
clear()303         public void clear() {
304             mAnimators.clear();
305         }
306 
start(DisplayListCanvas target)307         public void start(DisplayListCanvas target) {
308             if (target == null) {
309                 throw new IllegalArgumentException("Hardware canvas must be non-null");
310             }
311 
312             final ArrayList<RenderNodeAnimator> animators = mAnimators;
313             final int N = animators.size();
314             for (int i = 0; i < N; i++) {
315                 final RenderNodeAnimator anim = animators.get(i);
316                 anim.setTarget(target);
317                 anim.start();
318             }
319         }
320 
cancel()321         public void cancel() {
322             final ArrayList<RenderNodeAnimator> animators = mAnimators;
323             final int N = animators.size();
324             for (int i = 0; i < N; i++) {
325                 final RenderNodeAnimator anim = animators.get(i);
326                 anim.cancel();
327             }
328         }
329 
end()330         public void end() {
331             final ArrayList<RenderNodeAnimator> animators = mAnimators;
332             final int N = animators.size();
333             for (int i = 0; i < N; i++) {
334                 final RenderNodeAnimator anim = animators.get(i);
335                 anim.end();
336             }
337         }
338 
isRunning()339         public boolean isRunning() {
340             final ArrayList<RenderNodeAnimator> animators = mAnimators;
341             final int N = animators.size();
342             for (int i = 0; i < N; i++) {
343                 final RenderNodeAnimator anim = animators.get(i);
344                 if (anim.isRunning()) {
345                     return true;
346                 }
347             }
348             return false;
349         }
350     }
351 }
352