/* * 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 mOnSessionEnd; private final AnimationProperties mProperties; private AnimationProperties, CanvasProperty> mCanvasProperties; private Runnable mOnUpdate; private long mStartTime; private boolean mForceSoftware; private Animator mLoopAnimation; private Animator mCurrentAnimation; RippleAnimationSession(@NonNull AnimationProperties 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 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 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> 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 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> 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 getProperties() { return mProperties; } @NonNull AnimationProperties, CanvasProperty> 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 { 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; } } }