import android.util.MathUtils;
import android.view.HardwareCanvas;
import android.view.RenderNodeAnimator;
-import android.view.animation.DecelerateInterpolator;
import android.view.animation.LinearInterpolator;
import java.util.ArrayList;
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 float WAVE_OUTER_OPACITY_VELOCITY_MAX = 4.5f * GLOBAL_SPEED;
- private static final float WAVE_OUTER_OPACITY_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 long RIPPLE_ENTER_DELAY = 80;
// Hardware animators.
- private final ArrayList<RenderNodeAnimator> mRunningAnimations = new ArrayList<>();
- private final ArrayList<RenderNodeAnimator> mPendingAnimations = new ArrayList<>();
+ private final ArrayList<RenderNodeAnimator> mRunningAnimations =
+ new ArrayList<RenderNodeAnimator>();
+ private final ArrayList<RenderNodeAnimator> mPendingAnimations =
+ new ArrayList<RenderNodeAnimator>();
private final RippleDrawable mOwner;
private CanvasProperty<Float> mPropRadius;
private CanvasProperty<Float> mPropX;
private CanvasProperty<Float> mPropY;
- private CanvasProperty<Paint> mPropOuterPaint;
- private CanvasProperty<Float> mPropOuterRadius;
- private CanvasProperty<Float> mPropOuterX;
- private CanvasProperty<Float> mPropOuterY;
// Software animators.
private ObjectAnimator mAnimRadius;
private ObjectAnimator mAnimOpacity;
- private ObjectAnimator mAnimOuterOpacity;
private ObjectAnimator mAnimX;
private ObjectAnimator mAnimY;
+ // Temporary paint used for creating canvas properties.
+ private Paint mTempPaint;
+
// Software rendering properties.
- private float mOuterOpacity = 0;
private float mOpacity = 1;
private float mOuterX;
private float mOuterY;
return mOpacity;
}
- public void setOuterOpacity(float a) {
- mOuterOpacity = a;
- invalidateSelf();
- }
-
- public float getOuterOpacity() {
- return mOuterOpacity;
- }
-
+ @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;
}
// If we have any pending hardware animations, cancel any running
// animations and start those now.
final ArrayList<RenderNodeAnimator> pendingAnimations = mPendingAnimations;
- final int N = pendingAnimations == null ? 0 : pendingAnimations.size();
+ final int N = pendingAnimations.size();
if (N > 0) {
cancelHardwareAnimations();
pendingAnimations.clear();
}
- c.drawCircle(mPropOuterX, mPropOuterY, mPropOuterRadius, mPropOuterPaint);
c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint);
return true;
// Cache the paint alpha so we can restore it later.
final int paintAlpha = p.getAlpha();
-
- final int outerAlpha = (int) (paintAlpha * mOuterOpacity + 0.5f);
- if (outerAlpha > 0 && mOuterRadius > 0) {
- p.setAlpha(outerAlpha);
- p.setStyle(Style.FILL);
- c.drawCircle(mOuterX, mOuterY, mOuterRadius, p);
- hasContent = true;
- }
-
final int alpha = (int) (paintAlpha * mOpacity + 0.5f);
final float radius = MathUtils.lerp(0, mOuterRadius, mTweenRadius);
if (alpha > 0 && radius > 0) {
public void enter() {
final int radiusDuration = (int)
(1000 * Math.sqrt(mOuterRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensity) + 0.5);
- final int outerDuration = (int) (1000 * 1.0f / WAVE_OUTER_OPACITY_VELOCITY_MIN);
final ObjectAnimator radius = ObjectAnimator.ofFloat(this, "radiusGravity", 1);
radius.setAutoCancel(true);
cY.setInterpolator(LINEAR_INTERPOLATOR);
cY.setStartDelay(RIPPLE_ENTER_DELAY);
- final ObjectAnimator outer = ObjectAnimator.ofFloat(this, "outerOpacity", 0, 1);
- outer.setAutoCancel(true);
- outer.setDuration(outerDuration);
- outer.setInterpolator(LINEAR_INTERPOLATOR);
-
mAnimRadius = radius;
- mAnimOuterOpacity = outer;
mAnimX = cX;
mAnimY = cY;
// that anything interesting is happening until the user lifts their
// finger.
radius.start();
- outer.start();
cX.start();
cY.start();
}
+ WAVE_TOUCH_DOWN_ACCELERATION) * mDensity) + 0.5);
final int opacityDuration = (int) (1000 * mOpacity / WAVE_OPACITY_DECAY_VELOCITY + 0.5f);
- // Scale the outer max opacity and opacity velocity based
- // on the size of the outer radius
-
- float outerSizeInfluence = MathUtils.constrain(
- (mOuterRadius - WAVE_OUTER_SIZE_INFLUENCE_MIN * mDensity)
- / (WAVE_OUTER_SIZE_INFLUENCE_MAX * mDensity), 0, 1);
- float outerOpacityVelocity = MathUtils.lerp(WAVE_OUTER_OPACITY_VELOCITY_MIN,
- WAVE_OUTER_OPACITY_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 outerInflection = Math.max(0, (int) (1000 * (mOpacity - mOuterOpacity)
- / (WAVE_OPACITY_DECAY_VELOCITY + outerOpacityVelocity) + 0.5f));
- final int inflectionOpacity = (int) (255 * (mOuterOpacity + outerInflection
- * outerOpacityVelocity * outerSizeInfluence / 1000) + 0.5f);
-
if (mCanUseHardware) {
- exitHardware(radiusDuration, opacityDuration, outerInflection, inflectionOpacity);
+ exitHardware(radiusDuration, opacityDuration);
} else {
- exitSoftware(radiusDuration, opacityDuration, outerInflection, inflectionOpacity);
+ exitSoftware(radiusDuration, opacityDuration);
}
}
- private void exitHardware(int radiusDuration, int opacityDuration, int outerInflection,
- int inflectionOpacity) {
+ private void exitHardware(int radiusDuration, int opacityDuration) {
mPendingAnimations.clear();
final float startX = MathUtils.lerp(
mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX);
final float startY = MathUtils.lerp(
mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY);
- final Paint outerPaint = new Paint();
- outerPaint.setAntiAlias(true);
- outerPaint.setColor(mColor);
- outerPaint.setAlpha((int) (255 * mOuterOpacity + 0.5f));
- outerPaint.setStyle(Style.FILL);
- mPropOuterPaint = CanvasProperty.createPaint(outerPaint);
- mPropOuterRadius = CanvasProperty.createFloat(mOuterRadius);
- mPropOuterX = CanvasProperty.createFloat(mOuterX);
- mPropOuterY = CanvasProperty.createFloat(mOuterY);
final float startRadius = MathUtils.lerp(0, mOuterRadius, mTweenRadius);
- final Paint paint = new Paint();
+ final Paint paint = getTempPaint();
paint.setAntiAlias(true);
paint.setColor(mColor);
paint.setAlpha((int) (255 * mOpacity + 0.5f));
RenderNodeAnimator.PAINT_ALPHA, 0);
opacityAnim.setDuration(opacityDuration);
opacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
-
- final RenderNodeAnimator outerOpacityAnim;
- if (outerInflection > 0) {
- // Outer opacity continues to increase for a bit.
- outerOpacityAnim = new RenderNodeAnimator(
- mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, inflectionOpacity);
- outerOpacityAnim.setDuration(outerInflection);
- outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
-
- // Chain the outer opacity exit animation.
- final int outerDuration = opacityDuration - outerInflection;
- if (outerDuration > 0) {
- final RenderNodeAnimator outerFadeOutAnim = new RenderNodeAnimator(
- mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0);
- outerFadeOutAnim.setDuration(outerDuration);
- outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR);
- outerFadeOutAnim.setStartDelay(outerInflection);
- outerFadeOutAnim.setStartValue(inflectionOpacity);
- outerFadeOutAnim.addListener(mAnimationListener);
-
- mPendingAnimations.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);
- }
+ opacityAnim.addListener(mAnimationListener);
mPendingAnimations.add(radiusAnim);
mPendingAnimations.add(opacityAnim);
- mPendingAnimations.add(outerOpacityAnim);
mPendingAnimations.add(xAnim);
mPendingAnimations.add(yAnim);
invalidateSelf();
}
- private void exitSoftware(int radiusDuration, int opacityDuration, int outerInflection,
- int inflectionOpacity) {
+ private Paint getTempPaint() {
+ if (mTempPaint == null) {
+ mTempPaint = new Paint();
+ }
+ return mTempPaint;
+ }
+
+ private void exitSoftware(int radiusDuration, int opacityDuration) {
final ObjectAnimator radiusAnim = ObjectAnimator.ofFloat(this, "radiusGravity", 1);
radiusAnim.setAutoCancel(true);
radiusAnim.setDuration(radiusDuration);
opacityAnim.setAutoCancel(true);
opacityAnim.setDuration(opacityDuration);
opacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
-
- final ObjectAnimator outerOpacityAnim;
- if (outerInflection > 0) {
- // Outer opacity continues to increase for a bit.
- outerOpacityAnim = ObjectAnimator.ofFloat(this,
- "outerOpacity", inflectionOpacity / 255.0f);
- outerOpacityAnim.setAutoCancel(true);
- outerOpacityAnim.setDuration(outerInflection);
- outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
-
- // Chain the outer opacity exit animation.
- final int outerDuration = opacityDuration - outerInflection;
- if (outerDuration > 0) {
- outerOpacityAnim.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- final ObjectAnimator outerFadeOutAnim = ObjectAnimator.ofFloat(Ripple.this,
- "outerOpacity", 0);
- outerFadeOutAnim.setAutoCancel(true);
- outerFadeOutAnim.setDuration(outerDuration);
- outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR);
- outerFadeOutAnim.addListener(mAnimationListener);
-
- mAnimOuterOpacity = outerFadeOutAnim;
-
- outerFadeOutAnim.start();
- }
-
- @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);
- }
+ opacityAnim.addListener(mAnimationListener);
mAnimRadius = radiusAnim;
mAnimOpacity = opacityAnim;
- mAnimOuterOpacity = outerOpacityAnim;
- mAnimX = opacityAnim;
- mAnimY = opacityAnim;
+ mAnimX = xAnim;
+ mAnimY = yAnim;
radiusAnim.start();
opacityAnim.start();
- outerOpacityAnim.start();
xAnim.start();
yAnim.start();
}
mAnimOpacity.cancel();
}
- if (mAnimOuterOpacity != null) {
- mAnimOuterOpacity.cancel();
- }
-
if (mAnimX != null) {
mAnimX.cancel();
}
*/
private void cancelHardwareAnimations() {
final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations;
- final int N = runningAnimations == null ? 0 : runningAnimations.size();
+ final int N = runningAnimations.size();
for (int i = 0; i < N; i++) {
runningAnimations.get(i).cancel();
}
--- /dev/null
+/*
+ * 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.Paint.Style;
+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 RippleBackground {
+ 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_OPACITY_DECAY_VELOCITY = 3.0f / GLOBAL_SPEED;
+ private static final float WAVE_OUTER_OPACITY_VELOCITY_MAX = 4.5f * GLOBAL_SPEED;
+ private static final float WAVE_OUTER_OPACITY_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 long RIPPLE_ENTER_DELAY = 80;
+
+ // Hardware animators.
+ private final ArrayList<RenderNodeAnimator> mRunningAnimations =
+ new ArrayList<RenderNodeAnimator>();
+ private final ArrayList<RenderNodeAnimator> mPendingAnimations =
+ new ArrayList<RenderNodeAnimator>();
+
+ private final RippleDrawable mOwner;
+
+ /** Bounds used for computing max radius. */
+ private final Rect mBounds;
+
+ /** Full-opacity 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 float mStartingX;
+ private float mStartingY;
+ private float mClampedStartingX;
+ private float mClampedStartingY;
+
+ // Hardware rendering properties.
+ private CanvasProperty<Paint> mPropOuterPaint;
+ private CanvasProperty<Float> mPropOuterRadius;
+ private CanvasProperty<Float> mPropOuterX;
+ private CanvasProperty<Float> mPropOuterY;
+
+ // Software animators.
+ private ObjectAnimator mAnimOuterOpacity;
+ private ObjectAnimator mAnimX;
+ private ObjectAnimator mAnimY;
+
+ // Temporary paint used for creating canvas properties.
+ private Paint mTempPaint;
+
+ // Software rendering properties.
+ private float mOuterOpacity = 0;
+ private float mOuterX;
+ private float mOuterY;
+
+ // Values used to tween between the start and end positions.
+ 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;
+
+ /**
+ * Creates a new ripple.
+ */
+ public RippleBackground(RippleDrawable owner, Rect bounds, float startingX, float startingY) {
+ mOwner = owner;
+ mBounds = bounds;
+
+ mStartingX = startingX;
+ mStartingY = startingY;
+ }
+
+ public void setup(int maxRadius, int color, float density) {
+ mColor = color | 0xFF000000;
+
+ if (maxRadius != RippleDrawable.RADIUS_AUTO) {
+ 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();
+ }
+
+ 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();
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void setOuterOpacity(float a) {
+ mOuterOpacity = a;
+ invalidateSelf();
+ }
+
+ @SuppressWarnings("unused")
+ public float getOuterOpacity() {
+ return mOuterOpacity;
+ }
+
+ @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();
+ }
+ mCanUseHardware = canUseHardware;
+
+ final boolean hasContent;
+ if (canUseHardware && mHardwareAnimating) {
+ hasContent = drawHardware((HardwareCanvas) c);
+ } else {
+ hasContent = drawSoftware(c, p);
+ }
+
+ return hasContent;
+ }
+
+ private boolean drawHardware(HardwareCanvas c) {
+ // If we have any pending hardware animations, cancel any running
+ // animations and start those now.
+ final ArrayList<RenderNodeAnimator> pendingAnimations = mPendingAnimations;
+ final int N = pendingAnimations.size();
+ if (N > 0) {
+ cancelHardwareAnimations();
+
+ for (int i = 0; i < N; i++) {
+ pendingAnimations.get(i).setTarget(c);
+ pendingAnimations.get(i).start();
+ }
+
+ mRunningAnimations.addAll(pendingAnimations);
+ pendingAnimations.clear();
+ }
+
+ c.drawCircle(mPropOuterX, mPropOuterY, mPropOuterRadius, mPropOuterPaint);
+
+ return true;
+ }
+
+ private boolean drawSoftware(Canvas c, Paint p) {
+ boolean hasContent = false;
+
+ // Cache the paint alpha so we can restore it later.
+ final int paintAlpha = p.getAlpha();
+
+ final int outerAlpha = (int) (paintAlpha * mOuterOpacity + 0.5f);
+ if (outerAlpha > 0 && mOuterRadius > 0) {
+ p.setAlpha(outerAlpha);
+ p.setStyle(Style.FILL);
+ c.drawCircle(mOuterX, mOuterY, mOuterRadius, p);
+ hasContent = true;
+ }
+
+ p.setAlpha(paintAlpha);
+
+ 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;
+ 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() {
+ final int radiusDuration = (int)
+ (1000 * Math.sqrt(mOuterRadius / WAVE_TOUCH_DOWN_ACCELERATION * mDensity) + 0.5);
+ final int outerDuration = (int) (1000 * 1.0f / WAVE_OUTER_OPACITY_VELOCITY_MIN);
+
+ 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);
+
+ final ObjectAnimator outer = ObjectAnimator.ofFloat(this, "outerOpacity", 0, 1);
+ outer.setAutoCancel(true);
+ outer.setDuration(outerDuration);
+ outer.setInterpolator(LINEAR_INTERPOLATOR);
+
+ mAnimOuterOpacity = outer;
+ 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.
+ outer.start();
+ cX.start();
+ cY.start();
+ }
+
+ /**
+ * Starts the exit animation.
+ */
+ public void exit() {
+ cancelSoftwareAnimations();
+
+ // 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_VELOCITY_MIN,
+ WAVE_OUTER_OPACITY_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 outerInflection = Math.max(0, (int) (1000 * (1 - mOuterOpacity)
+ / (WAVE_OPACITY_DECAY_VELOCITY + outerOpacityVelocity) + 0.5f));
+ final int inflectionOpacity = (int) (255 * (mOuterOpacity + outerInflection
+ * outerOpacityVelocity * outerSizeInfluence / 1000) + 0.5f);
+
+ if (mCanUseHardware) {
+ exitHardware(opacityDuration, outerInflection, inflectionOpacity);
+ } else {
+ exitSoftware(opacityDuration, outerInflection, inflectionOpacity);
+ }
+ }
+
+ private void exitHardware(int opacityDuration, int outerInflection, int inflectionOpacity) {
+ mPendingAnimations.clear();
+
+ // TODO: Adjust background by starting position.
+ final float startX = MathUtils.lerp(
+ mClampedStartingX - mBounds.exactCenterX(), mOuterX, mTweenX);
+ final float startY = MathUtils.lerp(
+ mClampedStartingY - mBounds.exactCenterY(), mOuterY, mTweenY);
+
+ final Paint outerPaint = getTempPaint();
+ outerPaint.setAntiAlias(true);
+ outerPaint.setColor(mColor);
+ outerPaint.setAlpha((int) (255 * mOuterOpacity + 0.5f));
+ outerPaint.setStyle(Style.FILL);
+ mPropOuterPaint = CanvasProperty.createPaint(outerPaint);
+ mPropOuterRadius = CanvasProperty.createFloat(mOuterRadius);
+ mPropOuterX = CanvasProperty.createFloat(mOuterX);
+ mPropOuterY = CanvasProperty.createFloat(mOuterY);
+
+ final RenderNodeAnimator outerOpacityAnim;
+ if (outerInflection > 0) {
+ // Outer opacity continues to increase for a bit.
+ outerOpacityAnim = new RenderNodeAnimator(
+ mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, inflectionOpacity);
+ outerOpacityAnim.setDuration(outerInflection);
+ outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
+
+ // Chain the outer opacity exit animation.
+ final int outerDuration = opacityDuration - outerInflection;
+ if (outerDuration > 0) {
+ final RenderNodeAnimator outerFadeOutAnim = new RenderNodeAnimator(
+ mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0);
+ outerFadeOutAnim.setDuration(outerDuration);
+ outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR);
+ outerFadeOutAnim.setStartDelay(outerInflection);
+ outerFadeOutAnim.setStartValue(inflectionOpacity);
+ outerFadeOutAnim.addListener(mAnimationListener);
+
+ mPendingAnimations.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);
+ }
+
+ mPendingAnimations.add(outerOpacityAnim);
+
+ mHardwareAnimating = true;
+
+ invalidateSelf();
+ }
+
+ private Paint getTempPaint() {
+ if (mTempPaint == null) {
+ mTempPaint = new Paint();
+ }
+ return mTempPaint;
+ }
+
+ private void exitSoftware(int opacityDuration, int outerInflection, int inflectionOpacity) {
+ final ObjectAnimator xAnim = ObjectAnimator.ofFloat(this, "xGravity", 1);
+ xAnim.setAutoCancel(true);
+ xAnim.setDuration(opacityDuration);
+ xAnim.setInterpolator(DECEL_INTERPOLATOR);
+
+ final ObjectAnimator yAnim = ObjectAnimator.ofFloat(this, "yGravity", 1);
+ yAnim.setAutoCancel(true);
+ yAnim.setDuration(opacityDuration);
+ yAnim.setInterpolator(DECEL_INTERPOLATOR);
+
+ final ObjectAnimator outerOpacityAnim;
+ if (outerInflection > 0) {
+ // Outer opacity continues to increase for a bit.
+ outerOpacityAnim = ObjectAnimator.ofFloat(this,
+ "outerOpacity", inflectionOpacity / 255.0f);
+ outerOpacityAnim.setAutoCancel(true);
+ outerOpacityAnim.setDuration(outerInflection);
+ outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
+
+ // Chain the outer opacity exit animation.
+ final int outerDuration = opacityDuration - outerInflection;
+ 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);
+
+ mAnimOuterOpacity = outerFadeOutAnim;
+
+ outerFadeOutAnim.start();
+ }
+
+ @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);
+ }
+
+ mAnimOuterOpacity = outerOpacityAnim;
+ mAnimX = xAnim;
+ mAnimY = yAnim;
+
+ outerOpacityAnim.start();
+ xAnim.start();
+ yAnim.start();
+ }
+
+ /**
+ * Cancel all animations.
+ */
+ public void cancel() {
+ cancelSoftwareAnimations();
+ cancelHardwareAnimations();
+ }
+
+ private void cancelSoftwareAnimations() {
+ if (mAnimOuterOpacity != null) {
+ mAnimOuterOpacity.cancel();
+ }
+
+ if (mAnimX != null) {
+ mAnimX.cancel();
+ }
+
+ if (mAnimY != null) {
+ mAnimY.cancel();
+ }
+ }
+
+ /**
+ * Cancels any running hardware animations.
+ */
+ private void cancelHardwareAnimations() {
+ final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations;
+ final int N = runningAnimations.size();
+ for (int i = 0; i < N; i++) {
+ runningAnimations.get(i).cancel();
+ }
+
+ runningAnimations.clear();
+ }
+
+ private void removeSelf() {
+ // The owner will invalidate itself.
+ mOwner.removeBackground(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);
+ }
+ }
+}
/** The masking layer, e.g. the layer with id R.id.mask. */
private Drawable mMask;
- /** The current hotspot. May be actively animating or pending entry. */
- private Ripple mHotspot;
+ /** The current background. May be actively animating or pending entry. */
+ private RippleBackground mBackground;
+
+ /** Whether we expect to draw a background when visible. */
+ private boolean mBackgroundActive;
+
+ /** The current ripple. May be actively animating or pending entry. */
+ private Ripple mRipple;
+
+ /** Whether we expect to draw a ripple when visible. */
+ private boolean mRippleActive;
+
+ // Hotspot coordinates that are awaiting activation.
+ private float mPendingX;
+ private float mPendingY;
+ private boolean mHasPending;
/**
* Lazily-created array of actively animating ripples. Inactive ripples are
/** Whether bounds are being overridden. */
private boolean mOverrideBounds;
- /** Whether the hotspot is currently active (e.g. focused or pressed). */
- private boolean mActive;
-
/**
* Constructor used for drawable inflation.
*/
protected boolean onStateChange(int[] stateSet) {
super.onStateChange(stateSet);
- // TODO: This would make more sense in a StateListDrawable.
- boolean active = false;
boolean enabled = false;
+ boolean pressed = false;
+ boolean focused = false;
+
final int N = stateSet.length;
for (int i = 0; i < N; i++) {
if (stateSet[i] == R.attr.state_enabled) {
enabled = true;
}
if (stateSet[i] == R.attr.state_focused
- || stateSet[i] == R.attr.state_pressed) {
- active = true;
+ || stateSet[i] == R.attr.state_selected) {
+ focused = true;
+ }
+ if (stateSet[i] == R.attr.state_pressed) {
+ pressed = true;
}
}
- setActive(active && enabled);
+
+ setRippleActive(enabled && pressed);
+ setBackgroundActive(focused || (enabled && pressed));
// Update the paint color. Only applicable when animated in software.
if (mRipplePaint != null && mState.mColor != null) {
return false;
}
- private void setActive(boolean active) {
- if (mActive != active) {
- mActive = active;
+ private void setRippleActive(boolean active) {
+ if (mRippleActive != active) {
+ mRippleActive = active;
+ if (active) {
+ activateRipple();
+ } else {
+ removeRipple();
+ }
+ }
+ }
+ private void setBackgroundActive(boolean active) {
+ if (mBackgroundActive != active) {
+ mBackgroundActive = active;
if (active) {
- activateHotspot();
+ activateBackground();
} else {
- removeHotspot();
+ removeBackground();
}
}
}
@Override
public boolean setVisible(boolean visible, boolean restart) {
+ final boolean changed = super.setVisible(visible, restart);
+
if (!visible) {
clearHotspots();
+ } else if (changed) {
+ // If we just became visible, ensure the background and ripple
+ // visibilities are consistent with their internal states.
+ if (mRippleActive) {
+ activateRipple();
+ }
+
+ if (mBackgroundActive) {
+ activateBackground();
+ }
}
- return super.setVisible(visible, restart);
+ return changed;
}
/**
@Override
public void setHotspot(float x, float y) {
- if (mHotspot == null) {
- mHotspot = new Ripple(this, mHotspotBounds, x, y);
+ if (mRipple == null || mBackground == null) {
+ mPendingX = x;
+ mPendingY = y;
+ mHasPending = true;
+ }
+
+ if (mRipple != null) {
+ mRipple.move(x, y);
+ }
- if (mActive) {
- activateHotspot();
+ if (mBackground != null) {
+ mBackground.move(x, y);
+ }
+ }
+
+ /**
+ * Creates an active hotspot at the specified location.
+ */
+ private void activateBackground() {
+ if (mBackground == null) {
+ final float x;
+ final float y;
+ if (mHasPending) {
+ mHasPending = false;
+ x = mPendingX;
+ y = mPendingY;
+ } else {
+ x = mHotspotBounds.exactCenterX();
+ y = mHotspotBounds.exactCenterY();
}
- } else {
- mHotspot.move(x, y);
+ mBackground = new RippleBackground(this, mHotspotBounds, x, y);
+ }
+
+ final int color = mState.mColor.getColorForState(getState(), Color.TRANSPARENT);
+ mBackground.setup(mState.mMaxRadius, color, mDensity);
+ mBackground.enter();
+ }
+
+ private void removeBackground() {
+ if (mBackground != null) {
+ // Don't null out the background, we need it to draw!
+ mBackground.exit();
}
}
/**
* Creates an active hotspot at the specified location.
*/
- private void activateHotspot() {
+ private void activateRipple() {
if (mAnimatingRipplesCount >= MAX_RIPPLES) {
// This should never happen unless the user is tapping like a maniac
// or there is a bug that's preventing ripples from being removed.
return;
}
- if (mHotspot == null) {
- final float x = mHotspotBounds.exactCenterX();
- final float y = mHotspotBounds.exactCenterY();
- mHotspot = new Ripple(this, mHotspotBounds, x, y);
+ if (mRipple == null) {
+ final float x;
+ final float y;
+ if (mHasPending) {
+ mHasPending = false;
+ x = mPendingX;
+ y = mPendingY;
+ } else {
+ x = mHotspotBounds.exactCenterX();
+ y = mHotspotBounds.exactCenterY();
+ }
+ mRipple = new Ripple(this, mHotspotBounds, x, y);
}
final int color = mState.mColor.getColorForState(getState(), Color.TRANSPARENT);
- mHotspot.setup(mState.mMaxRadius, color, mDensity);
- mHotspot.enter();
+ mRipple.setup(mState.mMaxRadius, color, mDensity);
+ mRipple.enter();
if (mAnimatingRipples == null) {
mAnimatingRipples = new Ripple[MAX_RIPPLES];
}
- mAnimatingRipples[mAnimatingRipplesCount++] = mHotspot;
+ mAnimatingRipples[mAnimatingRipplesCount++] = mRipple;
}
- private void removeHotspot() {
- if (mHotspot != null) {
- mHotspot.exit();
- mHotspot = null;
+ private void removeRipple() {
+ if (mRipple != null) {
+ mRipple.exit();
+ mRipple = null;
}
}
private void clearHotspots() {
- if (mHotspot != null) {
- mHotspot.cancel();
- mHotspot = null;
+ if (mRipple != null) {
+ mRipple.cancel();
+ mRipple = null;
+ }
+
+ if (mBackground != null) {
+ mBackground.cancel();
+ mBackground = null;
}
final int count = mAnimatingRipplesCount;
for (int i = 0; i < count; i++) {
ripples[i].onHotspotBoundsChanged();
}
+
+ if (mBackground != null) {
+ mBackground.onHotspotBoundsChanged();
+ }
}
/**
// Next, try to draw the ripples (into a layer if necessary). If we need
// to mask against the underlying content, set the xfermode to SRC_ATOP.
final PorterDuffXfermode xfermode = (hasMask || !drawNonMaskContent) ? SRC_OVER : SRC_ATOP;
- final int rippleLayer = drawRippleLayer(canvas, bounds, xfermode);
+
+ // If we have a background and a non-opaque mask, draw the masking layer.
+ final int backgroundLayer = drawBackgroundLayer(canvas, bounds, xfermode);
+ if (backgroundLayer >= 0) {
+ if (drawMask) {
+ drawMaskingLayer(canvas, bounds, DST_IN);
+ }
+ canvas.restoreToCount(backgroundLayer);
+ }
// If we have ripples and a non-opaque mask, draw the masking layer.
- if (rippleLayer >= 0 && drawMask) {
- drawMaskingLayer(canvas, bounds, DST_IN);
+ final int rippleLayer = drawRippleLayer(canvas, bounds, xfermode);
+ if (rippleLayer >= 0) {
+ if (drawMask) {
+ drawMaskingLayer(canvas, bounds, DST_IN);
+ }
+ canvas.restoreToCount(rippleLayer);
}
// Composite the layers if needed.
if (contentLayer >= 0) {
canvas.restoreToCount(contentLayer);
- } else if (rippleLayer >= 0) {
- canvas.restoreToCount(rippleLayer);
}
}
final int count = mAnimatingRipplesCount;
final int index = getRippleIndex(ripple);
if (index >= 0) {
- for (int i = index + 1; i < count; i++) {
- ripples[i - 1] = ripples[i];
- }
+ System.arraycopy(ripples, index + 1, ripples, index + 1 - 1, count - (index + 1));
ripples[count - 1] = null;
mAnimatingRipplesCount--;
invalidateSelf();
}
}
+ void removeBackground(RippleBackground background) {
+ if (mBackground == background) {
+ mBackground = null;
+ invalidateSelf();
+ }
+ }
+
private int getRippleIndex(Ripple ripple) {
final Ripple[] ripples = mAnimatingRipples;
final int count = mAnimatingRipplesCount;
// We don't need a layer if we don't expect to draw any ripples, we have
// an explicit mask, or if the non-mask content is all opaque.
boolean needsLayer = false;
- if (mAnimatingRipplesCount > 0 && mMask == null) {
+ if ((mAnimatingRipplesCount > 0 || mBackground != null) && mMask == null) {
for (int i = 0; i < count; i++) {
if (array[i].mId != R.id.mask
&& array[i].mDrawable.getOpacity() != PixelFormat.OPAQUE) {
return restoreToCount;
}
- private int drawRippleLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
- final int count = mAnimatingRipplesCount;
- if (count == 0) {
- return -1;
+ private int drawBackgroundLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
+ // Separate the ripple color and alpha channel. The alpha will be
+ // applied when we merge the ripples down to the canvas.
+ final int rippleARGB;
+ if (mState.mColor != null) {
+ rippleARGB = mState.mColor.getColorForState(getState(), Color.TRANSPARENT);
+ } else {
+ rippleARGB = Color.TRANSPARENT;
+ }
+
+ if (mRipplePaint == null) {
+ mRipplePaint = new Paint();
+ mRipplePaint.setAntiAlias(true);
+ }
+
+ final int rippleAlpha = Color.alpha(rippleARGB);
+ final Paint ripplePaint = mRipplePaint;
+ ripplePaint.setColor(rippleARGB);
+ ripplePaint.setAlpha(0xFF);
+
+ boolean drewRipples = false;
+ int restoreToCount = -1;
+ int restoreTranslate = -1;
+
+ // Draw background.
+ final RippleBackground background = mBackground;
+ if (background != null) {
+ // If we're masking the ripple layer, make sure we have a layer
+ // first. This will merge SRC_OVER (directly) onto the canvas.
+ final Paint maskingPaint = getMaskingPaint(mode);
+ maskingPaint.setAlpha(rippleAlpha);
+ restoreToCount = canvas.saveLayer(bounds.left, bounds.top,
+ bounds.right, bounds.bottom, maskingPaint);
+
+ restoreTranslate = canvas.save();
+ // Translate the canvas to the current hotspot bounds.
+ canvas.translate(mHotspotBounds.exactCenterX(), mHotspotBounds.exactCenterY());
+
+ drewRipples = background.draw(canvas, ripplePaint);
}
+ // Always restore the translation.
+ if (restoreTranslate >= 0) {
+ canvas.restoreToCount(restoreTranslate);
+ }
+
+ // If we created a layer with no content, merge it immediately.
+ if (restoreToCount >= 0 && !drewRipples) {
+ canvas.restoreToCount(restoreToCount);
+ restoreToCount = -1;
+ }
+
+ return restoreToCount;
+ }
+
+ private int drawRippleLayer(Canvas canvas, Rect bounds, PorterDuffXfermode mode) {
// Separate the ripple color and alpha channel. The alpha will be
// applied when we merge the ripples down to the canvas.
final int rippleARGB;
int restoreTranslate = -1;
// Draw ripples and update the animating ripples array.
+ final int count = mAnimatingRipplesCount;
final Ripple[] ripples = mAnimatingRipples;
for (int i = 0; i < count; i++) {
final Ripple ripple = ripples[i];
drawingBounds.union(rippleBounds);
}
+ final RippleBackground background = mBackground;
+ if (background != null) {
+ background.getBounds(rippleBounds);
+ rippleBounds.offset(cX, cY);
+ drawingBounds.union(rippleBounds);
+ }
+
dirtyBounds.union(drawingBounds);
dirtyBounds.union(super.getDirtyBounds());
return dirtyBounds;