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