RippleComponent.java revision 5bc0144302bb378e5f007baa4bf6620ab9031879
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    }
238
239    protected final void invalidateSelf() {
240        mOwner.invalidateSelf();
241    }
242
243    protected final boolean isHardwareAnimating() {
244        return mHardwareAnimator != null && mHardwareAnimator.isRunning()
245                || mHasPendingHardwareAnimator;
246    }
247
248    protected final void onHotspotBoundsChanged() {
249        if (!mHasMaxRadius) {
250            final float halfWidth = mBounds.width() / 2.0f;
251            final float halfHeight = mBounds.height() / 2.0f;
252            final float targetRadius = (float) Math.sqrt(halfWidth * halfWidth
253                    + halfHeight * halfHeight);
254
255            onTargetRadiusChanged(targetRadius);
256        }
257    }
258
259    /**
260     * Called when the target radius changes.
261     *
262     * @param targetRadius the new target radius
263     */
264    protected void onTargetRadiusChanged(float targetRadius) {
265        // Stub.
266    }
267
268    protected abstract Animator createSoftwareEnter(boolean fast);
269
270    protected abstract Animator createSoftwareExit();
271
272    protected abstract RenderNodeAnimatorSet createHardwareExit(Paint p);
273
274    protected abstract boolean drawHardware(DisplayListCanvas c);
275
276    protected abstract boolean drawSoftware(Canvas c, Paint p);
277
278    /**
279     * Called when the hardware exit is cancelled. Jumps software values to end
280     * state to ensure that software and hardware values are synchronized.
281     */
282    protected abstract void jumpValuesToExit();
283
284    public static class RenderNodeAnimatorSet {
285        private final ArrayList<RenderNodeAnimator> mAnimators = new ArrayList<>();
286
287        public void add(RenderNodeAnimator anim) {
288            mAnimators.add(anim);
289        }
290
291        public void clear() {
292            mAnimators.clear();
293        }
294
295        public void start(DisplayListCanvas target) {
296            if (target == null) {
297                throw new IllegalArgumentException("Hardware canvas must be non-null");
298            }
299
300            final ArrayList<RenderNodeAnimator> animators = mAnimators;
301            final int N = animators.size();
302            for (int i = 0; i < N; i++) {
303                final RenderNodeAnimator anim = animators.get(i);
304                anim.setTarget(target);
305                anim.start();
306            }
307        }
308
309        public void cancel() {
310            final ArrayList<RenderNodeAnimator> animators = mAnimators;
311            final int N = animators.size();
312            for (int i = 0; i < N; i++) {
313                final RenderNodeAnimator anim = animators.get(i);
314                anim.cancel();
315            }
316        }
317
318        public void end() {
319            final ArrayList<RenderNodeAnimator> animators = mAnimators;
320            final int N = animators.size();
321            for (int i = 0; i < N; i++) {
322                final RenderNodeAnimator anim = animators.get(i);
323                anim.end();
324            }
325        }
326
327        public boolean isRunning() {
328            final ArrayList<RenderNodeAnimator> animators = mAnimators;
329            final int N = animators.size();
330            for (int i = 0; i < N; i++) {
331                final RenderNodeAnimator anim = animators.get(i);
332                if (anim.isRunning()) {
333                    return true;
334                }
335            }
336            return false;
337        }
338    }
339}
340