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