Files
frameworks_base/graphics/java/android/graphics/drawable/RippleBackground.java
Alan Viverette cc3c573334 Reduce number of saveLayer calls in RippleDrawable
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
2014-11-07 15:31:16 -08:00

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;
}
};
}