RippleComponent.java revision f872ee0057ed247aa93589347f1b53afc99517f8
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.HardwareCanvas; 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 mHasHardwareCanvas; 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 (mHasHardwareCanvas) { 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 hasHardwareCanvas = c.isHardwareAccelerated() 134 && c instanceof HardwareCanvas; 135 if (mHasHardwareCanvas != hasHardwareCanvas) { 136 mHasHardwareCanvas = hasHardwareCanvas; 137 138 if (!hasHardwareCanvas) { 139 // We've switched from hardware to non-hardware mode. Panic. 140 endHardwareAnimations(); 141 } 142 } 143 144 if (hasHardwareCanvas) { 145 final HardwareCanvas hw = (HardwareCanvas) 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(HardwareCanvas 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(HardwareCanvas 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(HardwareCanvas 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