Ripple.java revision c3f35b01b5a21e110ca4eedf09c8c6164ab85dfb
1/* 2 * Copyright (C) 2013 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.TimeInterpolator; 20import android.graphics.Canvas; 21import android.graphics.Paint; 22import android.graphics.Paint.Style; 23import android.graphics.Rect; 24import android.util.MathUtils; 25import android.view.animation.AnimationUtils; 26import android.view.animation.DecelerateInterpolator; 27 28/** 29 * Draws a Quantum Paper ripple. 30 */ 31class Ripple { 32 private static final TimeInterpolator INTERPOLATOR = new DecelerateInterpolator(2.0f); 33 34 /** Starting radius for a ripple. */ 35 private static final int STARTING_RADIUS_DP = 16; 36 37 /** Radius when finger is outside view bounds. */ 38 private static final int OUTSIDE_RADIUS_DP = 16; 39 40 /** Margin when constraining outside touches (fraction of outer radius). */ 41 private static final float OUTSIDE_MARGIN = 0.8f; 42 43 /** Resistance factor when constraining outside touches. */ 44 private static final float OUTSIDE_RESISTANCE = 0.7f; 45 46 /** Minimum alpha value during a pulse animation. */ 47 private static final int PULSE_MIN_ALPHA = 128; 48 49 private final Rect mBounds; 50 private final Rect mPadding; 51 52 private RippleAnimator mAnimator; 53 54 private int mMinRadius; 55 private int mOutsideRadius; 56 57 /** Center x-coordinate. */ 58 private float mX; 59 60 /** Center y-coordinate. */ 61 private float mY; 62 63 /** Whether the center is within the parent bounds. */ 64 private boolean mInside; 65 66 /** Whether to pulse this ripple. */ 67 boolean mPulse; 68 69 /** Enter state. A value in [0...1] or -1 if not set. */ 70 float mEnterState = -1; 71 72 /** Exit state. A value in [0...1] or -1 if not set. */ 73 float mExitState = -1; 74 75 /** Outside state. A value in [0...1] or -1 if not set. */ 76 float mOutsideState = -1; 77 78 /** Pulse state. A value in [0...1] or -1 if not set. */ 79 float mPulseState = -1; 80 81 /** 82 * Creates a new ripple with the specified parent bounds, padding, initial 83 * position, and screen density. 84 */ 85 public Ripple(Rect bounds, Rect padding, float x, float y, float density, boolean pulse) { 86 mBounds = bounds; 87 mPadding = padding; 88 mInside = mBounds.contains((int) x, (int) y); 89 mPulse = pulse; 90 91 mX = x; 92 mY = y; 93 94 mMinRadius = (int) (density * STARTING_RADIUS_DP + 0.5f); 95 mOutsideRadius = (int) (density * OUTSIDE_RADIUS_DP + 0.5f); 96 } 97 98 public void setMinRadius(int minRadius) { 99 mMinRadius = minRadius; 100 } 101 102 public void setOutsideRadius(int outsideRadius) { 103 mOutsideRadius = outsideRadius; 104 } 105 106 /** 107 * Updates the center coordinates. 108 */ 109 public void move(float x, float y) { 110 mX = x; 111 mY = y; 112 113 final boolean inside = mBounds.contains((int) x, (int) y); 114 if (mInside != inside) { 115 if (mAnimator != null) { 116 mAnimator.outside(); 117 } 118 mInside = inside; 119 } 120 } 121 122 public void onBoundsChanged() { 123 final boolean inside = mBounds.contains((int) mX, (int) mY); 124 if (mInside != inside) { 125 if (mAnimator != null) { 126 mAnimator.outside(); 127 } 128 mInside = inside; 129 } 130 } 131 132 public RippleAnimator animate() { 133 if (mAnimator == null) { 134 mAnimator = new RippleAnimator(this); 135 } 136 return mAnimator; 137 } 138 139 public boolean draw(Canvas c, Paint p) { 140 final Rect bounds = mBounds; 141 final Rect padding = mPadding; 142 final float dX = Math.max(mX, bounds.right - mX); 143 final float dY = Math.max(mY, bounds.bottom - mY); 144 final int maxRadius = (int) Math.ceil(Math.sqrt(dX * dX + dY * dY)); 145 146 final float enterState = mEnterState; 147 final float exitState = mExitState; 148 final float outsideState = mOutsideState; 149 final float pulseState = mPulseState; 150 final float insideRadius = MathUtils.lerp(mMinRadius, maxRadius, enterState); 151 final float outerRadius = MathUtils.lerp(mOutsideRadius, insideRadius, 152 mInside ? outsideState : 1 - outsideState); 153 154 // Apply resistance effect when outside bounds. 155 final float x = looseConstrain(mX, bounds.left + padding.left, bounds.right - padding.right, 156 outerRadius * OUTSIDE_MARGIN, OUTSIDE_RESISTANCE); 157 final float y = looseConstrain(mY, bounds.top + padding.top, bounds.bottom - padding.bottom, 158 outerRadius * OUTSIDE_MARGIN, OUTSIDE_RESISTANCE); 159 160 // Compute maximum alpha, taking pulse into account when active. 161 final int maxAlpha; 162 if (pulseState < 0 || pulseState >= 1) { 163 maxAlpha = 255; 164 } else { 165 final float pulseAlpha; 166 if (pulseState > 0.5) { 167 // Pulsing in to max alpha. 168 pulseAlpha = MathUtils.lerp(PULSE_MIN_ALPHA, 255, (pulseState - .5f) * 2); 169 } else { 170 // Pulsing out to min alpha. 171 pulseAlpha = MathUtils.lerp(255, PULSE_MIN_ALPHA, pulseState * 2f); 172 } 173 174 if (exitState > 0) { 175 // Animating exit, interpolate pulse with exit state. 176 maxAlpha = (int) (MathUtils.lerp(255, pulseAlpha, exitState) + 0.5f); 177 } else if (mInside) { 178 // No animation, no need to interpolate. 179 maxAlpha = (int) (pulseAlpha + 0.5f); 180 } else { 181 // Animating inside, interpolate pulse with inside state. 182 maxAlpha = (int) (MathUtils.lerp(pulseAlpha, 255, outsideState) + 0.5f); 183 } 184 } 185 186 if (maxAlpha > 0) { 187 if (exitState <= 0) { 188 // Exit state isn't showing, so we can simplify to a solid 189 // circle. 190 if (outerRadius > 0) { 191 p.setAlpha(maxAlpha); 192 p.setStyle(Style.FILL); 193 c.drawCircle(x, y, outerRadius, p); 194 return true; 195 } 196 } else { 197 // Both states are showing, so we need a circular stroke. 198 final float innerRadius = MathUtils.lerp(0, outerRadius, exitState); 199 final float strokeWidth = outerRadius - innerRadius; 200 if (strokeWidth > 0) { 201 final float strokeRadius = innerRadius + strokeWidth / 2f; 202 final int alpha = (int) (MathUtils.lerp(maxAlpha, 0, exitState) + 0.5f); 203 if (alpha > 0) { 204 p.setAlpha(alpha); 205 p.setStyle(Style.STROKE); 206 p.setStrokeWidth(strokeWidth); 207 c.drawCircle(x, y, strokeRadius, p); 208 return true; 209 } 210 } 211 } 212 } 213 214 return false; 215 } 216 217 public void getBounds(Rect bounds) { 218 final int x = (int) mX; 219 final int y = (int) mY; 220 final int dX = Math.max(x, mBounds.right - x); 221 final int dY = Math.max(x, mBounds.bottom - y); 222 final int maxRadius = (int) Math.ceil(Math.sqrt(dX * dX + dY * dY)); 223 bounds.set(x - maxRadius, y - maxRadius, x + maxRadius, y + maxRadius); 224 } 225 226 /** 227 * Constrains a value within a specified asymptotic margin outside a minimum 228 * and maximum. 229 */ 230 private static float looseConstrain(float value, float min, float max, float margin, 231 float factor) { 232 if (value < min) { 233 return min - Math.min(margin, (float) Math.pow(min - value, factor)); 234 } else if (value > max) { 235 return max + Math.min(margin, (float) Math.pow(value - max, factor)); 236 } else { 237 return value; 238 } 239 } 240 241 public static class RippleAnimator { 242 /** Duration for animating the trailing edge of the ripple. */ 243 private static final int EXIT_DURATION = 600; 244 245 /** Duration for animating the leading edge of the ripple. */ 246 private static final int ENTER_DURATION = 400; 247 248 /** Minimum elapsed time between start of enter and exit animations. */ 249 private static final int EXIT_MIN_DELAY = 200; 250 251 /** Duration for animating between inside and outside touch. */ 252 private static final int OUTSIDE_DURATION = 300; 253 254 /** Duration for animating pulses. */ 255 private static final int PULSE_DURATION = 400; 256 257 /** Interval between pulses while inside and fully entered. */ 258 private static final int PULSE_INTERVAL = 400; 259 260 /** Delay before pulses start. */ 261 private static final int PULSE_DELAY = 500; 262 263 /** The target ripple being animated. */ 264 private final Ripple mTarget; 265 266 /** When the ripple started appearing. */ 267 private long mEnterTime = -1; 268 269 /** When the ripple started vanishing. */ 270 private long mExitTime = -1; 271 272 /** When the ripple last transitioned between inside and outside touch. */ 273 private long mOutsideTime = -1; 274 275 public RippleAnimator(Ripple target) { 276 mTarget = target; 277 } 278 279 /** 280 * Starts the enter animation. 281 */ 282 public void enter() { 283 mEnterTime = AnimationUtils.currentAnimationTimeMillis(); 284 } 285 286 /** 287 * Starts the exit animation. If {@link #enter()} was called recently, the 288 * animation may be postponed. 289 */ 290 public void exit() { 291 final long minTime = mEnterTime + EXIT_MIN_DELAY; 292 mExitTime = Math.max(minTime, AnimationUtils.currentAnimationTimeMillis()); 293 } 294 295 /** 296 * Starts the outside transition animation. 297 */ 298 public void outside() { 299 mOutsideTime = AnimationUtils.currentAnimationTimeMillis(); 300 } 301 302 /** 303 * Returns whether this ripple is currently animating. 304 */ 305 public boolean isRunning() { 306 final long currentTime = AnimationUtils.currentAnimationTimeMillis(); 307 return mEnterTime >= 0 && mEnterTime <= currentTime 308 && (mExitTime < 0 || currentTime <= mExitTime + EXIT_DURATION); 309 } 310 311 public void update() { 312 // Track three states: 313 // - Enter: touch begins, affects outer radius 314 // - Outside: touch moves outside bounds, affects maximum outer radius 315 // - Exit: touch ends, affects inner radius 316 final long currentTime = AnimationUtils.currentAnimationTimeMillis(); 317 mTarget.mEnterState = mEnterTime < 0 ? 0 : INTERPOLATOR.getInterpolation( 318 MathUtils.constrain((currentTime - mEnterTime) / (float) ENTER_DURATION, 0, 1)); 319 mTarget.mExitState = mExitTime < 0 ? 0 : INTERPOLATOR.getInterpolation( 320 MathUtils.constrain((currentTime - mExitTime) / (float) EXIT_DURATION, 0, 1)); 321 mTarget.mOutsideState = mOutsideTime < 0 ? 1 : INTERPOLATOR.getInterpolation( 322 MathUtils.constrain((currentTime - mOutsideTime) / (float) OUTSIDE_DURATION, 0, 1)); 323 324 // Pulse is a little more complicated. 325 if (mTarget.mPulse) { 326 final long pulseTime = (currentTime - mEnterTime - ENTER_DURATION - PULSE_DELAY); 327 mTarget.mPulseState = pulseTime < 0 ? -1 328 : (pulseTime % (PULSE_INTERVAL + PULSE_DURATION)) / (float) PULSE_DURATION; 329 } 330 } 331 } 332} 333