diff --git a/graphics/java/android/graphics/drawable/Ripple.java b/graphics/java/android/graphics/drawable/Ripple.java index 2d4936582a4a7..6f21f2e004959 100644 --- a/graphics/java/android/graphics/drawable/Ripple.java +++ b/graphics/java/android/graphics/drawable/Ripple.java @@ -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 mRunningAnimations = new ArrayList<>(); - private final ArrayList mPendingAnimations = new ArrayList<>(); + private final ArrayList mRunningAnimations = + new ArrayList(); + private final ArrayList mPendingAnimations = + new ArrayList(); private final RippleDrawable mOwner; @@ -79,20 +76,17 @@ class Ripple { private CanvasProperty mPropRadius; private CanvasProperty mPropX; private CanvasProperty mPropY; - private CanvasProperty mPropOuterPaint; - private CanvasProperty mPropOuterRadius; - private CanvasProperty mPropOuterX; - private CanvasProperty 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 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 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(); } diff --git a/graphics/java/android/graphics/drawable/RippleBackground.java b/graphics/java/android/graphics/drawable/RippleBackground.java new file mode 100644 index 0000000000000..d404ccd8b1f2f --- /dev/null +++ b/graphics/java/android/graphics/drawable/RippleBackground.java @@ -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 mRunningAnimations = + new ArrayList(); + private final ArrayList mPendingAnimations = + new ArrayList(); + + 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 mPropOuterPaint; + private CanvasProperty mPropOuterRadius; + private CanvasProperty mPropOuterX; + private CanvasProperty 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 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 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); + } + } +} diff --git a/graphics/java/android/graphics/drawable/RippleDrawable.java b/graphics/java/android/graphics/drawable/RippleDrawable.java index 0e719ee429bdf..0c9c5584ac8e7 100644 --- a/graphics/java/android/graphics/drawable/RippleDrawable.java +++ b/graphics/java/android/graphics/drawable/RippleDrawable.java @@ -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;