RippleComponent.java revision c3b68dff1654f95a9512027b29e8da49a15f4194
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    public RippleComponent(RippleDrawable owner, Rect bounds) {
56        mOwner = owner;
57        mBounds = bounds;
58    }
59
60    public void onBoundsChange() {
61        if (!mHasMaxRadius) {
62            mTargetRadius = getTargetRadius(mBounds);
63            onTargetRadiusChanged(mTargetRadius);
64        }
65    }
66
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
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     */
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     */
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     */
124    public void cancel() {
125        cancelSoftwareAnimations();
126        endHardwareAnimations();
127    }
128
129    /**
130     * Ends all animations, jumping values to the end state.
131     */
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     */
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     */
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     */
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     */
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     */
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     */
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
243    protected final void invalidateSelf() {
244        mOwner.invalidateSelf(false);
245    }
246
247    protected final boolean isHardwareAnimating() {
248        return mHardwareAnimator != null && mHardwareAnimator.isRunning()
249                || mHasPendingHardwareAnimator;
250    }
251
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     */
268    protected void onTargetRadiusChanged(float targetRadius) {
269        // Stub.
270    }
271
272    protected abstract Animator createSoftwareEnter(boolean fast);
273
274    protected abstract Animator createSoftwareExit();
275
276    protected abstract RenderNodeAnimatorSet createHardwareExit(Paint p);
277
278    protected abstract boolean drawHardware(DisplayListCanvas c);
279
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     */
286    protected abstract void jumpValuesToExit();
287
288    public static class RenderNodeAnimatorSet {
289        private final ArrayList<RenderNodeAnimator> mAnimators = new ArrayList<>();
290
291        public void add(RenderNodeAnimator anim) {
292            mAnimators.add(anim);
293        }
294
295        public void clear() {
296            mAnimators.clear();
297        }
298
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
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
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
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