summaryrefslogtreecommitdiffstats
path: root/graphics
diff options
context:
space:
mode:
authorAlan Viverette <alanv@google.com>2015-03-09 18:25:21 -0700
committerAlan Viverette <alanv@google.com>2015-03-09 18:25:21 -0700
commitf872ee0057ed247aa93589347f1b53afc99517f8 (patch)
treef4232c93caf16cec587e30be15485c845567fdd6 /graphics
parent7b91c55b3ff4857e904a11a0a67fcc86a32868b4 (diff)
downloadframeworks_base-f872ee0057ed247aa93589347f1b53afc99517f8.zip
frameworks_base-f872ee0057ed247aa93589347f1b53afc99517f8.tar.gz
frameworks_base-f872ee0057ed247aa93589347f1b53afc99517f8.tar.bz2
Refactor ripple components, simplify background animation
Moves animation lifecycle and software/hardware hand-off into a unified RippleComponent class, which makes it easier to maintain the components. Adds better javadocs and comments to explain what's going on. Bug: 19431322 Change-Id: Ifb9b9a7dcc24238719ef12293493ca3df107679f
Diffstat (limited to 'graphics')
-rw-r--r--graphics/java/android/graphics/drawable/Ripple.java578
-rw-r--r--graphics/java/android/graphics/drawable/RippleBackground.java452
-rw-r--r--graphics/java/android/graphics/drawable/RippleComponent.java332
-rw-r--r--graphics/java/android/graphics/drawable/RippleDrawable.java97
-rw-r--r--graphics/java/android/graphics/drawable/RippleForeground.java345
5 files changed, 813 insertions, 991 deletions
diff --git a/graphics/java/android/graphics/drawable/Ripple.java b/graphics/java/android/graphics/drawable/Ripple.java
deleted file mode 100644
index 138d73a..0000000
--- a/graphics/java/android/graphics/drawable/Ripple.java
+++ /dev/null
@@ -1,578 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.graphics.drawable;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ObjectAnimator;
-import android.animation.TimeInterpolator;
-import android.graphics.Canvas;
-import android.graphics.CanvasProperty;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.util.MathUtils;
-import android.view.HardwareCanvas;
-import android.view.RenderNodeAnimator;
-import android.view.animation.LinearInterpolator;
-
-import java.util.ArrayList;
-
-/**
- * Draws a Material ripple.
- */
-class Ripple {
- private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
- private static final TimeInterpolator DECEL_INTERPOLATOR = new LogInterpolator();
-
- private static final float GLOBAL_SPEED = 1.0f;
- private static final float WAVE_TOUCH_DOWN_ACCELERATION = 1024.0f * GLOBAL_SPEED;
- private static final float WAVE_TOUCH_UP_ACCELERATION = 3400.0f * GLOBAL_SPEED;
- private static final float WAVE_OPACITY_DECAY_VELOCITY = 3.0f / GLOBAL_SPEED;
-
- private static final long RIPPLE_ENTER_DELAY = 80;
-
- // Hardware animators.
- private final ArrayList<RenderNodeAnimator> mRunningAnimations = new ArrayList<>();
-
- private final RippleDrawable mOwner;
-
- /** Bounds used for computing max radius. */
- private final Rect mBounds;
-
- /** Maximum ripple radius. */
- private float mOuterRadius;
-
- /** Screen density used to adjust pixel-based velocities. */
- private float mDensity;
-
- private float mStartingX;
- private float mStartingY;
- private float mClampedStartingX;
- private float mClampedStartingY;
-
- // Hardware rendering properties.
- private CanvasProperty<Paint> mPropPaint;
- private CanvasProperty<Float> mPropRadius;
- private CanvasProperty<Float> mPropX;
- private CanvasProperty<Float> mPropY;
-
- // Software animators.
- private ObjectAnimator mAnimRadius;
- private ObjectAnimator mAnimOpacity;
- private ObjectAnimator mAnimX;
- private ObjectAnimator mAnimY;
-
- // Temporary paint used for creating canvas properties.
- private Paint mTempPaint;
-
- // Software rendering properties.
- private float mOpacity = 1;
- private float mOuterX;
- private float mOuterY;
-
- // Values used to tween between the start and end positions.
- private float mTweenRadius = 0;
- private float mTweenX = 0;
- private float mTweenY = 0;
-
- /** Whether we should be drawing hardware animations. */
- private boolean mHardwareAnimating;
-
- /** Whether we can use hardware acceleration for the exit animation. */
- private boolean mCanUseHardware;
-
- /** Whether we have an explicit maximum radius. */
- private boolean mHasMaxRadius;
-
- /** Whether we were canceled externally and should avoid self-removal. */
- private boolean mCanceled;
-
- private boolean mHasPendingHardwareExit;
- private int mPendingRadiusDuration;
- private int mPendingOpacityDuration;
-
- /**
- * Creates a new ripple.
- */
- public Ripple(RippleDrawable owner, Rect bounds, float startingX, float startingY) {
- mOwner = owner;
- mBounds = bounds;
-
- mStartingX = startingX;
- mStartingY = startingY;
- }
-
- public void setup(float maxRadius, float density) {
- if (maxRadius >= 0) {
- mHasMaxRadius = true;
- mOuterRadius = maxRadius;
- } else {
- final float halfWidth = mBounds.width() / 2.0f;
- final float halfHeight = mBounds.height() / 2.0f;
- mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
- }
-
- mOuterX = 0;
- mOuterY = 0;
- mDensity = density;
-
- clampStartingPosition();
- }
-
- public boolean isHardwareAnimating() {
- return mHardwareAnimating;
- }
-
- private void clampStartingPosition() {
- final float cX = mBounds.exactCenterX();
- final float cY = mBounds.exactCenterY();
- final float dX = mStartingX - cX;
- final float dY = mStartingY - cY;
- final float r = mOuterRadius;
- if (dX * dX + dY * dY > r * r) {
- // Point is outside the circle, clamp to the circumference.
- final double angle = Math.atan2(dY, dX);
- mClampedStartingX = cX + (float) (Math.cos(angle) * r);
- mClampedStartingY = cY + (float) (Math.sin(angle) * r);
- } else {
- mClampedStartingX = mStartingX;
- mClampedStartingY = mStartingY;
- }
- }
-
- public void onHotspotBoundsChanged() {
- if (!mHasMaxRadius) {
- final float halfWidth = mBounds.width() / 2.0f;
- final float halfHeight = mBounds.height() / 2.0f;
- mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
-
- clampStartingPosition();
- }
- }
-
- public void setOpacity(float a) {
- mOpacity = a;
- invalidateSelf();
- }
-
- public float getOpacity() {
- return mOpacity;
- }
-
- @SuppressWarnings("unused")
- public void setRadiusGravity(float r) {
- mTweenRadius = r;
- invalidateSelf();
- }
-
- @SuppressWarnings("unused")
- public float getRadiusGravity() {
- return mTweenRadius;
- }
-
- @SuppressWarnings("unused")
- public void setXGravity(float x) {
- mTweenX = x;
- invalidateSelf();
- }
-
- @SuppressWarnings("unused")
- public float getXGravity() {
- return mTweenX;
- }
-
- @SuppressWarnings("unused")
- public void setYGravity(float y) {
- mTweenY = y;
- invalidateSelf();
- }
-
- @SuppressWarnings("unused")
- public float getYGravity() {
- return mTweenY;
- }
-
- /**
- * Draws the ripple centered at (0,0) using the specified paint.
- */
- public boolean draw(Canvas c, Paint p) {
- final boolean canUseHardware = c.isHardwareAccelerated();
- if (mCanUseHardware != canUseHardware && mCanUseHardware) {
- // We've switched from hardware to non-hardware mode. Panic.
- cancelHardwareAnimations(true);
- }
- mCanUseHardware = canUseHardware;
-
- final boolean hasContent;
- if (canUseHardware && (mHardwareAnimating || mHasPendingHardwareExit)) {
- hasContent = drawHardware((HardwareCanvas) c, p);
- } else {
- hasContent = drawSoftware(c, p);
- }
-
- return hasContent;
- }
-
- private boolean drawHardware(HardwareCanvas c, Paint p) {
- if (mHasPendingHardwareExit) {
- cancelHardwareAnimations(false);
- startPendingHardwareExit(c, p);
- }
-
- c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint);
-
- return true;
- }
-
- private boolean drawSoftware(Canvas c, Paint p) {
- boolean hasContent = false;
-
- final int paintAlpha = p.getAlpha();
- final int alpha = (int) (paintAlpha * mOpacity + 0.5f);
- final float radius = MathUtils.lerp(0, mOuterRadius, mTweenRadius);
- if (alpha > 0 && radius > 0) {
- final float x = MathUtils.lerp(
- mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX);
- final float y = MathUtils.lerp(
- mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY);
- p.setAlpha(alpha);
- c.drawCircle(x, y, radius, p);
- p.setAlpha(paintAlpha);
- hasContent = true;
- }
-
- return hasContent;
- }
-
- /**
- * Returns the maximum bounds of the ripple relative to the ripple center.
- */
- public void getBounds(Rect bounds) {
- final int outerX = (int) mOuterX;
- final int outerY = (int) mOuterY;
- final int r = (int) mOuterRadius + 1;
- bounds.set(outerX - r, outerY - r, outerX + r, outerY + r);
- }
-
- /**
- * Specifies the starting position relative to the drawable bounds. No-op if
- * the ripple has already entered.
- */
- public void move(float x, float y) {
- mStartingX = x;
- mStartingY = y;
-
- clampStartingPosition();
- }
-
- /**
- * Starts the enter animation.
- */
- public void enter() {
- cancel();
-
- final int radiusDuration = (int)
- (1000 * Math.sqrt(mOuterRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensity) + 0.5);
-
- final ObjectAnimator radius = ObjectAnimator.ofFloat(this, "radiusGravity", 1);
- radius.setAutoCancel(true);
- radius.setDuration(radiusDuration);
- radius.setInterpolator(LINEAR_INTERPOLATOR);
- radius.setStartDelay(RIPPLE_ENTER_DELAY);
-
- final ObjectAnimator cX = ObjectAnimator.ofFloat(this, "xGravity", 1);
- cX.setAutoCancel(true);
- cX.setDuration(radiusDuration);
- cX.setInterpolator(LINEAR_INTERPOLATOR);
- cX.setStartDelay(RIPPLE_ENTER_DELAY);
-
- final ObjectAnimator cY = ObjectAnimator.ofFloat(this, "yGravity", 1);
- cY.setAutoCancel(true);
- cY.setDuration(radiusDuration);
- cY.setInterpolator(LINEAR_INTERPOLATOR);
- cY.setStartDelay(RIPPLE_ENTER_DELAY);
-
- mAnimRadius = radius;
- mAnimX = cX;
- mAnimY = cY;
-
- // Enter animations always run on the UI thread, since it's unlikely
- // that anything interesting is happening until the user lifts their
- // finger.
- radius.start();
- cX.start();
- cY.start();
- }
-
- /**
- * Starts the exit animation.
- */
- public void exit() {
- final float radius = MathUtils.lerp(0, mOuterRadius, mTweenRadius);
- final float remaining;
- if (mAnimRadius != null && mAnimRadius.isRunning()) {
- remaining = mOuterRadius - radius;
- } else {
- remaining = mOuterRadius;
- }
-
- cancel();
-
- final int radiusDuration = (int) (1000 * Math.sqrt(remaining / (WAVE_TOUCH_UP_ACCELERATION
- + WAVE_TOUCH_DOWN_ACCELERATION) * mDensity) + 0.5);
- final int opacityDuration = (int) (1000 * mOpacity / WAVE_OPACITY_DECAY_VELOCITY + 0.5f);
-
- if (mCanUseHardware) {
- createPendingHardwareExit(radiusDuration, opacityDuration);
- } else {
- exitSoftware(radiusDuration, opacityDuration);
- }
- }
-
- private void createPendingHardwareExit(int radiusDuration, int opacityDuration) {
- mHasPendingHardwareExit = true;
- mPendingRadiusDuration = radiusDuration;
- mPendingOpacityDuration = opacityDuration;
-
- // The animation will start on the next draw().
- invalidateSelf();
- }
-
- private void startPendingHardwareExit(HardwareCanvas c, Paint p) {
- mHasPendingHardwareExit = false;
-
- final int radiusDuration = mPendingRadiusDuration;
- final int opacityDuration = mPendingOpacityDuration;
-
- final float startX = MathUtils.lerp(
- mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX);
- final float startY = MathUtils.lerp(
- mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY);
-
- final float startRadius = MathUtils.lerp(0, mOuterRadius, mTweenRadius);
- final Paint paint = getTempPaint(p);
- paint.setAlpha((int) (paint.getAlpha() * mOpacity + 0.5f));
- mPropPaint = CanvasProperty.createPaint(paint);
- mPropRadius = CanvasProperty.createFloat(startRadius);
- mPropX = CanvasProperty.createFloat(startX);
- mPropY = CanvasProperty.createFloat(startY);
-
- final RenderNodeAnimator radiusAnim = new RenderNodeAnimator(mPropRadius, mOuterRadius);
- radiusAnim.setDuration(radiusDuration);
- radiusAnim.setInterpolator(DECEL_INTERPOLATOR);
- radiusAnim.setTarget(c);
- radiusAnim.start();
-
- final RenderNodeAnimator xAnim = new RenderNodeAnimator(mPropX, mOuterX);
- xAnim.setDuration(radiusDuration);
- xAnim.setInterpolator(DECEL_INTERPOLATOR);
- xAnim.setTarget(c);
- xAnim.start();
-
- final RenderNodeAnimator yAnim = new RenderNodeAnimator(mPropY, mOuterY);
- yAnim.setDuration(radiusDuration);
- yAnim.setInterpolator(DECEL_INTERPOLATOR);
- yAnim.setTarget(c);
- yAnim.start();
-
- final RenderNodeAnimator opacityAnim = new RenderNodeAnimator(mPropPaint,
- RenderNodeAnimator.PAINT_ALPHA, 0);
- opacityAnim.setDuration(opacityDuration);
- opacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
- opacityAnim.addListener(mAnimationListener);
- opacityAnim.setTarget(c);
- opacityAnim.start();
-
- mRunningAnimations.add(radiusAnim);
- mRunningAnimations.add(opacityAnim);
- mRunningAnimations.add(xAnim);
- mRunningAnimations.add(yAnim);
-
- mHardwareAnimating = true;
-
- // Set up the software values to match the hardware end values.
- mOpacity = 0;
- mTweenX = 1;
- mTweenY = 1;
- mTweenRadius = 1;
- }
-
- /**
- * Jump all animations to their end state. The caller is responsible for
- * removing the ripple from the list of animating ripples.
- */
- public void jump() {
- mCanceled = true;
- endSoftwareAnimations();
- cancelHardwareAnimations(true);
- mCanceled = false;
- }
-
- private void endSoftwareAnimations() {
- if (mAnimRadius != null) {
- mAnimRadius.end();
- mAnimRadius = null;
- }
-
- if (mAnimOpacity != null) {
- mAnimOpacity.end();
- mAnimOpacity = null;
- }
-
- if (mAnimX != null) {
- mAnimX.end();
- mAnimX = null;
- }
-
- if (mAnimY != null) {
- mAnimY.end();
- mAnimY = null;
- }
- }
-
- private Paint getTempPaint(Paint original) {
- if (mTempPaint == null) {
- mTempPaint = new Paint();
- }
- mTempPaint.set(original);
- return mTempPaint;
- }
-
- private void exitSoftware(int radiusDuration, int opacityDuration) {
- final ObjectAnimator radiusAnim = ObjectAnimator.ofFloat(this, "radiusGravity", 1);
- radiusAnim.setAutoCancel(true);
- radiusAnim.setDuration(radiusDuration);
- radiusAnim.setInterpolator(DECEL_INTERPOLATOR);
-
- final ObjectAnimator xAnim = ObjectAnimator.ofFloat(this, "xGravity", 1);
- xAnim.setAutoCancel(true);
- xAnim.setDuration(radiusDuration);
- xAnim.setInterpolator(DECEL_INTERPOLATOR);
-
- final ObjectAnimator yAnim = ObjectAnimator.ofFloat(this, "yGravity", 1);
- yAnim.setAutoCancel(true);
- yAnim.setDuration(radiusDuration);
- yAnim.setInterpolator(DECEL_INTERPOLATOR);
-
- final ObjectAnimator opacityAnim = ObjectAnimator.ofFloat(this, "opacity", 0);
- opacityAnim.setAutoCancel(true);
- opacityAnim.setDuration(opacityDuration);
- opacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
- opacityAnim.addListener(mAnimationListener);
-
- mAnimRadius = radiusAnim;
- mAnimOpacity = opacityAnim;
- mAnimX = xAnim;
- mAnimY = yAnim;
-
- radiusAnim.start();
- opacityAnim.start();
- xAnim.start();
- yAnim.start();
- }
-
- /**
- * Cancels all animations. The caller is responsible for removing
- * the ripple from the list of animating ripples.
- */
- public void cancel() {
- mCanceled = true;
- cancelSoftwareAnimations();
- cancelHardwareAnimations(false);
- mCanceled = false;
- }
-
- private void cancelSoftwareAnimations() {
- if (mAnimRadius != null) {
- mAnimRadius.cancel();
- mAnimRadius = null;
- }
-
- if (mAnimOpacity != null) {
- mAnimOpacity.cancel();
- mAnimOpacity = null;
- }
-
- if (mAnimX != null) {
- mAnimX.cancel();
- mAnimX = null;
- }
-
- if (mAnimY != null) {
- mAnimY.cancel();
- mAnimY = null;
- }
- }
-
- /**
- * Cancels any running hardware animations.
- */
- private void cancelHardwareAnimations(boolean jumpToEnd) {
- final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations;
- final int N = runningAnimations.size();
- for (int i = 0; i < N; i++) {
- if (jumpToEnd) {
- runningAnimations.get(i).end();
- } else {
- runningAnimations.get(i).cancel();
- }
- }
- runningAnimations.clear();
-
- if (mHasPendingHardwareExit) {
- // If we had a pending hardware exit, jump to the end state.
- mHasPendingHardwareExit = false;
-
- if (jumpToEnd) {
- mOpacity = 0;
- mTweenX = 1;
- mTweenY = 1;
- mTweenRadius = 1;
- }
- }
-
- mHardwareAnimating = false;
- }
-
- private void removeSelf() {
- // The owner will invalidate itself.
- if (!mCanceled) {
- mOwner.removeRipple(this);
- }
- }
-
- private void invalidateSelf() {
- mOwner.invalidateSelf();
- }
-
- private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- removeSelf();
- }
- };
-
- /**
- * Interpolator with a smooth log deceleration
- */
- private static final class LogInterpolator implements TimeInterpolator {
- @Override
- public float getInterpolation(float input) {
- return 1 - (float) Math.pow(400, -input * 1.4);
- }
- }
-}
diff --git a/graphics/java/android/graphics/drawable/RippleBackground.java b/graphics/java/android/graphics/drawable/RippleBackground.java
index ef35289..6d1b1fe 100644
--- a/graphics/java/android/graphics/drawable/RippleBackground.java
+++ b/graphics/java/android/graphics/drawable/RippleBackground.java
@@ -17,432 +17,162 @@
package android.graphics.drawable;
import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.graphics.Canvas;
import android.graphics.CanvasProperty;
-import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
-import android.util.MathUtils;
+import android.util.FloatProperty;
import android.view.HardwareCanvas;
import android.view.RenderNodeAnimator;
import android.view.animation.LinearInterpolator;
-import java.util.ArrayList;
-
/**
- * Draws a Material ripple.
+ * Draws a ripple background.
*/
-class RippleBackground {
+class RippleBackground extends RippleComponent {
private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
- private static final float GLOBAL_SPEED = 1.0f;
- private static final float WAVE_OPACITY_DECAY_VELOCITY = 3.0f / GLOBAL_SPEED;
- private static final float WAVE_OUTER_OPACITY_EXIT_VELOCITY_MAX = 4.5f * GLOBAL_SPEED;
- private static final float WAVE_OUTER_OPACITY_EXIT_VELOCITY_MIN = 1.5f * GLOBAL_SPEED;
- private static final float WAVE_OUTER_SIZE_INFLUENCE_MAX = 200f;
- private static final float WAVE_OUTER_SIZE_INFLUENCE_MIN = 40f;
-
- private static final int ENTER_DURATION = 667;
- private static final int ENTER_DURATION_FAST = 100;
-
- // Hardware animators.
- private final ArrayList<RenderNodeAnimator> mRunningAnimations = new ArrayList<>();
-
- private final RippleDrawable mOwner;
-
- /** Bounds used for computing max radius. */
- private final Rect mBounds;
-
- /** ARGB color for drawing this ripple. */
- private int mColor;
-
- /** Maximum ripple radius. */
- private float mOuterRadius;
-
- /** Screen density used to adjust pixel-based velocities. */
- private float mDensity;
+ private static final int OPACITY_ENTER_DURATION = 600;
+ private static final int OPACITY_ENTER_DURATION_FAST = 120;
+ private static final int OPACITY_EXIT_DURATION = 480;
// Hardware rendering properties.
- private CanvasProperty<Paint> mPropOuterPaint;
- private CanvasProperty<Float> mPropOuterRadius;
- private CanvasProperty<Float> mPropOuterX;
- private CanvasProperty<Float> mPropOuterY;
-
- // Software animators.
- private ObjectAnimator mAnimOuterOpacity;
-
- // Temporary paint used for creating canvas properties.
- private Paint mTempPaint;
+ private CanvasProperty<Paint> mPropPaint;
+ private CanvasProperty<Float> mPropRadius;
+ private CanvasProperty<Float> mPropX;
+ private CanvasProperty<Float> mPropY;
// Software rendering properties.
- private float mOuterOpacity = 0;
- private float mOuterX;
- private float mOuterY;
-
- /** Whether we should be drawing hardware animations. */
- private boolean mHardwareAnimating;
-
- /** Whether we can use hardware acceleration for the exit animation. */
- private boolean mCanUseHardware;
-
- /** Whether we have an explicit maximum radius. */
- private boolean mHasMaxRadius;
-
- private boolean mHasPendingHardwareExit;
- private int mPendingOpacityDuration;
- private int mPendingInflectionDuration;
- private int mPendingInflectionOpacity;
+ private float mOpacity = 0;
- /**
- * Creates a new ripple.
- */
public RippleBackground(RippleDrawable owner, Rect bounds) {
- mOwner = owner;
- mBounds = bounds;
- }
-
- public void setup(float maxRadius, float density) {
- if (maxRadius >= 0) {
- mHasMaxRadius = true;
- mOuterRadius = maxRadius;
- } else {
- final float halfWidth = mBounds.width() / 2.0f;
- final float halfHeight = mBounds.height() / 2.0f;
- mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
- }
-
- mOuterX = 0;
- mOuterY = 0;
- mDensity = density;
+ super(owner, bounds);
}
- public void onHotspotBoundsChanged() {
- if (!mHasMaxRadius) {
- final float halfWidth = mBounds.width() / 2.0f;
- final float halfHeight = mBounds.height() / 2.0f;
- mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
- }
- }
-
- @SuppressWarnings("unused")
- public void setOuterOpacity(float a) {
- mOuterOpacity = a;
- invalidateSelf();
- }
-
- @SuppressWarnings("unused")
- public float getOuterOpacity() {
- return mOuterOpacity;
- }
-
- /**
- * Draws the ripple centered at (0,0) using the specified paint.
- */
- public boolean draw(Canvas c, Paint p) {
- mColor = p.getColor();
-
- final boolean canUseHardware = c.isHardwareAccelerated();
- if (mCanUseHardware != canUseHardware && mCanUseHardware) {
- // We've switched from hardware to non-hardware mode. Panic.
- cancelHardwareAnimations(true);
- }
- mCanUseHardware = canUseHardware;
-
- final boolean hasContent;
- if (canUseHardware && (mHardwareAnimating || mHasPendingHardwareExit)) {
- hasContent = drawHardware((HardwareCanvas) c, p);
- } else {
- hasContent = drawSoftware(c, p);
- }
-
- return hasContent;
+ public boolean isVisible() {
+ return mOpacity > 0 || isHardwareAnimating();
}
- public boolean shouldDraw() {
- return (mCanUseHardware && mHardwareAnimating) || (mOuterOpacity > 0 && mOuterRadius > 0);
- }
-
- private boolean drawHardware(HardwareCanvas c, Paint p) {
- if (mHasPendingHardwareExit) {
- cancelHardwareAnimations(false);
- startPendingHardwareExit(c, p);
- }
-
- c.drawCircle(mPropOuterX, mPropOuterY, mPropOuterRadius, mPropOuterPaint);
-
- return true;
- }
-
- private boolean drawSoftware(Canvas c, Paint p) {
+ @Override
+ protected boolean drawSoftware(Canvas c, Paint p) {
boolean hasContent = false;
- final int paintAlpha = p.getAlpha();
- final int alpha = (int) (paintAlpha * mOuterOpacity + 0.5f);
- final float radius = mOuterRadius;
- if (alpha > 0 && radius > 0) {
+ final int origAlpha = p.getAlpha();
+ final int alpha = (int) (origAlpha * mOpacity + 0.5f);
+ if (alpha > 0) {
p.setAlpha(alpha);
- c.drawCircle(mOuterX, mOuterY, radius, p);
- p.setAlpha(paintAlpha);
+ c.drawCircle(0, 0, mTargetRadius, p);
+ p.setAlpha(origAlpha);
hasContent = true;
}
return hasContent;
}
- /**
- * Returns the maximum bounds of the ripple relative to the ripple center.
- */
- public void getBounds(Rect bounds) {
- final int outerX = (int) mOuterX;
- final int outerY = (int) mOuterY;
- final int r = (int) mOuterRadius + 1;
- bounds.set(outerX - r, outerY - r, outerX + r, outerY + r);
+ @Override
+ protected boolean drawHardware(HardwareCanvas c) {
+ c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint);
+ return true;
}
- /**
- * Starts the enter animation.
- */
- public void enter(boolean fast) {
- cancel();
+ @Override
+ protected Animator createSoftwareEnter(boolean fast) {
+ // Linear enter based on current opacity.
+ final int maxDuration = fast ? OPACITY_ENTER_DURATION_FAST : OPACITY_ENTER_DURATION;
+ final int duration = (int) ((1 - mOpacity) * maxDuration);
- final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, "outerOpacity", 0, 1);
+ final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1);
opacity.setAutoCancel(true);
- opacity.setDuration(fast ? ENTER_DURATION_FAST : ENTER_DURATION);
+ opacity.setDuration(duration);
opacity.setInterpolator(LINEAR_INTERPOLATOR);
- mAnimOuterOpacity = opacity;
-
- // Enter animations always run on the UI thread, since it's unlikely
- // that anything interesting is happening until the user lifts their
- // finger.
- opacity.start();
- }
-
- /**
- * Starts the exit animation.
- */
- public void exit() {
- cancel();
-
- // Scale the outer max opacity and opacity velocity based
- // on the size of the outer radius.
- final int opacityDuration = (int) (1000 / WAVE_OPACITY_DECAY_VELOCITY + 0.5f);
- final float outerSizeInfluence = MathUtils.constrain(
- (mOuterRadius - WAVE_OUTER_SIZE_INFLUENCE_MIN * mDensity)
- / (WAVE_OUTER_SIZE_INFLUENCE_MAX * mDensity), 0, 1);
- final float outerOpacityVelocity = MathUtils.lerp(WAVE_OUTER_OPACITY_EXIT_VELOCITY_MIN,
- WAVE_OUTER_OPACITY_EXIT_VELOCITY_MAX, outerSizeInfluence);
-
- // Determine at what time the inner and outer opacity intersect.
- // inner(t) = mOpacity - t * WAVE_OPACITY_DECAY_VELOCITY / 1000
- // outer(t) = mOuterOpacity + t * WAVE_OUTER_OPACITY_VELOCITY / 1000
- final int inflectionDuration = Math.max(0, (int) (1000 * (1 - mOuterOpacity)
- / (WAVE_OPACITY_DECAY_VELOCITY + outerOpacityVelocity) + 0.5f));
- final int inflectionOpacity = (int) (Color.alpha(mColor) * (mOuterOpacity
- + inflectionDuration * outerOpacityVelocity * outerSizeInfluence / 1000) + 0.5f);
-
- if (mCanUseHardware) {
- createPendingHardwareExit(opacityDuration, inflectionDuration, inflectionOpacity);
- } else {
- exitSoftware(opacityDuration, inflectionDuration, inflectionOpacity);
- }
- }
-
- private void createPendingHardwareExit(
- int opacityDuration, int inflectionDuration, int inflectionOpacity) {
- mHasPendingHardwareExit = true;
- mPendingOpacityDuration = opacityDuration;
- mPendingInflectionDuration = inflectionDuration;
- mPendingInflectionOpacity = inflectionOpacity;
-
- // The animation will start on the next draw().
- invalidateSelf();
+ return opacity;
}
- private void startPendingHardwareExit(HardwareCanvas c, Paint p) {
- mHasPendingHardwareExit = false;
+ @Override
+ protected Animator createSoftwareExit() {
+ final AnimatorSet set = new AnimatorSet();
- final int opacityDuration = mPendingOpacityDuration;
- final int inflectionDuration = mPendingInflectionDuration;
- final int inflectionOpacity = mPendingInflectionOpacity;
+ // Linear exit after enter is completed.
+ final ObjectAnimator exit = ObjectAnimator.ofFloat(this, RippleBackground.OPACITY, 0);
+ exit.setInterpolator(LINEAR_INTERPOLATOR);
+ exit.setDuration(OPACITY_EXIT_DURATION);
+ exit.setAutoCancel(true);
- final Paint outerPaint = getTempPaint(p);
- outerPaint.setAlpha((int) (outerPaint.getAlpha() * mOuterOpacity + 0.5f));
- mPropOuterPaint = CanvasProperty.createPaint(outerPaint);
- mPropOuterRadius = CanvasProperty.createFloat(mOuterRadius);
- mPropOuterX = CanvasProperty.createFloat(mOuterX);
- mPropOuterY = CanvasProperty.createFloat(mOuterY);
+ final AnimatorSet.Builder builder = set.play(exit);
- final RenderNodeAnimator outerOpacityAnim;
- if (inflectionDuration > 0) {
- // Outer opacity continues to increase for a bit.
- outerOpacityAnim = new RenderNodeAnimator(mPropOuterPaint,
- RenderNodeAnimator.PAINT_ALPHA, inflectionOpacity);
- outerOpacityAnim.setDuration(inflectionDuration);
- outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
+ // Linear "fast" enter based on current opacity.
+ final int fastEnterDuration = (int) ((1 - mOpacity) * OPACITY_ENTER_DURATION_FAST);
+ if (fastEnterDuration > 0) {
+ final ObjectAnimator enter = ObjectAnimator.ofFloat(this, RippleBackground.OPACITY, 1);
+ enter.setInterpolator(LINEAR_INTERPOLATOR);
+ enter.setDuration(fastEnterDuration);
+ enter.setAutoCancel(true);
- // Chain the outer opacity exit animation.
- final int outerDuration = opacityDuration - inflectionDuration;
- if (outerDuration > 0) {
- final RenderNodeAnimator outerFadeOutAnim = new RenderNodeAnimator(
- mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0);
- outerFadeOutAnim.setDuration(outerDuration);
- outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR);
- outerFadeOutAnim.setStartDelay(inflectionDuration);
- outerFadeOutAnim.setStartValue(inflectionOpacity);
- outerFadeOutAnim.addListener(mAnimationListener);
- outerFadeOutAnim.setTarget(c);
- outerFadeOutAnim.start();
-
- mRunningAnimations.add(outerFadeOutAnim);
- } else {
- outerOpacityAnim.addListener(mAnimationListener);
- }
- } else {
- outerOpacityAnim = new RenderNodeAnimator(
- mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0);
- outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
- outerOpacityAnim.setDuration(opacityDuration);
- outerOpacityAnim.addListener(mAnimationListener);
+ builder.after(enter);
}
- outerOpacityAnim.setTarget(c);
- outerOpacityAnim.start();
-
- mRunningAnimations.add(outerOpacityAnim);
-
- mHardwareAnimating = true;
-
- // Set up the software values to match the hardware end values.
- mOuterOpacity = 0;
+ return set;
}
- /**
- * Jump all animations to their end state. The caller is responsible for
- * removing the ripple from the list of animating ripples.
- */
- public void jump() {
- endSoftwareAnimations();
- cancelHardwareAnimations(true);
- }
-
- private void endSoftwareAnimations() {
- if (mAnimOuterOpacity != null) {
- mAnimOuterOpacity.end();
- mAnimOuterOpacity = null;
- }
- }
-
- private Paint getTempPaint(Paint original) {
- if (mTempPaint == null) {
- mTempPaint = new Paint();
- }
- mTempPaint.set(original);
- return mTempPaint;
- }
-
- private void exitSoftware(int opacityDuration, int inflectionDuration, int inflectionOpacity) {
- final ObjectAnimator outerOpacityAnim;
- if (inflectionDuration > 0) {
- // Outer opacity continues to increase for a bit.
- outerOpacityAnim = ObjectAnimator.ofFloat(this,
- "outerOpacity", inflectionOpacity / 255.0f);
- outerOpacityAnim.setAutoCancel(true);
- outerOpacityAnim.setDuration(inflectionDuration);
- outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
-
- // Chain the outer opacity exit animation.
- final int outerDuration = opacityDuration - inflectionDuration;
- if (outerDuration > 0) {
- outerOpacityAnim.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- final ObjectAnimator outerFadeOutAnim = ObjectAnimator.ofFloat(
- RippleBackground.this, "outerOpacity", 0);
- outerFadeOutAnim.setAutoCancel(true);
- outerFadeOutAnim.setDuration(outerDuration);
- outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR);
- outerFadeOutAnim.addListener(mAnimationListener);
+ @Override
+ protected RenderNodeAnimatorSet createHardwareExit(Paint p) {
+ final RenderNodeAnimatorSet set = new RenderNodeAnimatorSet();
- mAnimOuterOpacity = outerFadeOutAnim;
+ final int targetAlpha = p.getAlpha();
+ final int currentAlpha = (int) (mOpacity * targetAlpha + 0.5f);
+ p.setAlpha(currentAlpha);
- outerFadeOutAnim.start();
- }
+ mPropPaint = CanvasProperty.createPaint(p);
+ mPropRadius = CanvasProperty.createFloat(mTargetRadius);
+ mPropX = CanvasProperty.createFloat(0);
+ mPropY = CanvasProperty.createFloat(0);
- @Override
- public void onAnimationCancel(Animator animation) {
- animation.removeListener(this);
- }
- });
- } else {
- outerOpacityAnim.addListener(mAnimationListener);
- }
- } else {
- outerOpacityAnim = ObjectAnimator.ofFloat(this, "outerOpacity", 0);
- outerOpacityAnim.setAutoCancel(true);
- outerOpacityAnim.setDuration(opacityDuration);
- outerOpacityAnim.addListener(mAnimationListener);
+ // Linear "fast" enter based on current opacity.
+ final int fastEnterDuration = (int) ((1 - mOpacity) * OPACITY_ENTER_DURATION_FAST);
+ if (fastEnterDuration > 0) {
+ final RenderNodeAnimator enter = new RenderNodeAnimator(
+ mPropPaint, RenderNodeAnimator.PAINT_ALPHA, targetAlpha);
+ enter.setInterpolator(LINEAR_INTERPOLATOR);
+ enter.setDuration(fastEnterDuration);
+ set.add(enter);
}
- mAnimOuterOpacity = outerOpacityAnim;
+ // Linear exit after enter is completed.
+ final RenderNodeAnimator exit = new RenderNodeAnimator(
+ mPropPaint, RenderNodeAnimator.PAINT_ALPHA, 0);
+ exit.setInterpolator(LINEAR_INTERPOLATOR);
+ exit.setDuration(OPACITY_EXIT_DURATION);
+ exit.setStartDelay(fastEnterDuration);
+ set.add(exit);
- outerOpacityAnim.start();
+ return set;
}
- /**
- * Cancel all animations. The caller is responsible for removing
- * the ripple from the list of animating ripples.
- */
- public void cancel() {
- cancelSoftwareAnimations();
- cancelHardwareAnimations(false);
+ @Override
+ protected void jumpValuesToExit() {
+ mOpacity = 0;
}
- private void cancelSoftwareAnimations() {
- if (mAnimOuterOpacity != null) {
- mAnimOuterOpacity.cancel();
- mAnimOuterOpacity = null;
+ private static abstract class BackgroundProperty extends FloatProperty<RippleBackground> {
+ public BackgroundProperty(String name) {
+ super(name);
}
}
- /**
- * Cancels any running hardware animations.
- */
- private void cancelHardwareAnimations(boolean jumpToEnd) {
- final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations;
- final int N = runningAnimations.size();
- for (int i = 0; i < N; i++) {
- if (jumpToEnd) {
- runningAnimations.get(i).end();
- } else {
- runningAnimations.get(i).cancel();
- }
- }
- runningAnimations.clear();
-
- if (mHasPendingHardwareExit) {
- // If we had a pending hardware exit, jump to the end state.
- mHasPendingHardwareExit = false;
-
- if (jumpToEnd) {
- mOuterOpacity = 0;
- }
+ private static final BackgroundProperty OPACITY = new BackgroundProperty("opacity") {
+ @Override
+ public void setValue(RippleBackground object, float value) {
+ object.mOpacity = value;
+ object.invalidateSelf();
}
- mHardwareAnimating = false;
- }
-
- private void invalidateSelf() {
- mOwner.invalidateSelf();
- }
-
- private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() {
@Override
- public void onAnimationEnd(Animator animation) {
- mHardwareAnimating = false;
+ public Float get(RippleBackground object) {
+ return object.mOpacity;
}
};
}
diff --git a/graphics/java/android/graphics/drawable/RippleComponent.java b/graphics/java/android/graphics/drawable/RippleComponent.java
new file mode 100644
index 0000000..fd3e06c
--- /dev/null
+++ b/graphics/java/android/graphics/drawable/RippleComponent.java
@@ -0,0 +1,332 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.graphics.drawable;
+
+import android.animation.Animator;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.view.HardwareCanvas;
+import android.view.RenderNodeAnimator;
+
+import java.util.ArrayList;
+
+/**
+ * Abstract class that handles hardware/software hand-off and lifecycle for
+ * animated ripple foreground and background components.
+ */
+abstract class RippleComponent {
+ private final RippleDrawable mOwner;
+
+ /** Bounds used for computing max radius. May be modified by the owner. */
+ protected final Rect mBounds;
+
+ /** Whether we can use hardware acceleration for the exit animation. */
+ private boolean mHasHardwareCanvas;
+
+ private boolean mHasPendingHardwareAnimator;
+ private RenderNodeAnimatorSet mHardwareAnimator;
+
+ private Animator mSoftwareAnimator;
+
+ /** Whether we have an explicit maximum radius. */
+ private boolean mHasMaxRadius;
+
+ /** How big this ripple should be when fully entered. */
+ protected float mTargetRadius;
+
+ /** Screen density used to adjust pixel-based constants. */
+ protected float mDensity;
+
+ public RippleComponent(RippleDrawable owner, Rect bounds) {
+ mOwner = owner;
+ mBounds = bounds;
+ }
+
+ public final void setup(float maxRadius, float density) {
+ if (maxRadius >= 0) {
+ mHasMaxRadius = true;
+ mTargetRadius = maxRadius;
+ } else {
+ final float halfWidth = mBounds.width() / 2.0f;
+ final float halfHeight = mBounds.height() / 2.0f;
+ mTargetRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
+ }
+
+ mDensity = density;
+
+ onSetup();
+ onTargetRadiusChanged(mTargetRadius);
+ }
+
+ /**
+ * Starts a ripple enter animation.
+ *
+ * @param fast whether the ripple should enter quickly
+ */
+ public final void enter(boolean fast) {
+ cancel();
+
+ mSoftwareAnimator = createSoftwareEnter(fast);
+ mSoftwareAnimator.start();
+ }
+
+ /**
+ * Starts a ripple exit animation.
+ */
+ public final void exit() {
+ cancel();
+
+ if (mHasHardwareCanvas) {
+ // We don't have access to a canvas here, but we expect one on the
+ // next frame. We'll start the render thread animation then.
+ mHasPendingHardwareAnimator = true;
+
+ // Request another frame.
+ invalidateSelf();
+ } else {
+ mSoftwareAnimator = createSoftwareExit();
+ mSoftwareAnimator.start();
+ }
+ }
+
+ /**
+ * Cancels all animations. Software animation values are left in the
+ * current state, while hardware animation values jump to the end state.
+ */
+ public void cancel() {
+ cancelSoftwareAnimations();
+ endHardwareAnimations();
+ }
+
+ /**
+ * Ends all animations, jumping values to the end state.
+ */
+ public void end() {
+ endSoftwareAnimations();
+ endHardwareAnimations();
+ }
+
+ /**
+ * Draws the ripple to the canvas, inheriting the paint's color and alpha
+ * properties.
+ *
+ * @param c the canvas to which the ripple should be drawn
+ * @param p the paint used to draw the ripple
+ * @return {@code true} if something was drawn, {@code false} otherwise
+ */
+ public boolean draw(Canvas c, Paint p) {
+ final boolean hasHardwareCanvas = c.isHardwareAccelerated()
+ && c instanceof HardwareCanvas;
+ if (mHasHardwareCanvas != hasHardwareCanvas) {
+ mHasHardwareCanvas = hasHardwareCanvas;
+
+ if (!hasHardwareCanvas) {
+ // We've switched from hardware to non-hardware mode. Panic.
+ endHardwareAnimations();
+ }
+ }
+
+ if (hasHardwareCanvas) {
+ final HardwareCanvas hw = (HardwareCanvas) c;
+ startPendingAnimation(hw, p);
+
+ if (mHardwareAnimator != null) {
+ return drawHardware(hw);
+ }
+ }
+
+ return drawSoftware(c, p);
+ }
+
+ /**
+ * Populates {@code bounds} with the maximum drawing bounds of the ripple
+ * relative to its center. The resulting bounds should be translated into
+ * parent drawable coordinates before use.
+ *
+ * @param bounds the rect to populate with drawing bounds
+ */
+ public void getBounds(Rect bounds) {
+ final int r = (int) Math.ceil(mTargetRadius);
+ bounds.set(-r, -r, r, r);
+ }
+
+ /**
+ * Starts the pending hardware animation, if available.
+ *
+ * @param hw hardware canvas on which the animation should draw
+ * @param p paint whose properties the hardware canvas should use
+ */
+ private void startPendingAnimation(HardwareCanvas hw, Paint p) {
+ if (mHasPendingHardwareAnimator) {
+ mHasPendingHardwareAnimator = false;
+
+ mHardwareAnimator = createHardwareExit(new Paint(p));
+ mHardwareAnimator.start(hw);
+
+ // Preemptively jump the software values to the end state now that
+ // the hardware exit has read whatever values it needs.
+ jumpValuesToExit();
+ }
+ }
+
+ /**
+ * Cancels any current software animations, leaving the values in their
+ * current state.
+ */
+ private void cancelSoftwareAnimations() {
+ if (mSoftwareAnimator != null) {
+ mSoftwareAnimator.cancel();
+ }
+ }
+
+ /**
+ * Ends any current software animations, jumping the values to their end
+ * state.
+ */
+ private void endSoftwareAnimations() {
+ if (mSoftwareAnimator != null) {
+ mSoftwareAnimator.end();
+ }
+ }
+
+ /**
+ * Ends any pending or current hardware animations.
+ * <p>
+ * Hardware animations can't synchronize values back to the software
+ * thread, so there is no "cancel" equivalent.
+ */
+ private void endHardwareAnimations() {
+ if (mHardwareAnimator != null) {
+ mHardwareAnimator.end();
+ mHardwareAnimator = null;
+ }
+
+ if (mHasPendingHardwareAnimator) {
+ mHasPendingHardwareAnimator = false;
+ }
+ }
+
+ protected final void invalidateSelf() {
+ mOwner.invalidateSelf();
+ }
+
+ protected final boolean isHardwareAnimating() {
+ return mHardwareAnimator != null && mHardwareAnimator.isRunning()
+ || mHasPendingHardwareAnimator;
+ }
+
+ protected final void onHotspotBoundsChanged() {
+ if (!mHasMaxRadius) {
+ final float halfWidth = mBounds.width() / 2.0f;
+ final float halfHeight = mBounds.height() / 2.0f;
+ final float targetRadius = (float) Math.sqrt(halfWidth * halfWidth
+ + halfHeight * halfHeight);
+
+ onTargetRadiusChanged(targetRadius);
+ }
+ }
+
+ /**
+ * Called when the target radius changes.
+ *
+ * @param targetRadius the new target radius
+ */
+ protected void onTargetRadiusChanged(float targetRadius) {
+ // Stub.
+ }
+
+ /**
+ * Called during ripple setup, which occurs before the first enter
+ * animation.
+ */
+ protected void onSetup() {
+ // Stub.
+ }
+
+ protected abstract Animator createSoftwareEnter(boolean fast);
+
+ protected abstract Animator createSoftwareExit();
+
+ protected abstract RenderNodeAnimatorSet createHardwareExit(Paint p);
+
+ protected abstract boolean drawHardware(HardwareCanvas c);
+
+ protected abstract boolean drawSoftware(Canvas c, Paint p);
+
+ /**
+ * Called when the hardware exit is cancelled. Jumps software values to end
+ * state to ensure that software and hardware values are synchronized.
+ */
+ protected abstract void jumpValuesToExit();
+
+ public static class RenderNodeAnimatorSet {
+ private final ArrayList<RenderNodeAnimator> mAnimators = new ArrayList<>();
+
+ public void add(RenderNodeAnimator anim) {
+ mAnimators.add(anim);
+ }
+
+ public void clear() {
+ mAnimators.clear();
+ }
+
+ public void start(HardwareCanvas target) {
+ if (target == null) {
+ throw new IllegalArgumentException("Hardware canvas must be non-null");
+ }
+
+ final ArrayList<RenderNodeAnimator> animators = mAnimators;
+ final int N = animators.size();
+ for (int i = 0; i < N; i++) {
+ final RenderNodeAnimator anim = animators.get(i);
+ anim.setTarget(target);
+ anim.start();
+ }
+ }
+
+ public void cancel() {
+ final ArrayList<RenderNodeAnimator> animators = mAnimators;
+ final int N = animators.size();
+ for (int i = 0; i < N; i++) {
+ final RenderNodeAnimator anim = animators.get(i);
+ anim.cancel();
+ }
+ }
+
+ public void end() {
+ final ArrayList<RenderNodeAnimator> animators = mAnimators;
+ final int N = animators.size();
+ for (int i = 0; i < N; i++) {
+ final RenderNodeAnimator anim = animators.get(i);
+ anim.end();
+ }
+ }
+
+ public boolean isRunning() {
+ final ArrayList<RenderNodeAnimator> animators = mAnimators;
+ final int N = animators.size();
+ for (int i = 0; i < N; i++) {
+ final RenderNodeAnimator anim = animators.get(i);
+ if (anim.isRunning()) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git a/graphics/java/android/graphics/drawable/RippleDrawable.java b/graphics/java/android/graphics/drawable/RippleDrawable.java
index c6ea91d..6d2f593 100644
--- a/graphics/java/android/graphics/drawable/RippleDrawable.java
+++ b/graphics/java/android/graphics/drawable/RippleDrawable.java
@@ -137,7 +137,7 @@ public class RippleDrawable extends LayerDrawable {
private boolean mBackgroundActive;
/** The current ripple. May be actively animating or pending entry. */
- private Ripple mRipple;
+ private RippleForeground mRipple;
/** Whether we expect to draw a ripple when visible. */
private boolean mRippleActive;
@@ -151,7 +151,7 @@ public class RippleDrawable extends LayerDrawable {
* Lazily-created array of actively animating ripples. Inactive ripples are
* pruned during draw(). The locations of these will not change.
*/
- private Ripple[] mExitingRipples;
+ private RippleForeground[] mExitingRipples;
private int mExitingRipplesCount = 0;
/** Paint used to control appearance of ripples. */
@@ -204,11 +204,11 @@ public class RippleDrawable extends LayerDrawable {
super.jumpToCurrentState();
if (mRipple != null) {
- mRipple.jump();
+ mRipple.end();
}
if (mBackground != null) {
- mBackground.jump();
+ mBackground.end();
}
cancelExitingRipples();
@@ -219,10 +219,13 @@ public class RippleDrawable extends LayerDrawable {
boolean needsDraw = false;
final int count = mExitingRipplesCount;
- final Ripple[] ripples = mExitingRipples;
+ final RippleForeground[] ripples = mExitingRipples;
for (int i = 0; i < count; i++) {
+ // If the ripple is animating on the hardware thread, we'll need to
+ // draw an additional frame after canceling to restore the software
+ // drawing path.
needsDraw |= ripples[i].isHardwareAnimating();
- ripples[i].cancel();
+ ripples[i].end();
}
if (ripples != null) {
@@ -264,11 +267,9 @@ public class RippleDrawable extends LayerDrawable {
for (int state : stateSet) {
if (state == R.attr.state_enabled) {
enabled = true;
- }
- if (state == R.attr.state_focused) {
+ } else if (state == R.attr.state_focused) {
focused = true;
- }
- if (state == R.attr.state_pressed) {
+ } else if (state == R.attr.state_pressed) {
pressed = true;
}
}
@@ -563,11 +564,11 @@ public class RippleDrawable extends LayerDrawable {
x = mHotspotBounds.exactCenterX();
y = mHotspotBounds.exactCenterY();
}
- mRipple = new Ripple(this, mHotspotBounds, x, y);
+ mRipple = new RippleForeground(this, mHotspotBounds, x, y);
}
mRipple.setup(mState.mMaxRadius, mDensity);
- mRipple.enter();
+ mRipple.enter(false);
}
/**
@@ -577,7 +578,7 @@ public class RippleDrawable extends LayerDrawable {
private void tryRippleExit() {
if (mRipple != null) {
if (mExitingRipples == null) {
- mExitingRipples = new Ripple[MAX_RIPPLES];
+ mExitingRipples = new RippleForeground[MAX_RIPPLES];
}
mExitingRipples[mExitingRipplesCount++] = mRipple;
mRipple.exit();
@@ -591,13 +592,13 @@ public class RippleDrawable extends LayerDrawable {
*/
private void clearHotspots() {
if (mRipple != null) {
- mRipple.cancel();
+ mRipple.end();
mRipple = null;
mRippleActive = false;
}
if (mBackground != null) {
- mBackground.cancel();
+ mBackground.end();
mBackground = null;
mBackgroundActive = false;
}
@@ -624,7 +625,7 @@ public class RippleDrawable extends LayerDrawable {
*/
private void onHotspotBoundsChanged() {
final int count = mExitingRipplesCount;
- final Ripple[] ripples = mExitingRipples;
+ final RippleForeground[] ripples = mExitingRipples;
for (int i = 0; i < count; i++) {
ripples[i].onHotspotBoundsChanged();
}
@@ -662,6 +663,8 @@ public class RippleDrawable extends LayerDrawable {
*/
@Override
public void draw(@NonNull Canvas canvas) {
+ pruneRipples();
+
// Clip to the dirty bounds, which will be the drawable bounds if we
// have a mask or content and the ripple bounds if we're projecting.
final Rect bounds = getDirtyBounds();
@@ -682,6 +685,26 @@ public class RippleDrawable extends LayerDrawable {
mHasValidMask = false;
}
+ private void pruneRipples() {
+ int remaining = 0;
+
+ // Move remaining entries into pruned spaces.
+ final RippleForeground[] ripples = mExitingRipples;
+ final int count = mExitingRipplesCount;
+ for (int i = 0; i < count; i++) {
+ if (!ripples[i].hasFinishedExit()) {
+ ripples[remaining++] = ripples[i];
+ }
+ }
+
+ // Null out the remaining entries.
+ for (int i = remaining; i < count; i++) {
+ ripples[i] = null;
+ }
+
+ mExitingRipplesCount = remaining;
+ }
+
/**
* @return whether we need to use a mask
*/
@@ -747,7 +770,7 @@ public class RippleDrawable extends LayerDrawable {
private int getMaskType() {
if (mRipple == null && mExitingRipplesCount <= 0
- && (mBackground == null || !mBackground.shouldDraw())) {
+ && (mBackground == null || !mBackground.isVisible())) {
// We might need a mask later.
return MASK_UNKNOWN;
}
@@ -774,36 +797,6 @@ public class RippleDrawable extends LayerDrawable {
return MASK_NONE;
}
- /**
- * Removes a ripple from the exiting ripple list.
- *
- * @param ripple the ripple to remove
- */
- void removeRipple(Ripple ripple) {
- // Ripple ripple ripple ripple. Ripple ripple.
- final Ripple[] ripples = mExitingRipples;
- final int count = mExitingRipplesCount;
- final int index = getRippleIndex(ripple);
- if (index >= 0) {
- System.arraycopy(ripples, index + 1, ripples, index, count - (index + 1));
- ripples[count - 1] = null;
- mExitingRipplesCount--;
-
- invalidateSelf();
- }
- }
-
- private int getRippleIndex(Ripple ripple) {
- final Ripple[] ripples = mExitingRipples;
- final int count = mExitingRipplesCount;
- for (int i = 0; i < count; i++) {
- if (ripples[i] == ripple) {
- return i;
- }
- }
- return -1;
- }
-
private void drawContent(Canvas canvas) {
// Draw everything except the mask.
final ChildDrawable[] array = mLayerState.mChildren;
@@ -816,10 +809,10 @@ public class RippleDrawable extends LayerDrawable {
}
private void drawBackgroundAndRipples(Canvas canvas) {
- final Ripple active = mRipple;
+ final RippleForeground active = mRipple;
final RippleBackground background = mBackground;
final int count = mExitingRipplesCount;
- if (active == null && count <= 0 && (background == null || !background.shouldDraw())) {
+ if (active == null && count <= 0 && (background == null || !background.isVisible())) {
// Move along, nothing to draw here.
return;
}
@@ -859,12 +852,12 @@ public class RippleDrawable extends LayerDrawable {
p.setShader(null);
}
- if (background != null && background.shouldDraw()) {
+ if (background != null && background.isVisible()) {
background.draw(canvas, p);
}
if (count > 0) {
- final Ripple[] ripples = mExitingRipples;
+ final RippleForeground[] ripples = mExitingRipples;
for (int i = 0; i < count; i++) {
ripples[i].draw(canvas, p);
}
@@ -902,7 +895,7 @@ public class RippleDrawable extends LayerDrawable {
final int cY = (int) mHotspotBounds.exactCenterY();
final Rect rippleBounds = mTempRect;
- final Ripple[] activeRipples = mExitingRipples;
+ final RippleForeground[] activeRipples = mExitingRipples;
final int N = mExitingRipplesCount;
for (int i = 0; i < N; i++) {
activeRipples[i].getBounds(rippleBounds);
diff --git a/graphics/java/android/graphics/drawable/RippleForeground.java b/graphics/java/android/graphics/drawable/RippleForeground.java
new file mode 100644
index 0000000..2023f04
--- /dev/null
+++ b/graphics/java/android/graphics/drawable/RippleForeground.java
@@ -0,0 +1,345 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.graphics.drawable;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
+import android.graphics.Canvas;
+import android.graphics.CanvasProperty;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.util.FloatProperty;
+import android.util.MathUtils;
+import android.view.HardwareCanvas;
+import android.view.RenderNodeAnimator;
+import android.view.animation.LinearInterpolator;
+
+/**
+ * Draws a ripple foreground.
+ */
+class RippleForeground extends RippleComponent {
+ private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
+ private static final TimeInterpolator DECELERATE_INTERPOLATOR = new LogDecelerateInterpolator(
+ 400f, 1.4f, 0);
+
+ // Pixel-based accelerations and velocities.
+ private static final float WAVE_TOUCH_DOWN_ACCELERATION = 1024;
+ private static final float WAVE_TOUCH_UP_ACCELERATION = 3400;
+ private static final float WAVE_OPACITY_DECAY_VELOCITY = 3;
+
+ private static final int RIPPLE_ENTER_DELAY = 80;
+ private static final int OPACITY_ENTER_DURATION_FAST = 120;
+
+ private float mStartingX;
+ private float mStartingY;
+ private float mClampedStartingX;
+ private float mClampedStartingY;
+
+ // Hardware rendering properties.
+ private CanvasProperty<Paint> mPropPaint;
+ private CanvasProperty<Float> mPropRadius;
+ private CanvasProperty<Float> mPropX;
+ private CanvasProperty<Float> mPropY;
+
+ // Software rendering properties.
+ private float mOpacity = 1;
+ private float mOuterX;
+ private float mOuterY;
+
+ // Values used to tween between the start and end positions.
+ private float mTweenRadius = 0;
+ private float mTweenX = 0;
+ private float mTweenY = 0;
+
+ /** Whether this ripple has finished its exit animation. */
+ private boolean mHasFinishedExit;
+
+ public RippleForeground(RippleDrawable owner, Rect bounds, float startingX, float startingY) {
+ super(owner, bounds);
+
+ mStartingX = startingX;
+ mStartingY = startingY;
+ }
+
+ @Override
+ public void onSetup() {
+ mOuterX = 0;
+ mOuterY = 0;
+ }
+
+ @Override
+ protected void onTargetRadiusChanged(float targetRadius) {
+ clampStartingPosition();
+ }
+
+ @Override
+ protected boolean drawSoftware(Canvas c, Paint p) {
+ boolean hasContent = false;
+
+ final int origAlpha = p.getAlpha();
+ final int alpha = (int) (origAlpha * mOpacity + 0.5f);
+ final float radius = MathUtils.lerp(0, mTargetRadius, mTweenRadius);
+ if (alpha > 0 && radius > 0) {
+ final float x = MathUtils.lerp(
+ mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX);
+ final float y = MathUtils.lerp(
+ mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY);
+ p.setAlpha(alpha);
+ c.drawCircle(x, y, radius, p);
+ p.setAlpha(origAlpha);
+ hasContent = true;
+ }
+
+ return hasContent;
+ }
+
+ @Override
+ protected boolean drawHardware(HardwareCanvas c) {
+ c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint);
+ return true;
+ }
+
+ /**
+ * Returns the maximum bounds of the ripple relative to the ripple center.
+ */
+ public void getBounds(Rect bounds) {
+ final int outerX = (int) mOuterX;
+ final int outerY = (int) mOuterY;
+ final int r = (int) mTargetRadius + 1;
+ bounds.set(outerX - r, outerY - r, outerX + r, outerY + r);
+ }
+
+ /**
+ * Specifies the starting position relative to the drawable bounds. No-op if
+ * the ripple has already entered.
+ */
+ public void move(float x, float y) {
+ mStartingX = x;
+ mStartingY = y;
+
+ clampStartingPosition();
+ }
+
+ /**
+ * @return {@code true} if this ripple has finished its exit animation
+ */
+ public boolean hasFinishedExit() {
+ return mHasFinishedExit;
+ }
+
+ @Override
+ protected Animator createSoftwareEnter(boolean fast) {
+ final int duration = (int)
+ (1000 * Math.sqrt(mTargetRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensity) + 0.5);
+
+ final ObjectAnimator tweenAll = ObjectAnimator.ofFloat(this, TWEEN_ALL, 1);
+ tweenAll.setAutoCancel(true);
+ tweenAll.setDuration(duration);
+ tweenAll.setInterpolator(LINEAR_INTERPOLATOR);
+ tweenAll.setStartDelay(RIPPLE_ENTER_DELAY);
+
+ final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1);
+ opacity.setAutoCancel(true);
+ opacity.setDuration(OPACITY_ENTER_DURATION_FAST);
+ opacity.setInterpolator(LINEAR_INTERPOLATOR);
+
+ final AnimatorSet set = new AnimatorSet();
+ set.play(tweenAll).with(opacity);
+
+ return set;
+ }
+
+ private int getRadiusExitDuration() {
+ final float radius = MathUtils.lerp(0, mTargetRadius, mTweenRadius);
+ final float remaining = mTargetRadius - radius;
+ return (int) (1000 * Math.sqrt(remaining / (WAVE_TOUCH_UP_ACCELERATION
+ + WAVE_TOUCH_DOWN_ACCELERATION) * mDensity) + 0.5);
+ }
+
+ private int getOpacityExitDuration() {
+ return (int) (1000 * mOpacity / WAVE_OPACITY_DECAY_VELOCITY + 0.5f);
+ }
+
+ @Override
+ protected Animator createSoftwareExit() {
+ final int radiusDuration = getRadiusExitDuration();
+ final int opacityDuration = getOpacityExitDuration();
+
+ final ObjectAnimator tweenAll = ObjectAnimator.ofFloat(this, TWEEN_ALL, 1);
+ tweenAll.setAutoCancel(true);
+ tweenAll.setDuration(radiusDuration);
+ tweenAll.setInterpolator(DECELERATE_INTERPOLATOR);
+
+ final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 0);
+ opacity.setAutoCancel(true);
+ opacity.setDuration(opacityDuration);
+ opacity.setInterpolator(LINEAR_INTERPOLATOR);
+
+ final AnimatorSet set = new AnimatorSet();
+ set.play(tweenAll).with(opacity);
+ set.addListener(mAnimationListener);
+
+ return set;
+ }
+
+ @Override
+ protected RenderNodeAnimatorSet createHardwareExit(Paint p) {
+ final int radiusDuration = getRadiusExitDuration();
+ final int opacityDuration = getOpacityExitDuration();
+
+ final float startX = MathUtils.lerp(
+ mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX);
+ final float startY = MathUtils.lerp(
+ mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY);
+
+ final float startRadius = MathUtils.lerp(0, mTargetRadius, mTweenRadius);
+ p.setAlpha((int) (p.getAlpha() * mOpacity + 0.5f));
+
+ mPropPaint = CanvasProperty.createPaint(p);
+ mPropRadius = CanvasProperty.createFloat(startRadius);
+ mPropX = CanvasProperty.createFloat(startX);
+ mPropY = CanvasProperty.createFloat(startY);
+
+ final RenderNodeAnimator radius = new RenderNodeAnimator(mPropRadius, mTargetRadius);
+ radius.setDuration(radiusDuration);
+ radius.setInterpolator(DECELERATE_INTERPOLATOR);
+
+ final RenderNodeAnimator x = new RenderNodeAnimator(mPropX, mOuterX);
+ x.setDuration(radiusDuration);
+ x.setInterpolator(DECELERATE_INTERPOLATOR);
+
+ final RenderNodeAnimator y = new RenderNodeAnimator(mPropY, mOuterY);
+ y.setDuration(radiusDuration);
+ y.setInterpolator(DECELERATE_INTERPOLATOR);
+
+ final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint,
+ RenderNodeAnimator.PAINT_ALPHA, 0);
+ opacity.setDuration(opacityDuration);
+ opacity.setInterpolator(LINEAR_INTERPOLATOR);
+ opacity.addListener(mAnimationListener);
+
+ final RenderNodeAnimatorSet set = new RenderNodeAnimatorSet();
+ set.add(radius);
+ set.add(opacity);
+ set.add(x);
+ set.add(y);
+
+ return set;
+ }
+
+ @Override
+ protected void jumpValuesToExit() {
+ mOpacity = 0;
+ mTweenX = 1;
+ mTweenY = 1;
+ mTweenRadius = 1;
+ }
+
+ /**
+ * Clamps the starting position to fit within the ripple bounds.
+ */
+ private void clampStartingPosition() {
+ final float cX = mBounds.exactCenterX();
+ final float cY = mBounds.exactCenterY();
+ final float dX = mStartingX - cX;
+ final float dY = mStartingY - cY;
+ final float r = mTargetRadius;
+ if (dX * dX + dY * dY > r * r) {
+ // Point is outside the circle, clamp to the perimeter.
+ final double angle = Math.atan2(dY, dX);
+ mClampedStartingX = cX + (float) (Math.cos(angle) * r);
+ mClampedStartingY = cY + (float) (Math.sin(angle) * r);
+ } else {
+ mClampedStartingX = mStartingX;
+ mClampedStartingY = mStartingY;
+ }
+ }
+
+ private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ mHasFinishedExit = true;
+ }
+ };
+
+ /**
+ * Interpolator with a smooth log deceleration.
+ */
+ private static final class LogDecelerateInterpolator implements TimeInterpolator {
+ private final float mBase;
+ private final float mDrift;
+ private final float mTimeScale;
+ private final float mOutputScale;
+
+ public LogDecelerateInterpolator(float base, float timeScale, float drift) {
+ mBase = base;
+ mDrift = drift;
+ mTimeScale = 1f / timeScale;
+
+ mOutputScale = 1f / computeLog(1f);
+ }
+
+ private float computeLog(float t) {
+ return 1f - (float) Math.pow(mBase, -t * mTimeScale) + (mDrift * t);
+ }
+
+ @Override
+ public float getInterpolation(float t) {
+ return computeLog(t) * mOutputScale;
+ }
+ }
+
+ /**
+ * Property for animating radius, center X, and center Y between their
+ * initial and target values.
+ */
+ private static final FloatProperty<RippleForeground> TWEEN_ALL =
+ new FloatProperty<RippleForeground>("tweenAll") {
+ @Override
+ public void setValue(RippleForeground object, float value) {
+ object.mTweenRadius = value;
+ object.mTweenX = value;
+ object.mTweenY = value;
+ object.invalidateSelf();
+ }
+
+ @Override
+ public Float get(RippleForeground object) {
+ return object.mTweenRadius;
+ }
+ };
+
+ /**
+ * Property for animating opacity between 0 and its target value.
+ */
+ private static final FloatProperty<RippleForeground> OPACITY =
+ new FloatProperty<RippleForeground>("opacity") {
+ @Override
+ public void setValue(RippleForeground object, float value) {
+ object.mOpacity = value;
+ object.invalidateSelf();
+ }
+
+ @Override
+ public Float get(RippleForeground object) {
+ return object.mOpacity;
+ }
+ };
+}