Merge "Separate background from ripple for better focus/press UX" into lmp-dev

This commit is contained in:
Alan Viverette
2014-07-25 00:12:31 +00:00
committed by Android (Google) Code Review
3 changed files with 781 additions and 210 deletions

View File

@@ -28,7 +28,6 @@ import android.graphics.Rect;
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;
@@ -44,16 +43,14 @@ class Ripple {
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;
@@ -79,20 +76,17 @@ class Ripple {
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;
@@ -177,38 +171,35 @@ class Ripple {
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;
}
@@ -238,7 +229,7 @@ class Ripple {
// 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();
@@ -251,7 +242,6 @@ class Ripple {
pendingAnimations.clear();
}
c.drawCircle(mPropOuterX, mPropOuterY, mPropOuterRadius, mPropOuterPaint);
c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint);
return true;
@@ -262,15 +252,6 @@ class Ripple {
// 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) {
@@ -316,7 +297,6 @@ class Ripple {
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);
@@ -336,13 +316,7 @@ class Ripple {
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;
@@ -350,7 +324,6 @@ class Ripple {
// that anything interesting is happening until the user lifts their
// finger.
radius.start();
outer.start();
cX.start();
cY.start();
}
@@ -372,51 +345,23 @@ class Ripple {
+ 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));
@@ -442,41 +387,10 @@ class Ripple {
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);
@@ -485,8 +399,14 @@ class Ripple {
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);
@@ -506,58 +426,15 @@ class Ripple {
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();
}
@@ -579,10 +456,6 @@ class Ripple {
mAnimOpacity.cancel();
}
if (mAnimOuterOpacity != null) {
mAnimOuterOpacity.cancel();
}
if (mAnimX != null) {
mAnimX.cancel();
}
@@ -597,7 +470,7 @@ class Ripple {
*/
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();
}

View File

@@ -0,0 +1,535 @@
/*
* 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);
}
}
}

View File

@@ -120,8 +120,22 @@ public class RippleDrawable extends LayerDrawable {
/** 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
@@ -142,9 +156,6 @@ public class RippleDrawable extends LayerDrawable {
/** 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.
*/
@@ -205,20 +216,26 @@ public class RippleDrawable extends LayerDrawable {
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) {
@@ -235,14 +252,24 @@ public class RippleDrawable extends LayerDrawable {
return false;
}
private void setActive(boolean active) {
if (mActive != active) {
mActive = active;
private void setRippleActive(boolean active) {
if (mRippleActive != active) {
mRippleActive = active;
if (active) {
activateHotspot();
activateRipple();
} else {
removeHotspot();
removeRipple();
}
}
}
private void setBackgroundActive(boolean active) {
if (mBackgroundActive != active) {
mBackgroundActive = active;
if (active) {
activateBackground();
} else {
removeBackground();
}
}
}
@@ -261,11 +288,23 @@ public class RippleDrawable extends LayerDrawable {
@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;
}
/**
@@ -399,54 +438,101 @@ public class RippleDrawable extends LayerDrawable {
@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 (mActive) {
activateHotspot();
}
} else {
mHotspot.move(x, y);
if (mRipple != null) {
mRipple.move(x, y);
}
if (mBackground != null) {
mBackground.move(x, y);
}
}
/**
* Creates an active hotspot at the specified location.
*/
private void activateHotspot() {
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();
}
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 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;
@@ -486,6 +572,10 @@ public class RippleDrawable extends LayerDrawable {
for (int i = 0; i < count; i++) {
ripples[i].onHotspotBoundsChanged();
}
if (mBackground != null) {
mBackground.onHotspotBoundsChanged();
}
}
/**
@@ -524,18 +614,28 @@ public class RippleDrawable extends LayerDrawable {
// 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);
}
}
@@ -550,15 +650,20 @@ public class RippleDrawable extends LayerDrawable {
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;
@@ -577,7 +682,7 @@ public class RippleDrawable extends LayerDrawable {
// 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) {
@@ -601,12 +706,62 @@ public class RippleDrawable extends LayerDrawable {
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;
@@ -631,6 +786,7 @@ public class RippleDrawable extends LayerDrawable {
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];
@@ -705,6 +861,13 @@ public class RippleDrawable extends LayerDrawable {
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;