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