blob: cce9ba31929f10942a390eae1541834b7c4ca095 [file] [log] [blame]
/*
* Copyright (C) 2015 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.RecordingCanvas;
import android.graphics.Rect;
import android.util.FloatProperty;
import android.util.MathUtils;
import android.view.RenderNodeAnimator;
import android.view.animation.AnimationUtils;
import android.view.animation.LinearInterpolator;
import android.view.animation.PathInterpolator;
import java.util.ArrayList;
/**
* Draws a ripple foreground.
*/
class RippleForeground extends RippleComponent {
private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
// Matches R.interpolator.fast_out_slow_in but as we have no context we can't just import that
private static final TimeInterpolator DECELERATE_INTERPOLATOR =
new PathInterpolator(0.4f, 0f, 0.2f, 1f);
// Time it takes for the ripple to expand
private static final int RIPPLE_ENTER_DURATION = 225;
// Time it takes for the ripple to slide from the touch to the center point
private static final int RIPPLE_ORIGIN_DURATION = 225;
private static final int OPACITY_ENTER_DURATION = 75;
private static final int OPACITY_EXIT_DURATION = 150;
private static final int OPACITY_HOLD_DURATION = OPACITY_ENTER_DURATION + 150;
// Parent-relative values for starting position.
private float mStartingX;
private float mStartingY;
private float mClampedStartingX;
private float mClampedStartingY;
// Hardware rendering properties.
private CanvasProperty<Paint> mPropPaint;
private CanvasProperty<Float> mPropRadius;
private CanvasProperty<Float> mPropX;
private CanvasProperty<Float> mPropY;
// Target values for tween animations.
private float mTargetX = 0;
private float mTargetY = 0;
// Software rendering properties.
private float mOpacity = 0;
// Values used to tween between the start and end positions.
private float mTweenRadius = 0;
private float mTweenX = 0;
private float mTweenY = 0;
/** Whether this ripple has finished its exit animation. */
private boolean mHasFinishedExit;
/** Whether we can use hardware acceleration for the exit animation. */
private boolean mUsingProperties;
private long mEnterStartedAtMillis;
private ArrayList<RenderNodeAnimator> mPendingHwAnimators = new ArrayList<>();
private ArrayList<RenderNodeAnimator> mRunningHwAnimators = new ArrayList<>();
private ArrayList<Animator> mRunningSwAnimators = new ArrayList<>();
/**
* If set, force all ripple animations to not run on RenderThread, even if it would be
* available.
*/
private final boolean mForceSoftware;
/**
* If we have a bound, don't start from 0. Start from 60% of the max out of width and height.
*/
private float mStartRadius = 0;
public RippleForeground(RippleDrawable owner, Rect bounds, float startingX, float startingY,
boolean forceSoftware) {
super(owner, bounds);
mForceSoftware = forceSoftware;
mStartingX = startingX;
mStartingY = startingY;
// Take 60% of the maximum of the width and height, then divided half to get the radius.
mStartRadius = Math.max(bounds.width(), bounds.height()) * 0.3f;
clampStartingPosition();
}
@Override
protected void onTargetRadiusChanged(float targetRadius) {
clampStartingPosition();
switchToUiThreadAnimation();
}
private void drawSoftware(Canvas c, Paint p) {
final int origAlpha = p.getAlpha();
final int alpha = (int) (origAlpha * mOpacity + 0.5f);
final float radius = getCurrentRadius();
if (alpha > 0 && radius > 0) {
final float x = getCurrentX();
final float y = getCurrentY();
p.setAlpha(alpha);
c.drawCircle(x, y, radius, p);
p.setAlpha(origAlpha);
}
}
private void startPending(RecordingCanvas c) {
if (!mPendingHwAnimators.isEmpty()) {
for (int i = 0; i < mPendingHwAnimators.size(); i++) {
RenderNodeAnimator animator = mPendingHwAnimators.get(i);
animator.setTarget(c);
animator.start();
mRunningHwAnimators.add(animator);
}
mPendingHwAnimators.clear();
}
}
private void pruneHwFinished() {
if (!mRunningHwAnimators.isEmpty()) {
for (int i = mRunningHwAnimators.size() - 1; i >= 0; i--) {
if (!mRunningHwAnimators.get(i).isRunning()) {
mRunningHwAnimators.remove(i);
}
}
}
}
private void pruneSwFinished() {
if (!mRunningSwAnimators.isEmpty()) {
for (int i = mRunningSwAnimators.size() - 1; i >= 0; i--) {
if (!mRunningSwAnimators.get(i).isRunning()) {
mRunningSwAnimators.remove(i);
}
}
}
}
private void drawHardware(RecordingCanvas c, Paint p) {
startPending(c);
pruneHwFinished();
if (mPropPaint != null) {
mUsingProperties = true;
c.drawCircle(mPropX, mPropY, mPropRadius, mPropPaint);
} else {
mUsingProperties = false;
drawSoftware(c, p);
}
}
/**
* Returns the maximum bounds of the ripple relative to the ripple center.
*/
public void getBounds(Rect bounds) {
final int outerX = (int) mTargetX;
final int outerY = (int) mTargetY;
final int r = (int) mTargetRadius + 1;
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();
}
/**
* @return {@code true} if this ripple has finished its exit animation
*/
public boolean hasFinishedExit() {
return mHasFinishedExit;
}
private long computeFadeOutDelay() {
long timeSinceEnter = AnimationUtils.currentAnimationTimeMillis() - mEnterStartedAtMillis;
if (timeSinceEnter > 0 && timeSinceEnter < OPACITY_HOLD_DURATION) {
return OPACITY_HOLD_DURATION - timeSinceEnter;
}
return 0;
}
private void startSoftwareEnter() {
for (int i = 0; i < mRunningSwAnimators.size(); i++) {
mRunningSwAnimators.get(i).cancel();
}
mRunningSwAnimators.clear();
final ObjectAnimator tweenRadius = ObjectAnimator.ofFloat(this, TWEEN_RADIUS, 1);
tweenRadius.setDuration(RIPPLE_ENTER_DURATION);
tweenRadius.setInterpolator(DECELERATE_INTERPOLATOR);
tweenRadius.start();
mRunningSwAnimators.add(tweenRadius);
final ObjectAnimator tweenOrigin = ObjectAnimator.ofFloat(this, TWEEN_ORIGIN, 1);
tweenOrigin.setDuration(RIPPLE_ORIGIN_DURATION);
tweenOrigin.setInterpolator(DECELERATE_INTERPOLATOR);
tweenOrigin.start();
mRunningSwAnimators.add(tweenOrigin);
final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 1);
opacity.setDuration(OPACITY_ENTER_DURATION);
opacity.setInterpolator(LINEAR_INTERPOLATOR);
opacity.start();
mRunningSwAnimators.add(opacity);
}
private void startSoftwareExit() {
final ObjectAnimator opacity = ObjectAnimator.ofFloat(this, OPACITY, 0);
opacity.setDuration(OPACITY_EXIT_DURATION);
opacity.setInterpolator(LINEAR_INTERPOLATOR);
opacity.addListener(mAnimationListener);
opacity.setStartDelay(computeFadeOutDelay());
opacity.start();
mRunningSwAnimators.add(opacity);
}
private void startHardwareEnter() {
if (mForceSoftware) { return; }
mPropX = CanvasProperty.createFloat(getCurrentX());
mPropY = CanvasProperty.createFloat(getCurrentY());
mPropRadius = CanvasProperty.createFloat(getCurrentRadius());
final Paint paint = mOwner.getRipplePaint();
mPropPaint = CanvasProperty.createPaint(paint);
final RenderNodeAnimator radius = new RenderNodeAnimator(mPropRadius, mTargetRadius);
radius.setDuration(RIPPLE_ORIGIN_DURATION);
radius.setInterpolator(DECELERATE_INTERPOLATOR);
mPendingHwAnimators.add(radius);
final RenderNodeAnimator x = new RenderNodeAnimator(mPropX, mTargetX);
x.setDuration(RIPPLE_ORIGIN_DURATION);
x.setInterpolator(DECELERATE_INTERPOLATOR);
mPendingHwAnimators.add(x);
final RenderNodeAnimator y = new RenderNodeAnimator(mPropY, mTargetY);
y.setDuration(RIPPLE_ORIGIN_DURATION);
y.setInterpolator(DECELERATE_INTERPOLATOR);
mPendingHwAnimators.add(y);
final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint,
RenderNodeAnimator.PAINT_ALPHA, paint.getAlpha());
opacity.setDuration(OPACITY_ENTER_DURATION);
opacity.setInterpolator(LINEAR_INTERPOLATOR);
opacity.setStartValue(0);
mPendingHwAnimators.add(opacity);
invalidateSelf();
}
private void startHardwareExit() {
// Only run a hardware exit if we had a hardware enter to continue from
if (mForceSoftware || mPropPaint == null) return;
final RenderNodeAnimator opacity = new RenderNodeAnimator(mPropPaint,
RenderNodeAnimator.PAINT_ALPHA, 0);
opacity.setDuration(OPACITY_EXIT_DURATION);
opacity.setInterpolator(LINEAR_INTERPOLATOR);
opacity.addListener(mAnimationListener);
opacity.setStartDelay(computeFadeOutDelay());
opacity.setStartValue(mOwner.getRipplePaint().getAlpha());
mPendingHwAnimators.add(opacity);
invalidateSelf();
}
/**
* Starts a ripple enter animation.
*/
public final void enter() {
mEnterStartedAtMillis = AnimationUtils.currentAnimationTimeMillis();
startSoftwareEnter();
startHardwareEnter();
}
/**
* Starts a ripple exit animation.
*/
public final void exit() {
startSoftwareExit();
startHardwareExit();
}
private float getCurrentX() {
return MathUtils.lerp(mClampedStartingX - mBounds.exactCenterX(), mTargetX, mTweenX);
}
private float getCurrentY() {
return MathUtils.lerp(mClampedStartingY - mBounds.exactCenterY(), mTargetY, mTweenY);
}
private float getCurrentRadius() {
return MathUtils.lerp(mStartRadius, mTargetRadius, mTweenRadius);
}
/**
* Draws the ripple to the canvas, inheriting the paint's color and alpha
* properties.
*
* @param c the canvas to which the ripple should be drawn
* @param p the paint used to draw the ripple
*/
public void draw(Canvas c, Paint p) {
final boolean hasDisplayListCanvas = !mForceSoftware && c instanceof RecordingCanvas;
pruneSwFinished();
if (hasDisplayListCanvas) {
final RecordingCanvas hw = (RecordingCanvas) c;
drawHardware(hw, p);
} else {
drawSoftware(c, p);
}
}
/**
* Clamps the starting position to fit within the ripple bounds.
*/
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 = mTargetRadius - mStartRadius;
if (dX * dX + dY * dY > r * r) {
// Point is outside the circle, clamp to the perimeter.
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;
}
}
/**
* Ends all animations, jumping values to the end state.
*/
public void end() {
for (int i = 0; i < mRunningSwAnimators.size(); i++) {
mRunningSwAnimators.get(i).end();
}
mRunningSwAnimators.clear();
for (int i = 0; i < mRunningHwAnimators.size(); i++) {
mRunningHwAnimators.get(i).end();
}
mRunningHwAnimators.clear();
}
private void onAnimationPropertyChanged() {
if (!mUsingProperties) {
invalidateSelf();
}
}
private void clearHwProps() {
mPropPaint = null;
mPropRadius = null;
mPropX = null;
mPropY = null;
mUsingProperties = false;
}
private final AnimatorListenerAdapter mAnimationListener = new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animator) {
mHasFinishedExit = true;
pruneHwFinished();
pruneSwFinished();
if (mRunningHwAnimators.isEmpty()) {
clearHwProps();
}
}
};
private void switchToUiThreadAnimation() {
for (int i = 0; i < mRunningHwAnimators.size(); i++) {
Animator animator = mRunningHwAnimators.get(i);
animator.removeListener(mAnimationListener);
animator.end();
}
mRunningHwAnimators.clear();
clearHwProps();
invalidateSelf();
}
/**
* Property for animating radius between its initial and target values.
*/
private static final FloatProperty<RippleForeground> TWEEN_RADIUS =
new FloatProperty<RippleForeground>("tweenRadius") {
@Override
public void setValue(RippleForeground object, float value) {
object.mTweenRadius = value;
object.onAnimationPropertyChanged();
}
@Override
public Float get(RippleForeground object) {
return object.mTweenRadius;
}
};
/**
* Property for animating origin between its initial and target values.
*/
private static final FloatProperty<RippleForeground> TWEEN_ORIGIN =
new FloatProperty<RippleForeground>("tweenOrigin") {
@Override
public void setValue(RippleForeground object, float value) {
object.mTweenX = value;
object.mTweenY = value;
object.onAnimationPropertyChanged();
}
@Override
public Float get(RippleForeground object) {
return object.mTweenX;
}
};
/**
* Property for animating opacity between 0 and its target value.
*/
private static final FloatProperty<RippleForeground> OPACITY =
new FloatProperty<RippleForeground>("opacity") {
@Override
public void setValue(RippleForeground object, float value) {
object.mOpacity = value;
object.onAnimationPropertyChanged();
}
@Override
public Float get(RippleForeground object) {
return object.mOpacity;
}
};
}