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