blob: 872331c826039cdeffa0d9015ff39648a1a9fb04 [file] [log] [blame]
/*
* Copyright (C) 2020 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.TimeInterpolator;
import android.animation.ValueAnimator;
import android.annotation.ColorInt;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Canvas;
import android.graphics.CanvasProperty;
import android.graphics.Paint;
import android.graphics.RecordingCanvas;
import android.graphics.animation.RenderNodeAnimator;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import android.view.animation.PathInterpolator;
import java.util.function.Consumer;
/**
* @hide
*/
public final class RippleAnimationSession {
private static final String TAG = "RippleAnimationSession";
private static final int ENTER_ANIM_DURATION = 450;
private static final int EXIT_ANIM_DURATION = 375;
private static final long NOISE_ANIMATION_DURATION = 7000;
private static final long MAX_NOISE_PHASE = NOISE_ANIMATION_DURATION / 214;
private static final TimeInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
private static final Interpolator FAST_OUT_SLOW_IN =
new PathInterpolator(0.4f, 0f, 0.2f, 1f);
private Consumer<RippleAnimationSession> mOnSessionEnd;
private final AnimationProperties<Float, Paint> mProperties;
private AnimationProperties<CanvasProperty<Float>, CanvasProperty<Paint>> mCanvasProperties;
private Runnable mOnUpdate;
private long mStartTime;
private boolean mForceSoftware;
private Animator mLoopAnimation;
private Animator mCurrentAnimation;
RippleAnimationSession(@NonNull AnimationProperties<Float, Paint> properties,
boolean forceSoftware) {
mProperties = properties;
mForceSoftware = forceSoftware;
}
boolean isForceSoftware() {
return mForceSoftware;
}
@NonNull RippleAnimationSession enter(Canvas canvas) {
mStartTime = AnimationUtils.currentAnimationTimeMillis();
if (useRTAnimations(canvas)) {
enterHardware((RecordingCanvas) canvas);
} else {
enterSoftware();
}
return this;
}
void end() {
if (mCurrentAnimation != null) {
mCurrentAnimation.end();
}
}
@NonNull RippleAnimationSession exit(Canvas canvas) {
if (useRTAnimations(canvas)) exitHardware((RecordingCanvas) canvas);
else exitSoftware();
return this;
}
private void onAnimationEnd(Animator anim) {
notifyUpdate();
}
@NonNull RippleAnimationSession setOnSessionEnd(
@Nullable Consumer<RippleAnimationSession> onSessionEnd) {
mOnSessionEnd = onSessionEnd;
return this;
}
RippleAnimationSession setOnAnimationUpdated(@Nullable Runnable run) {
mOnUpdate = run;
return this;
}
private boolean useRTAnimations(Canvas canvas) {
if (mForceSoftware) return false;
if (!canvas.isHardwareAccelerated()) return false;
RecordingCanvas hwCanvas = (RecordingCanvas) canvas;
if (hwCanvas.mNode == null || !hwCanvas.mNode.isAttached()) return false;
return true;
}
private void exitSoftware() {
ValueAnimator expand = ValueAnimator.ofFloat(.5f, 1f);
expand.setDuration(EXIT_ANIM_DURATION);
expand.setStartDelay(computeDelay());
expand.addUpdateListener(updatedAnimation -> {
notifyUpdate();
mProperties.getShader().setProgress((Float) expand.getAnimatedValue());
});
expand.addListener(new AnimatorListener(this) {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
if (mLoopAnimation != null) mLoopAnimation.cancel();
Consumer<RippleAnimationSession> onEnd = mOnSessionEnd;
if (onEnd != null) onEnd.accept(RippleAnimationSession.this);
if (mCurrentAnimation == expand) mCurrentAnimation = null;
}
});
expand.setInterpolator(LINEAR_INTERPOLATOR);
expand.start();
mCurrentAnimation = expand;
}
private long computeDelay() {
final long timePassed = AnimationUtils.currentAnimationTimeMillis() - mStartTime;
return Math.max((long) ENTER_ANIM_DURATION - timePassed, 0);
}
private void notifyUpdate() {
if (mOnUpdate != null) mOnUpdate.run();
}
RippleAnimationSession setForceSoftwareAnimation(boolean forceSw) {
mForceSoftware = forceSw;
return this;
}
private void exitHardware(RecordingCanvas canvas) {
AnimationProperties<CanvasProperty<Float>, CanvasProperty<Paint>>
props = getCanvasProperties();
RenderNodeAnimator exit =
new RenderNodeAnimator(props.getProgress(), 1f);
exit.setDuration(EXIT_ANIM_DURATION);
exit.addListener(new AnimatorListener(this) {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
if (mLoopAnimation != null) mLoopAnimation.cancel();
Consumer<RippleAnimationSession> onEnd = mOnSessionEnd;
if (onEnd != null) onEnd.accept(RippleAnimationSession.this);
if (mCurrentAnimation == exit) mCurrentAnimation = null;
}
});
exit.setTarget(canvas);
exit.setInterpolator(LINEAR_INTERPOLATOR);
long delay = computeDelay();
exit.setStartDelay(delay);
exit.start();
mCurrentAnimation = exit;
}
private void enterHardware(RecordingCanvas canvas) {
AnimationProperties<CanvasProperty<Float>, CanvasProperty<Paint>>
props = getCanvasProperties();
RenderNodeAnimator expand =
new RenderNodeAnimator(props.getProgress(), .5f);
expand.setTarget(canvas);
RenderNodeAnimator loop = new RenderNodeAnimator(props.getNoisePhase(),
mStartTime + MAX_NOISE_PHASE);
loop.setTarget(canvas);
startAnimation(expand, loop);
mCurrentAnimation = expand;
}
private void startAnimation(Animator expand, Animator loop) {
expand.setDuration(ENTER_ANIM_DURATION);
expand.addListener(new AnimatorListener(this));
expand.setInterpolator(FAST_OUT_SLOW_IN);
expand.start();
loop.setDuration(NOISE_ANIMATION_DURATION);
loop.addListener(new AnimatorListener(this) {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mLoopAnimation = null;
}
});
loop.setInterpolator(LINEAR_INTERPOLATOR);
loop.start();
if (mLoopAnimation != null) mLoopAnimation.cancel();
mLoopAnimation = loop;
}
private void enterSoftware() {
ValueAnimator expand = ValueAnimator.ofFloat(0f, 0.5f);
expand.addUpdateListener(updatedAnimation -> {
notifyUpdate();
mProperties.getShader().setProgress((float) expand.getAnimatedValue());
});
ValueAnimator loop = ValueAnimator.ofFloat(mStartTime, mStartTime + MAX_NOISE_PHASE);
loop.addUpdateListener(updatedAnimation -> {
notifyUpdate();
mProperties.getShader().setNoisePhase((float) loop.getAnimatedValue());
});
startAnimation(expand, loop);
mCurrentAnimation = expand;
}
void setRadius(float radius) {
mProperties.setRadius(radius);
mProperties.getShader().setRadius(radius);
if (mCanvasProperties != null) {
mCanvasProperties.setRadius(CanvasProperty.createFloat(radius));
mCanvasProperties.getShader().setRadius(radius);
}
}
@NonNull AnimationProperties<Float, Paint> getProperties() {
return mProperties;
}
@NonNull
AnimationProperties<CanvasProperty<Float>, CanvasProperty<Paint>> getCanvasProperties() {
if (mCanvasProperties == null) {
mCanvasProperties = new AnimationProperties<>(
CanvasProperty.createFloat(mProperties.getX()),
CanvasProperty.createFloat(mProperties.getY()),
CanvasProperty.createFloat(mProperties.getMaxRadius()),
CanvasProperty.createFloat(mProperties.getNoisePhase()),
CanvasProperty.createPaint(mProperties.getPaint()),
CanvasProperty.createFloat(mProperties.getProgress()),
mProperties.getColor(),
mProperties.getShader());
}
return mCanvasProperties;
}
private static class AnimatorListener implements Animator.AnimatorListener {
private final RippleAnimationSession mSession;
AnimatorListener(RippleAnimationSession session) {
mSession = session;
}
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
mSession.onAnimationEnd(animation);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
}
static class AnimationProperties<FloatType, PaintType> {
private final FloatType mProgress;
private FloatType mMaxRadius;
private final FloatType mNoisePhase;
private final PaintType mPaint;
private final RippleShader mShader;
private final @ColorInt int mColor;
private FloatType mX;
private FloatType mY;
AnimationProperties(FloatType x, FloatType y, FloatType maxRadius, FloatType noisePhase,
PaintType paint, FloatType progress, @ColorInt int color, RippleShader shader) {
mY = y;
mX = x;
mMaxRadius = maxRadius;
mNoisePhase = noisePhase;
mPaint = paint;
mShader = shader;
mProgress = progress;
mColor = color;
}
FloatType getProgress() {
return mProgress;
}
void setRadius(FloatType radius) {
mMaxRadius = radius;
}
void setOrigin(FloatType x, FloatType y) {
mX = x;
mY = y;
}
FloatType getX() {
return mX;
}
FloatType getY() {
return mY;
}
FloatType getMaxRadius() {
return mMaxRadius;
}
PaintType getPaint() {
return mPaint;
}
RippleShader getShader() {
return mShader;
}
FloatType getNoisePhase() {
return mNoisePhase;
}
@ColorInt int getColor() {
return mColor;
}
}
}