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