Removes an extra saveLayer call by rendering ripples as overlapping. We are now down to zero saveLayers when drawing unmasked ripples or ripples masked against opaque content/masks, one saveLayer for ripples masked against content and two saveLayers when masked against an explicit mask layer. BUG: 18226391 Change-Id: I0fc09d21fbc462fbcfe4c26fc7b18737f584043e
443 lines
16 KiB
Java
443 lines
16 KiB
Java
/*
|
|
* 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.Color;
|
|
import android.graphics.Paint;
|
|
import android.graphics.Paint.Style;
|
|
import android.graphics.Rect;
|
|
import android.graphics.Xfermode;
|
|
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 float GLOBAL_SPEED = 1.0f;
|
|
private static final float WAVE_OPACITY_DECAY_VELOCITY = 3.0f / GLOBAL_SPEED;
|
|
private static final float WAVE_OUTER_OPACITY_EXIT_VELOCITY_MAX = 4.5f * GLOBAL_SPEED;
|
|
private static final float WAVE_OUTER_OPACITY_EXIT_VELOCITY_MIN = 1.5f * GLOBAL_SPEED;
|
|
private static final float WAVE_OUTER_SIZE_INFLUENCE_MAX = 200f;
|
|
private static final float WAVE_OUTER_SIZE_INFLUENCE_MIN = 40f;
|
|
|
|
private static final int ENTER_DURATION = 667;
|
|
private static final int ENTER_DURATION_FAST = 100;
|
|
|
|
// Hardware animators.
|
|
private final ArrayList<RenderNodeAnimator> mRunningAnimations =
|
|
new ArrayList<RenderNodeAnimator>();
|
|
private final ArrayList<RenderNodeAnimator> mPendingAnimations =
|
|
new ArrayList<RenderNodeAnimator>();
|
|
|
|
private final RippleDrawable mOwner;
|
|
|
|
/** Bounds used for computing max radius. */
|
|
private final Rect mBounds;
|
|
|
|
/** ARGB color for drawing this ripple. */
|
|
private int mColor;
|
|
|
|
private Xfermode mXfermode;
|
|
|
|
/** Maximum ripple radius. */
|
|
private float mOuterRadius;
|
|
|
|
/** Screen density used to adjust pixel-based velocities. */
|
|
private float mDensity;
|
|
|
|
// Hardware rendering properties.
|
|
private CanvasProperty<Paint> mPropOuterPaint;
|
|
private CanvasProperty<Float> mPropOuterRadius;
|
|
private CanvasProperty<Float> mPropOuterX;
|
|
private CanvasProperty<Float> mPropOuterY;
|
|
|
|
// Software animators.
|
|
private ObjectAnimator mAnimOuterOpacity;
|
|
|
|
// Temporary paint used for creating canvas properties.
|
|
private Paint mTempPaint;
|
|
|
|
// Software rendering properties.
|
|
private float mOuterOpacity = 0;
|
|
private float mOuterX;
|
|
private float mOuterY;
|
|
|
|
/** Whether we should be drawing hardware animations. */
|
|
private boolean mHardwareAnimating;
|
|
|
|
/** Whether we can use hardware acceleration for the exit animation. */
|
|
private boolean mCanUseHardware;
|
|
|
|
/** Whether we have an explicit maximum radius. */
|
|
private boolean mHasMaxRadius;
|
|
|
|
/**
|
|
* Creates a new ripple.
|
|
*/
|
|
public RippleBackground(RippleDrawable owner, Rect bounds) {
|
|
mOwner = owner;
|
|
mBounds = bounds;
|
|
}
|
|
|
|
public void setup(int maxRadius, float density) {
|
|
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;
|
|
}
|
|
|
|
public void onHotspotBoundsChanged() {
|
|
if (!mHasMaxRadius) {
|
|
final float halfWidth = mBounds.width() / 2.0f;
|
|
final float halfHeight = mBounds.height() / 2.0f;
|
|
mOuterRadius = (float) Math.sqrt(halfWidth * halfWidth + halfHeight * halfHeight);
|
|
}
|
|
}
|
|
|
|
@SuppressWarnings("unused")
|
|
public void setOuterOpacity(float a) {
|
|
mOuterOpacity = a;
|
|
invalidateSelf();
|
|
}
|
|
|
|
@SuppressWarnings("unused")
|
|
public float getOuterOpacity() {
|
|
return mOuterOpacity;
|
|
}
|
|
|
|
/**
|
|
* Draws the ripple centered at (0,0) using the specified paint.
|
|
*/
|
|
public boolean draw(Canvas c, Paint p) {
|
|
// Store the color and xfermode, we might need them later.
|
|
mColor = p.getColor();
|
|
mXfermode = p.getXfermode();
|
|
|
|
final boolean canUseHardware = c.isHardwareAccelerated();
|
|
if (mCanUseHardware != canUseHardware && mCanUseHardware) {
|
|
// We've switched from hardware to non-hardware mode. Panic.
|
|
cancelHardwareAnimations(true);
|
|
}
|
|
mCanUseHardware = canUseHardware;
|
|
|
|
final boolean hasContent;
|
|
if (canUseHardware && mHardwareAnimating) {
|
|
hasContent = drawHardware((HardwareCanvas) c);
|
|
} else {
|
|
hasContent = drawSoftware(c, p);
|
|
}
|
|
|
|
return hasContent;
|
|
}
|
|
|
|
public boolean shouldDraw() {
|
|
return (mCanUseHardware && mHardwareAnimating) || (mOuterOpacity > 0 && mOuterRadius > 0);
|
|
}
|
|
|
|
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(false);
|
|
|
|
// We canceled old animations, but we're about to run new ones.
|
|
mHardwareAnimating = true;
|
|
|
|
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;
|
|
|
|
final int paintAlpha = p.getAlpha();
|
|
final int alpha = (int) (paintAlpha * mOuterOpacity + 0.5f);
|
|
final float radius = mOuterRadius;
|
|
if (alpha > 0 && radius > 0) {
|
|
p.setAlpha(alpha);
|
|
c.drawCircle(mOuterX, mOuterY, radius, p);
|
|
p.setAlpha(paintAlpha);
|
|
hasContent = true;
|
|
}
|
|
|
|
return hasContent;
|
|
}
|
|
|
|
/**
|
|
* Returns the maximum bounds of the ripple relative to the ripple center.
|
|
*/
|
|
public void getBounds(Rect bounds) {
|
|
final int outerX = (int) mOuterX;
|
|
final int outerY = (int) mOuterY;
|
|
final int r = (int) mOuterRadius + 1;
|
|
bounds.set(outerX - r, outerY - r, outerX + r, outerY + r);
|
|
}
|
|
|
|
/**
|
|
* Starts the enter animation.
|
|
*/
|
|
public void enter(boolean fast) {
|
|
cancel();
|
|
|
|
final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, "outerOpacity", 0, 1);
|
|
opacity.setAutoCancel(true);
|
|
opacity.setDuration(fast ? ENTER_DURATION_FAST : ENTER_DURATION);
|
|
opacity.setInterpolator(LINEAR_INTERPOLATOR);
|
|
|
|
mAnimOuterOpacity = opacity;
|
|
|
|
// Enter animations always run on the UI thread, since it's unlikely
|
|
// that anything interesting is happening until the user lifts their
|
|
// finger.
|
|
opacity.start();
|
|
}
|
|
|
|
/**
|
|
* Starts the exit animation.
|
|
*/
|
|
public void exit() {
|
|
cancel();
|
|
|
|
// Scale the outer max opacity and opacity velocity based
|
|
// on the size of the outer radius.
|
|
final int opacityDuration = (int) (1000 / WAVE_OPACITY_DECAY_VELOCITY + 0.5f);
|
|
final float outerSizeInfluence = MathUtils.constrain(
|
|
(mOuterRadius - WAVE_OUTER_SIZE_INFLUENCE_MIN * mDensity)
|
|
/ (WAVE_OUTER_SIZE_INFLUENCE_MAX * mDensity), 0, 1);
|
|
final float outerOpacityVelocity = MathUtils.lerp(WAVE_OUTER_OPACITY_EXIT_VELOCITY_MIN,
|
|
WAVE_OUTER_OPACITY_EXIT_VELOCITY_MAX, outerSizeInfluence);
|
|
|
|
// Determine at what time the inner and outer opacity intersect.
|
|
// inner(t) = mOpacity - t * WAVE_OPACITY_DECAY_VELOCITY / 1000
|
|
// outer(t) = mOuterOpacity + t * WAVE_OUTER_OPACITY_VELOCITY / 1000
|
|
final int inflectionDuration = Math.max(0, (int) (1000 * (1 - mOuterOpacity)
|
|
/ (WAVE_OPACITY_DECAY_VELOCITY + outerOpacityVelocity) + 0.5f));
|
|
final int inflectionOpacity = (int) (Color.alpha(mColor) * (mOuterOpacity
|
|
+ inflectionDuration * outerOpacityVelocity * outerSizeInfluence / 1000) + 0.5f);
|
|
|
|
if (mCanUseHardware) {
|
|
exitHardware(opacityDuration, inflectionDuration, inflectionOpacity);
|
|
} else {
|
|
exitSoftware(opacityDuration, inflectionDuration, inflectionOpacity);
|
|
}
|
|
}
|
|
|
|
private void exitHardware(int opacityDuration, int inflectionDuration, int inflectionOpacity) {
|
|
mPendingAnimations.clear();
|
|
|
|
final Paint outerPaint = getTempPaint();
|
|
outerPaint.setAntiAlias(true);
|
|
outerPaint.setXfermode(mXfermode);
|
|
outerPaint.setColor(mColor);
|
|
outerPaint.setAlpha((int) (Color.alpha(mColor) * 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 (inflectionDuration > 0) {
|
|
// Outer opacity continues to increase for a bit.
|
|
outerOpacityAnim = new RenderNodeAnimator(mPropOuterPaint,
|
|
RenderNodeAnimator.PAINT_ALPHA, inflectionOpacity);
|
|
outerOpacityAnim.setDuration(inflectionDuration);
|
|
outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
|
|
|
|
// Chain the outer opacity exit animation.
|
|
final int outerDuration = opacityDuration - inflectionDuration;
|
|
if (outerDuration > 0) {
|
|
final RenderNodeAnimator outerFadeOutAnim = new RenderNodeAnimator(
|
|
mPropOuterPaint, RenderNodeAnimator.PAINT_ALPHA, 0);
|
|
outerFadeOutAnim.setDuration(outerDuration);
|
|
outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR);
|
|
outerFadeOutAnim.setStartDelay(inflectionDuration);
|
|
outerFadeOutAnim.setStartValue(inflectionOpacity);
|
|
outerFadeOutAnim.addListener(mAnimationListener);
|
|
|
|
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;
|
|
|
|
// Set up the software values to match the hardware end values.
|
|
mOuterOpacity = 0;
|
|
|
|
invalidateSelf();
|
|
}
|
|
|
|
/**
|
|
* Jump all animations to their end state. The caller is responsible for
|
|
* removing the ripple from the list of animating ripples.
|
|
*/
|
|
public void jump() {
|
|
endSoftwareAnimations();
|
|
cancelHardwareAnimations(true);
|
|
}
|
|
|
|
private void endSoftwareAnimations() {
|
|
if (mAnimOuterOpacity != null) {
|
|
mAnimOuterOpacity.end();
|
|
mAnimOuterOpacity = null;
|
|
}
|
|
}
|
|
|
|
private Paint getTempPaint() {
|
|
if (mTempPaint == null) {
|
|
mTempPaint = new Paint();
|
|
}
|
|
return mTempPaint;
|
|
}
|
|
|
|
private void exitSoftware(int opacityDuration, int inflectionDuration, int inflectionOpacity) {
|
|
final ObjectAnimator outerOpacityAnim;
|
|
if (inflectionDuration > 0) {
|
|
// Outer opacity continues to increase for a bit.
|
|
outerOpacityAnim = ObjectAnimator.ofFloat(this,
|
|
"outerOpacity", inflectionOpacity / 255.0f);
|
|
outerOpacityAnim.setAutoCancel(true);
|
|
outerOpacityAnim.setDuration(inflectionDuration);
|
|
outerOpacityAnim.setInterpolator(LINEAR_INTERPOLATOR);
|
|
|
|
// Chain the outer opacity exit animation.
|
|
final int outerDuration = opacityDuration - inflectionDuration;
|
|
if (outerDuration > 0) {
|
|
outerOpacityAnim.addListener(new AnimatorListenerAdapter() {
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
final ObjectAnimator outerFadeOutAnim = ObjectAnimator.ofFloat(
|
|
RippleBackground.this, "outerOpacity", 0);
|
|
outerFadeOutAnim.setAutoCancel(true);
|
|
outerFadeOutAnim.setDuration(outerDuration);
|
|
outerFadeOutAnim.setInterpolator(LINEAR_INTERPOLATOR);
|
|
outerFadeOutAnim.addListener(mAnimationListener);
|
|
|
|
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;
|
|
|
|
outerOpacityAnim.start();
|
|
}
|
|
|
|
/**
|
|
* Cancel all animations. The caller is responsible for removing
|
|
* the ripple from the list of animating ripples.
|
|
*/
|
|
public void cancel() {
|
|
cancelSoftwareAnimations();
|
|
cancelHardwareAnimations(true);
|
|
}
|
|
|
|
private void cancelSoftwareAnimations() {
|
|
if (mAnimOuterOpacity != null) {
|
|
mAnimOuterOpacity.cancel();
|
|
mAnimOuterOpacity = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cancels any running hardware animations.
|
|
*/
|
|
private void cancelHardwareAnimations(boolean cancelPending) {
|
|
final ArrayList<RenderNodeAnimator> runningAnimations = mRunningAnimations;
|
|
final int N = runningAnimations.size();
|
|
for (int i = 0; i < N; i++) {
|
|
runningAnimations.get(i).cancel();
|
|
}
|
|
runningAnimations.clear();
|
|
|
|
if (cancelPending && !mPendingAnimations.isEmpty()) {
|
|
mPendingAnimations.clear();
|
|
}
|
|
|
|
mHardwareAnimating = false;
|
|
}
|
|
|
|
private void invalidateSelf() {
|
|
mOwner.invalidateSelf();
|
|
}
|
|
|
|
private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() {
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
mHardwareAnimating = false;
|
|
}
|
|
};
|
|
}
|