Loading graphics/java/android/graphics/drawable/RippleAnimationSession.java +85 −48 Original line number Diff line number Diff line Loading @@ -27,8 +27,9 @@ import android.graphics.Paint; import android.graphics.RecordingCanvas; import android.graphics.animation.RenderNodeAnimator; import android.util.ArraySet; import android.view.animation.DecelerateInterpolator; import android.view.animation.AnimationUtils; import android.view.animation.LinearInterpolator; import android.view.animation.PathInterpolator; import java.util.function.Consumer; Loading @@ -36,32 +37,41 @@ import java.util.function.Consumer; * @hide */ public final class RippleAnimationSession { private static final int ENTER_ANIM_DURATION = 350; private static final int EXIT_ANIM_OFFSET = ENTER_ANIM_DURATION; private static final int EXIT_ANIM_DURATION = 350; private static final String TAG = "RippleAnimationSession"; private static final int ENTER_ANIM_DURATION = 300; private static final int SLIDE_ANIM_DURATION = 450; private static final int EXIT_ANIM_DURATION = 300; 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 DecelerateInterpolator(); private static final TimeInterpolator PATH_INTERPOLATOR = new PathInterpolator(.2f, 0, 0, 1f); private Consumer<RippleAnimationSession> mOnSessionEnd; private AnimationProperties<Float, Paint> mProperties; private final AnimationProperties<Float, Paint> mProperties; private AnimationProperties<CanvasProperty<Float>, CanvasProperty<Paint>> mCanvasProperties; private Runnable mOnUpdate; private long mStartTime; private boolean mForceSoftware; private ArraySet<Animator> mActiveAnimations = new ArraySet(3); private final float mWidth, mHeight; private final ValueAnimator mSparkle = ValueAnimator.ofFloat(0, 1); private final ArraySet<Animator> mActiveAnimations = new ArraySet<>(3); RippleAnimationSession(@NonNull AnimationProperties<Float, Paint> properties, boolean forceSoftware) { boolean forceSoftware, float width, float height) { mProperties = properties; mForceSoftware = forceSoftware; } void end() { for (Animator anim: mActiveAnimations) { if (anim != null) anim.end(); } mActiveAnimations.clear(); mWidth = width; mHeight = height; mSparkle.addUpdateListener(anim -> { final long now = AnimationUtils.currentAnimationTimeMillis(); final long elapsed = now - mStartTime - ENTER_ANIM_DURATION; final float phase = (float) elapsed / 1000f; mProperties.getShader().setSecondsOffset(phase); notifyUpdate(); }); mSparkle.setDuration(ENTER_ANIM_DURATION); mSparkle.setStartDelay(ENTER_ANIM_DURATION); mSparkle.setInterpolator(LINEAR_INTERPOLATOR); mSparkle.setRepeatCount(ValueAnimator.INFINITE); } @NonNull RippleAnimationSession enter(Canvas canvas) { Loading @@ -70,17 +80,19 @@ public final class RippleAnimationSession { } else { enterSoftware(); } mStartTime = System.nanoTime(); mStartTime = AnimationUtils.currentAnimationTimeMillis(); return this; } @NonNull RippleAnimationSession exit(Canvas canvas) { mSparkle.end(); if (isHwAccelerated(canvas)) exitHardware((RecordingCanvas) canvas); else exitSoftware(); return this; } private void onAnimationEnd(Animator anim) { notifyUpdate(); mActiveAnimations.remove(anim); } Loading @@ -92,7 +104,6 @@ public final class RippleAnimationSession { RippleAnimationSession setOnAnimationUpdated(@Nullable Runnable run) { mOnUpdate = run; mProperties.setOnChange(mOnUpdate); return this; } Loading Loading @@ -122,14 +133,12 @@ public final class RippleAnimationSession { } private long computeDelay() { long currentTime = System.nanoTime(); long timePassed = (currentTime - mStartTime) / 1_000_000; long difference = EXIT_ANIM_OFFSET; return Math.max(difference - timePassed, 0); final long timePassed = AnimationUtils.currentAnimationTimeMillis() - mStartTime; return Math.max((long) SLIDE_ANIM_DURATION - timePassed, 0); } private void notifyUpdate() { Runnable onUpdate = mOnUpdate; if (onUpdate != null) onUpdate.run(); if (mOnUpdate != null) mOnUpdate.run(); } RippleAnimationSession setForceSoftwareAnimation(boolean forceSw) { Loading @@ -153,7 +162,7 @@ public final class RippleAnimationSession { } }); exit.setTarget(canvas); exit.setInterpolator(DECELERATE_INTERPOLATOR); exit.setInterpolator(LINEAR_INTERPOLATOR); long delay = computeDelay(); exit.setStartDelay(delay); Loading @@ -161,36 +170,67 @@ public final class RippleAnimationSession { mActiveAnimations.add(exit); } private void enterHardware(RecordingCanvas can) { private void enterHardware(RecordingCanvas canvas) { AnimationProperties<CanvasProperty<Float>, CanvasProperty<Paint>> props = getCanvasProperties(); RenderNodeAnimator expand = new RenderNodeAnimator(props.getProgress(), .5f); expand.setTarget(can); expand.setDuration(ENTER_ANIM_DURATION); expand.addListener(new AnimatorListener(this)); RenderNodeAnimator slideX = new RenderNodeAnimator(props.getX(), mWidth / 2); RenderNodeAnimator slideY = new RenderNodeAnimator(props.getY(), mHeight / 2); expand.setTarget(canvas); slideX.setTarget(canvas); slideY.setTarget(canvas); startAnimation(expand, slideX, slideY); } private void startAnimation(Animator expand, Animator slideX, Animator slideY) { expand.setDuration(SLIDE_ANIM_DURATION); slideX.setDuration(SLIDE_ANIM_DURATION); slideY.setDuration(SLIDE_ANIM_DURATION); slideX.addListener(new AnimatorListener(this)); expand.setInterpolator(LINEAR_INTERPOLATOR); slideX.setInterpolator(PATH_INTERPOLATOR); slideY.setInterpolator(PATH_INTERPOLATOR); expand.start(); slideX.start(); slideY.start(); if (!mSparkle.isRunning()) { mSparkle.start(); mActiveAnimations.add(mSparkle); } mActiveAnimations.add(expand); mActiveAnimations.add(slideX); mActiveAnimations.add(slideY); } private void enterSoftware() { ValueAnimator expand = ValueAnimator.ofFloat(0f, 0.5f); ValueAnimator slideX = ValueAnimator.ofFloat( mProperties.getX(), mWidth / 2); ValueAnimator slideY = ValueAnimator.ofFloat( mProperties.getY(), mHeight / 2); expand.addUpdateListener(updatedAnimation -> { notifyUpdate(); mProperties.getShader().setProgress((Float) expand.getAnimatedValue()); }); expand.addListener(new AnimatorListener(this)); expand.setInterpolator(LINEAR_INTERPOLATOR); expand.start(); mActiveAnimations.add(expand); slideX.addUpdateListener(anim -> { float x = (float) slideX.getAnimatedValue(); float y = (float) slideY.getAnimatedValue(); mProperties.setOrigin(x, y); mProperties.getShader().setOrigin(x, y); }); startAnimation(expand, slideX, slideY); } @NonNull AnimationProperties<Float, Paint> getProperties() { return mProperties; } @NonNull AnimationProperties getCanvasProperties() { @NonNull AnimationProperties<CanvasProperty<Float>, CanvasProperty<Paint>> getCanvasProperties() { if (mCanvasProperties == null) { mCanvasProperties = new AnimationProperties<>( CanvasProperty.createFloat(mProperties.getX()), Loading @@ -209,6 +249,7 @@ public final class RippleAnimationSession { AnimatorListener(RippleAnimationSession session) { mSession = session; } @Override public void onAnimationStart(Animator animation) { Loading @@ -231,21 +272,12 @@ public final class RippleAnimationSession { } static class AnimationProperties<FloatType, PaintType> { private final FloatType mY; private FloatType mProgress; private FloatType mMaxRadius; private final FloatType mProgress; private final FloatType mMaxRadius; private final PaintType mPaint; private final FloatType mX; private final RippleShader mShader; private Runnable mOnChange; private void onChange() { if (mOnChange != null) mOnChange.run(); } private void setOnChange(Runnable onChange) { mOnChange = onChange; } private FloatType mX; private FloatType mY; AnimationProperties(FloatType x, FloatType y, FloatType maxRadius, PaintType paint, FloatType progress, RippleShader shader) { Loading @@ -261,6 +293,11 @@ public final class RippleAnimationSession { return mProgress; } void setOrigin(FloatType x, FloatType y) { mX = x; mY = y; } FloatType getX() { return mX; } Loading graphics/java/android/graphics/drawable/RippleDrawable.java +29 −29 Original line number Diff line number Diff line Loading @@ -48,6 +48,7 @@ import android.graphics.RecordingCanvas; import android.graphics.Rect; import android.graphics.Shader; import android.os.Build; import android.os.SystemProperties; import android.util.AttributeSet; import android.view.animation.LinearInterpolator; Loading Loading @@ -159,6 +160,9 @@ public class RippleDrawable extends LayerDrawable { /** The maximum number of ripples supported. */ private static final int MAX_RIPPLES = 10; private static final LinearInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); /** Temporary flag for teamfood. **/ private static final boolean FORCE_PATTERNED_STYLE = SystemProperties.getBoolean("persist.material.patternedripple", false); private final Rect mTempRect = new Rect(); Loading Loading @@ -361,7 +365,9 @@ public class RippleDrawable extends LayerDrawable { } } else { if (focused || hovered) { if (!pressed) { enterPatternedBackgroundAnimation(focused, hovered); } } else { exitPatternedBackgroundAnimation(); } Loading Loading @@ -571,7 +577,10 @@ public class RippleDrawable extends LayerDrawable { mState.mMaxRadius = a.getDimensionPixelSize( R.styleable.RippleDrawable_radius, mState.mMaxRadius); mState.mRippleStyle = a.getInteger(R.styleable.RippleDrawable_rippleStyle, STYLE_SOLID); if (!FORCE_PATTERNED_STYLE) { mState.mRippleStyle = a.getInteger(R.styleable.RippleDrawable_rippleStyle, mState.mRippleStyle); } } private void verifyRequiredAttributes(@NonNull TypedArray a) throws XmlPullParserException { Loading Loading @@ -812,21 +821,25 @@ public class RippleDrawable extends LayerDrawable { } private void drawPatterned(@NonNull Canvas canvas) { final Rect bounds = getBounds(); final Rect bounds = getDirtyBounds(); final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG); boolean useCanvasProps = shouldUseCanvasProps(canvas); boolean changedHotspotBounds = !bounds.equals(mHotspotBounds); if (isBounded()) { canvas.clipRect(mHotspotBounds); canvas.clipRect(bounds); } float x, y; float x, y, w, h; if (changedHotspotBounds) { x = mHotspotBounds.exactCenterX(); y = mHotspotBounds.exactCenterY(); w = mHotspotBounds.width(); h = mHotspotBounds.height(); useCanvasProps = false; } else { x = mPendingX; y = mPendingY; w = bounds.width(); h = bounds.height(); } boolean shouldAnimate = mRippleActive; boolean shouldExit = mExitingAnimation; Loading @@ -837,9 +850,9 @@ public class RippleDrawable extends LayerDrawable { drawPatternedBackground(canvas); if (shouldAnimate && mRunningAnimations.size() <= MAX_RIPPLES) { RippleAnimationSession.AnimationProperties<Float, Paint> properties = createAnimationProperties(x, y); mRunningAnimations.add(new RippleAnimationSession(properties, !useCanvasProps) .setOnAnimationUpdated(useCanvasProps ? null : () -> invalidateSelf(false)) createAnimationProperties(x, y, w, h); mRunningAnimations.add(new RippleAnimationSession(properties, !useCanvasProps, w, h) .setOnAnimationUpdated(() -> invalidateSelf(false)) .setOnSessionEnd(session -> { mRunningAnimations.remove(session); }) Loading @@ -864,20 +877,8 @@ public class RippleDrawable extends LayerDrawable { } else { RippleAnimationSession.AnimationProperties<Float, Paint> p = s.getProperties(); float posX, posY; if (changedHotspotBounds) { posX = x; posY = y; if (p.getPaint().getShader() instanceof RippleShader) { RippleShader shader = (RippleShader) p.getPaint().getShader(); shader.setOrigin(posX, posY); } } else { posX = p.getX(); posY = p.getY(); } float radius = p.getMaxRadius(); canvas.drawCircle(posX, posY, radius, p.getPaint()); canvas.drawCircle(p.getX(), p.getY(), radius, p.getPaint()); } } canvas.restoreToCount(saveCount); Loading Loading @@ -905,14 +906,13 @@ public class RippleDrawable extends LayerDrawable { private float computeRadius() { Rect b = getDirtyBounds(); float gap = 0; float radius = (float) Math.sqrt(b.width() * b.width() + b.height() * b.height()) / 2 + gap; float radius = (float) Math.sqrt(b.width() * b.width() + b.height() * b.height()) / 2; return radius; } @NonNull private RippleAnimationSession.AnimationProperties<Float, Paint> createAnimationProperties( float x, float y) { float x, float y, float w, float h) { Paint p = new Paint(mRipplePaint); float radius = mState.mMaxRadius; RippleAnimationSession.AnimationProperties<Float, Paint> properties; Loading @@ -920,19 +920,19 @@ public class RippleDrawable extends LayerDrawable { int color = mMaskColorFilter == null ? mState.mColor.getColorForState(getState(), Color.BLACK) : mMaskColorFilter.getColor(); color = color | 0xFF000000; shader.setColor(color); shader.setOrigin(x, y); shader.setResolution(w, h); shader.setSecondsOffset(0); shader.setRadius(radius); shader.setProgress(.0f); properties = new RippleAnimationSession.AnimationProperties<>( x, y, radius, p, 0f, shader); if (mMaskShader == null) { shader.setHasMask(false); shader.setShader(null); } else { shader.setShader(mMaskShader); shader.setHasMask(true); } p.setShader(shader); p.setColorFilter(null); Loading Loading @@ -1160,7 +1160,7 @@ public class RippleDrawable extends LayerDrawable { // The ripple timing depends on the paint's alpha value, so we need // to push just the alpha channel into the paint and let the filter // handle the full-alpha color. int maskColor = color | 0xFF000000; int maskColor = mState.mRippleStyle == STYLE_PATTERNED ? color : color | 0xFF000000; if (mMaskColorFilter.getColor() != maskColor) { mMaskColorFilter = new PorterDuffColorFilter(maskColor, mMaskColorFilter.getMode()); } Loading Loading @@ -1276,7 +1276,7 @@ public class RippleDrawable extends LayerDrawable { @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) ColorStateList mColor = ColorStateList.valueOf(Color.MAGENTA); int mMaxRadius = RADIUS_AUTO; int mRippleStyle = STYLE_SOLID; int mRippleStyle = FORCE_PATTERNED_STYLE ? STYLE_PATTERNED : STYLE_SOLID; public RippleState(LayerState orig, RippleDrawable owner, Resources res) { super(orig, owner, res); Loading graphics/java/android/graphics/drawable/RippleShader.java +135 −37 Original line number Diff line number Diff line Loading @@ -17,58 +17,153 @@ package android.graphics.drawable; import android.annotation.ColorInt; import android.annotation.NonNull; import android.graphics.Color; import android.graphics.RuntimeShader; import android.graphics.Shader; final class RippleShader extends RuntimeShader { private static final String SHADER = "uniform float2 in_origin;\n" + "uniform float in_maxRadius;\n" private static final String SHADER_UNIFORMS = "uniform vec2 in_origin;\n" + "uniform float in_progress;\n" + "uniform float in_maxRadius;\n" + "uniform vec2 in_resolution;\n" + "uniform float in_hasMask;\n" + "uniform float4 in_color;\n" + "uniform shader in_shader;\n" + "float dist2(float2 p0, float2 pf) { return sqrt((pf.x - p0.x) * (pf.x - p0.x) + " + "(pf.y - p0.y) * (pf.y - p0.y)); }\n" + "float mod2(float a, float b) { return a - (b * floor(a / b)); }\n" + "float rand(float2 src) { return fract(sin(dot(src.xy, float2(12.9898, 78.233))) * " + "43758.5453123); }\n" + "float4 main(float2 p)\n" + "{\n" + " float fraction = in_progress;\n" + " float2 fragCoord = p;//sk_FragCoord.xy;\n" + " float maxDist = in_maxRadius;\n" + " float fragDist = dist2(in_origin, fragCoord.xy);\n" + " float circleRadius = maxDist * fraction;\n" + " float colorVal = (fragDist - circleRadius) / maxDist;\n" + " float d = fragDist < circleRadius \n" + " ? 1. - abs(colorVal * 3. * smoothstep(0., 1., fraction)) \n" + " : 1. - abs(colorVal * 5.);\n" + " d = smoothstep(0., 1., d);\n" + " float divider = 2.;\n" + " float x = floor(fragCoord.x / divider);\n" + " float y = floor(fragCoord.y / divider);\n" + " float density = .95;\n" + " d = rand(float2(x, y)) > density ? d : d * .2;\n" + " d = d * rand(float2(fraction, x * y));\n" + " float alpha = 1. - pow(fraction, 2.);\n" + " if (in_hasMask != 0.) {return sample(in_shader).a * in_color * d * alpha;}\n" + " return in_color * d * alpha;\n" + "uniform float in_secondsOffset;\n" + "uniform vec4 in_color;\n" + "uniform shader in_shader;\n"; private static final String SHADER_LIB = "float triangleNoise(vec2 n) {\n" + " n = fract(n * vec2(5.3987, 5.4421));\n" + " n += dot(n.yx, n.xy + vec2(21.5351, 14.3137));\n" + " float xy = n.x * n.y;\n" + " return fract(xy * 95.4307) + fract(xy * 75.04961) - 1.0;\n" + "}" + "const float PI = 3.1415926535897932384626;\n" + "\n" + "float threshold(float v, float l, float h) {\n" + " return step(l, v) * (1.0 - step(h, v));\n" + "}\n" + "\n" + "float sparkles(vec2 uv, float t) {\n" + " float n = triangleNoise(uv);\n" + " float s = 0.0;\n" + " for (float i = 0; i < 4; i += 1) {\n" + " float l = i * 0.25;\n" + " float h = l + 0.025;\n" + " float o = abs(sin(0.1 * PI * (t + i)));\n" + " s += threshold(n + o, l, h);\n" + " }\n" + " return saturate(s);\n" + "}\n" + "\n" + "float softCircle(vec2 uv, vec2 xy, float radius, float blur) {\n" + " float blurHalf = blur * 0.5;\n" + " float d = distance(uv, xy);\n" + " return 1. - smoothstep(1. - blurHalf, 1. + blurHalf, d / radius);\n" + "}\n" + "\n" + "float softRing(vec2 uv, vec2 xy, float radius, float blur) {\n" + " float thickness = 0.4;\n" + " float circle_outer = softCircle(uv, xy, radius + thickness * 0.5, blur);\n" + " float circle_inner = softCircle(uv, xy, radius - thickness * 0.5, blur);\n" + " return circle_outer - circle_inner;\n" + "}\n" + "\n" + "struct Viewport {\n" + " float aspect;\n" + " vec2 uv;\n" + " vec2 resolution_pixels;\n" + "};\n" + "\n" + "Viewport getViewport(vec2 frag_coord, vec2 resolution_pixels) {\n" + " Viewport v;\n" + " v.aspect = resolution_pixels.y / resolution_pixels.x;\n" + " v.uv = frag_coord / resolution_pixels;\n" + " v.uv.y = (1.0 - v.uv.y) * v.aspect;\n" + " v.resolution_pixels = resolution_pixels;\n" + " return v;\n" + "}\n" + "\n" + "vec2 getTouch(vec2 touch_position_pixels, Viewport viewport) {\n" + " vec2 touch = touch_position_pixels / viewport.resolution_pixels;\n" + " touch.y *= viewport.aspect;\n" + " return touch;\n" + "}\n" + "\n" + "struct Wave {\n" + " float ring;\n" + " float circle;\n" + "};\n" + "\n" + "Wave getWave(Viewport viewport, vec2 touch, float progress) {\n" + " float fade = pow((clamp(progress, 0.8, 1.0)), 8.);\n" + " Wave w;\n" + " w.ring = max(softRing(viewport.uv, touch, progress, 0.45) - fade, 0.);\n" + " w.circle = softCircle(viewport.uv, touch, 2.0 * progress, 0.2) - progress;\n" + " return w;\n" + "}\n" + "\n" + "vec4 getRipple(vec4 color, float loudness, float sparkle, Wave wave) {\n" + " float alpha = wave.ring * sparkle * loudness\n" + " + wave.circle * color.a;\n" + " return vec4(color.rgb, saturate(alpha));\n" + "}\n" + "\n" + "float getRingMask(vec2 frag, vec2 center, float r, float progress) {\n" + " float dist = distance(frag, center);\n" + " float expansion = r * .6;\n" + " r = r * min(1.,progress);\n" + " float minD = max(r - expansion, 0.);\n" + " float maxD = r + expansion;\n" + " if (dist > maxD || dist < minD) return .0;\n" + " return min(maxD - dist, dist - minD) / expansion; \n" + "}\n" + "\n" + "float subProgress(float start, float end, float progress) {\n" + " float sub = clamp(progress, start, end);\n" + " return (sub - start) / (end - start); \n" + "}\n"; private static final String SHADER_MAIN = "vec4 main(vec2 p) {\n" + " float fadeIn = subProgress(0., 0.175, in_progress);\n" + " float fadeOutNoise = subProgress(0.375, 1., in_progress);\n" + " float fadeOutRipple = subProgress(0.375, 0.75, in_progress);\n" + " Viewport vp = getViewport(p, in_resolution);\n" + " vec2 touch = getTouch(in_origin, vp);\n" + " Wave w = getWave(vp, touch, in_progress * 0.25);\n" + " float ring = getRingMask(p, in_origin, in_maxRadius, fadeIn);\n" + " float alpha = min(fadeIn, 1. - fadeOutNoise);\n" + " float sparkle = sparkles(p, in_progress * 0.25 + in_secondsOffset)\n" + " * ring * alpha;\n" + " vec4 r = getRipple(in_color, 1., sparkle, w);\n" + " float fade = min(fadeIn, 1.-fadeOutRipple);\n" + " vec4 circle = vec4(in_color.rgb, softCircle(p, in_origin, in_maxRadius " + " * fadeIn, 0.2) * fade * in_color.a);\n" + " float mask = in_hasMask == 1. ? sample(in_shader).a > 0. ? 1. : 0. : 1.;\n" + " return mix(circle, vec4(1.), sparkle * mask);\n" + "}"; private static final String SHADER = SHADER_UNIFORMS + SHADER_LIB + SHADER_MAIN; RippleShader() { super(SHADER, false); } public void setShader(@NonNull Shader s) { setInputShader("in_shader", s); public void setShader(Shader shader) { if (shader != null) { setInputShader("in_shader", shader); } setUniform("in_hasMask", shader == null ? 0 : 1); } public void setRadius(float radius) { setUniform("in_maxRadius", radius); } /** * Continuous offset used as noise phase. */ public void setSecondsOffset(float t) { setUniform("in_secondsOffset", t); } public void setOrigin(float x, float y) { setUniform("in_origin", new float[] {x, y}); } Loading @@ -77,13 +172,16 @@ final class RippleShader extends RuntimeShader { setUniform("in_progress", progress); } public void setHasMask(boolean hasMask) { setUniform("in_hasMask", hasMask ? 1 : 0); } /** * Color of the circle that's under the sparkles. Sparkles will always be white. */ public void setColor(@ColorInt int colorIn) { Color color = Color.valueOf(colorIn); this.setUniform("in_color", new float[] {color.red(), color.green(), color.blue(), color.alpha()}); } public void setResolution(float w, float h) { setUniform("in_resolution", w, h); } } Loading
graphics/java/android/graphics/drawable/RippleAnimationSession.java +85 −48 Original line number Diff line number Diff line Loading @@ -27,8 +27,9 @@ import android.graphics.Paint; import android.graphics.RecordingCanvas; import android.graphics.animation.RenderNodeAnimator; import android.util.ArraySet; import android.view.animation.DecelerateInterpolator; import android.view.animation.AnimationUtils; import android.view.animation.LinearInterpolator; import android.view.animation.PathInterpolator; import java.util.function.Consumer; Loading @@ -36,32 +37,41 @@ import java.util.function.Consumer; * @hide */ public final class RippleAnimationSession { private static final int ENTER_ANIM_DURATION = 350; private static final int EXIT_ANIM_OFFSET = ENTER_ANIM_DURATION; private static final int EXIT_ANIM_DURATION = 350; private static final String TAG = "RippleAnimationSession"; private static final int ENTER_ANIM_DURATION = 300; private static final int SLIDE_ANIM_DURATION = 450; private static final int EXIT_ANIM_DURATION = 300; 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 DecelerateInterpolator(); private static final TimeInterpolator PATH_INTERPOLATOR = new PathInterpolator(.2f, 0, 0, 1f); private Consumer<RippleAnimationSession> mOnSessionEnd; private AnimationProperties<Float, Paint> mProperties; private final AnimationProperties<Float, Paint> mProperties; private AnimationProperties<CanvasProperty<Float>, CanvasProperty<Paint>> mCanvasProperties; private Runnable mOnUpdate; private long mStartTime; private boolean mForceSoftware; private ArraySet<Animator> mActiveAnimations = new ArraySet(3); private final float mWidth, mHeight; private final ValueAnimator mSparkle = ValueAnimator.ofFloat(0, 1); private final ArraySet<Animator> mActiveAnimations = new ArraySet<>(3); RippleAnimationSession(@NonNull AnimationProperties<Float, Paint> properties, boolean forceSoftware) { boolean forceSoftware, float width, float height) { mProperties = properties; mForceSoftware = forceSoftware; } void end() { for (Animator anim: mActiveAnimations) { if (anim != null) anim.end(); } mActiveAnimations.clear(); mWidth = width; mHeight = height; mSparkle.addUpdateListener(anim -> { final long now = AnimationUtils.currentAnimationTimeMillis(); final long elapsed = now - mStartTime - ENTER_ANIM_DURATION; final float phase = (float) elapsed / 1000f; mProperties.getShader().setSecondsOffset(phase); notifyUpdate(); }); mSparkle.setDuration(ENTER_ANIM_DURATION); mSparkle.setStartDelay(ENTER_ANIM_DURATION); mSparkle.setInterpolator(LINEAR_INTERPOLATOR); mSparkle.setRepeatCount(ValueAnimator.INFINITE); } @NonNull RippleAnimationSession enter(Canvas canvas) { Loading @@ -70,17 +80,19 @@ public final class RippleAnimationSession { } else { enterSoftware(); } mStartTime = System.nanoTime(); mStartTime = AnimationUtils.currentAnimationTimeMillis(); return this; } @NonNull RippleAnimationSession exit(Canvas canvas) { mSparkle.end(); if (isHwAccelerated(canvas)) exitHardware((RecordingCanvas) canvas); else exitSoftware(); return this; } private void onAnimationEnd(Animator anim) { notifyUpdate(); mActiveAnimations.remove(anim); } Loading @@ -92,7 +104,6 @@ public final class RippleAnimationSession { RippleAnimationSession setOnAnimationUpdated(@Nullable Runnable run) { mOnUpdate = run; mProperties.setOnChange(mOnUpdate); return this; } Loading Loading @@ -122,14 +133,12 @@ public final class RippleAnimationSession { } private long computeDelay() { long currentTime = System.nanoTime(); long timePassed = (currentTime - mStartTime) / 1_000_000; long difference = EXIT_ANIM_OFFSET; return Math.max(difference - timePassed, 0); final long timePassed = AnimationUtils.currentAnimationTimeMillis() - mStartTime; return Math.max((long) SLIDE_ANIM_DURATION - timePassed, 0); } private void notifyUpdate() { Runnable onUpdate = mOnUpdate; if (onUpdate != null) onUpdate.run(); if (mOnUpdate != null) mOnUpdate.run(); } RippleAnimationSession setForceSoftwareAnimation(boolean forceSw) { Loading @@ -153,7 +162,7 @@ public final class RippleAnimationSession { } }); exit.setTarget(canvas); exit.setInterpolator(DECELERATE_INTERPOLATOR); exit.setInterpolator(LINEAR_INTERPOLATOR); long delay = computeDelay(); exit.setStartDelay(delay); Loading @@ -161,36 +170,67 @@ public final class RippleAnimationSession { mActiveAnimations.add(exit); } private void enterHardware(RecordingCanvas can) { private void enterHardware(RecordingCanvas canvas) { AnimationProperties<CanvasProperty<Float>, CanvasProperty<Paint>> props = getCanvasProperties(); RenderNodeAnimator expand = new RenderNodeAnimator(props.getProgress(), .5f); expand.setTarget(can); expand.setDuration(ENTER_ANIM_DURATION); expand.addListener(new AnimatorListener(this)); RenderNodeAnimator slideX = new RenderNodeAnimator(props.getX(), mWidth / 2); RenderNodeAnimator slideY = new RenderNodeAnimator(props.getY(), mHeight / 2); expand.setTarget(canvas); slideX.setTarget(canvas); slideY.setTarget(canvas); startAnimation(expand, slideX, slideY); } private void startAnimation(Animator expand, Animator slideX, Animator slideY) { expand.setDuration(SLIDE_ANIM_DURATION); slideX.setDuration(SLIDE_ANIM_DURATION); slideY.setDuration(SLIDE_ANIM_DURATION); slideX.addListener(new AnimatorListener(this)); expand.setInterpolator(LINEAR_INTERPOLATOR); slideX.setInterpolator(PATH_INTERPOLATOR); slideY.setInterpolator(PATH_INTERPOLATOR); expand.start(); slideX.start(); slideY.start(); if (!mSparkle.isRunning()) { mSparkle.start(); mActiveAnimations.add(mSparkle); } mActiveAnimations.add(expand); mActiveAnimations.add(slideX); mActiveAnimations.add(slideY); } private void enterSoftware() { ValueAnimator expand = ValueAnimator.ofFloat(0f, 0.5f); ValueAnimator slideX = ValueAnimator.ofFloat( mProperties.getX(), mWidth / 2); ValueAnimator slideY = ValueAnimator.ofFloat( mProperties.getY(), mHeight / 2); expand.addUpdateListener(updatedAnimation -> { notifyUpdate(); mProperties.getShader().setProgress((Float) expand.getAnimatedValue()); }); expand.addListener(new AnimatorListener(this)); expand.setInterpolator(LINEAR_INTERPOLATOR); expand.start(); mActiveAnimations.add(expand); slideX.addUpdateListener(anim -> { float x = (float) slideX.getAnimatedValue(); float y = (float) slideY.getAnimatedValue(); mProperties.setOrigin(x, y); mProperties.getShader().setOrigin(x, y); }); startAnimation(expand, slideX, slideY); } @NonNull AnimationProperties<Float, Paint> getProperties() { return mProperties; } @NonNull AnimationProperties getCanvasProperties() { @NonNull AnimationProperties<CanvasProperty<Float>, CanvasProperty<Paint>> getCanvasProperties() { if (mCanvasProperties == null) { mCanvasProperties = new AnimationProperties<>( CanvasProperty.createFloat(mProperties.getX()), Loading @@ -209,6 +249,7 @@ public final class RippleAnimationSession { AnimatorListener(RippleAnimationSession session) { mSession = session; } @Override public void onAnimationStart(Animator animation) { Loading @@ -231,21 +272,12 @@ public final class RippleAnimationSession { } static class AnimationProperties<FloatType, PaintType> { private final FloatType mY; private FloatType mProgress; private FloatType mMaxRadius; private final FloatType mProgress; private final FloatType mMaxRadius; private final PaintType mPaint; private final FloatType mX; private final RippleShader mShader; private Runnable mOnChange; private void onChange() { if (mOnChange != null) mOnChange.run(); } private void setOnChange(Runnable onChange) { mOnChange = onChange; } private FloatType mX; private FloatType mY; AnimationProperties(FloatType x, FloatType y, FloatType maxRadius, PaintType paint, FloatType progress, RippleShader shader) { Loading @@ -261,6 +293,11 @@ public final class RippleAnimationSession { return mProgress; } void setOrigin(FloatType x, FloatType y) { mX = x; mY = y; } FloatType getX() { return mX; } Loading
graphics/java/android/graphics/drawable/RippleDrawable.java +29 −29 Original line number Diff line number Diff line Loading @@ -48,6 +48,7 @@ import android.graphics.RecordingCanvas; import android.graphics.Rect; import android.graphics.Shader; import android.os.Build; import android.os.SystemProperties; import android.util.AttributeSet; import android.view.animation.LinearInterpolator; Loading Loading @@ -159,6 +160,9 @@ public class RippleDrawable extends LayerDrawable { /** The maximum number of ripples supported. */ private static final int MAX_RIPPLES = 10; private static final LinearInterpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); /** Temporary flag for teamfood. **/ private static final boolean FORCE_PATTERNED_STYLE = SystemProperties.getBoolean("persist.material.patternedripple", false); private final Rect mTempRect = new Rect(); Loading Loading @@ -361,7 +365,9 @@ public class RippleDrawable extends LayerDrawable { } } else { if (focused || hovered) { if (!pressed) { enterPatternedBackgroundAnimation(focused, hovered); } } else { exitPatternedBackgroundAnimation(); } Loading Loading @@ -571,7 +577,10 @@ public class RippleDrawable extends LayerDrawable { mState.mMaxRadius = a.getDimensionPixelSize( R.styleable.RippleDrawable_radius, mState.mMaxRadius); mState.mRippleStyle = a.getInteger(R.styleable.RippleDrawable_rippleStyle, STYLE_SOLID); if (!FORCE_PATTERNED_STYLE) { mState.mRippleStyle = a.getInteger(R.styleable.RippleDrawable_rippleStyle, mState.mRippleStyle); } } private void verifyRequiredAttributes(@NonNull TypedArray a) throws XmlPullParserException { Loading Loading @@ -812,21 +821,25 @@ public class RippleDrawable extends LayerDrawable { } private void drawPatterned(@NonNull Canvas canvas) { final Rect bounds = getBounds(); final Rect bounds = getDirtyBounds(); final int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG); boolean useCanvasProps = shouldUseCanvasProps(canvas); boolean changedHotspotBounds = !bounds.equals(mHotspotBounds); if (isBounded()) { canvas.clipRect(mHotspotBounds); canvas.clipRect(bounds); } float x, y; float x, y, w, h; if (changedHotspotBounds) { x = mHotspotBounds.exactCenterX(); y = mHotspotBounds.exactCenterY(); w = mHotspotBounds.width(); h = mHotspotBounds.height(); useCanvasProps = false; } else { x = mPendingX; y = mPendingY; w = bounds.width(); h = bounds.height(); } boolean shouldAnimate = mRippleActive; boolean shouldExit = mExitingAnimation; Loading @@ -837,9 +850,9 @@ public class RippleDrawable extends LayerDrawable { drawPatternedBackground(canvas); if (shouldAnimate && mRunningAnimations.size() <= MAX_RIPPLES) { RippleAnimationSession.AnimationProperties<Float, Paint> properties = createAnimationProperties(x, y); mRunningAnimations.add(new RippleAnimationSession(properties, !useCanvasProps) .setOnAnimationUpdated(useCanvasProps ? null : () -> invalidateSelf(false)) createAnimationProperties(x, y, w, h); mRunningAnimations.add(new RippleAnimationSession(properties, !useCanvasProps, w, h) .setOnAnimationUpdated(() -> invalidateSelf(false)) .setOnSessionEnd(session -> { mRunningAnimations.remove(session); }) Loading @@ -864,20 +877,8 @@ public class RippleDrawable extends LayerDrawable { } else { RippleAnimationSession.AnimationProperties<Float, Paint> p = s.getProperties(); float posX, posY; if (changedHotspotBounds) { posX = x; posY = y; if (p.getPaint().getShader() instanceof RippleShader) { RippleShader shader = (RippleShader) p.getPaint().getShader(); shader.setOrigin(posX, posY); } } else { posX = p.getX(); posY = p.getY(); } float radius = p.getMaxRadius(); canvas.drawCircle(posX, posY, radius, p.getPaint()); canvas.drawCircle(p.getX(), p.getY(), radius, p.getPaint()); } } canvas.restoreToCount(saveCount); Loading Loading @@ -905,14 +906,13 @@ public class RippleDrawable extends LayerDrawable { private float computeRadius() { Rect b = getDirtyBounds(); float gap = 0; float radius = (float) Math.sqrt(b.width() * b.width() + b.height() * b.height()) / 2 + gap; float radius = (float) Math.sqrt(b.width() * b.width() + b.height() * b.height()) / 2; return radius; } @NonNull private RippleAnimationSession.AnimationProperties<Float, Paint> createAnimationProperties( float x, float y) { float x, float y, float w, float h) { Paint p = new Paint(mRipplePaint); float radius = mState.mMaxRadius; RippleAnimationSession.AnimationProperties<Float, Paint> properties; Loading @@ -920,19 +920,19 @@ public class RippleDrawable extends LayerDrawable { int color = mMaskColorFilter == null ? mState.mColor.getColorForState(getState(), Color.BLACK) : mMaskColorFilter.getColor(); color = color | 0xFF000000; shader.setColor(color); shader.setOrigin(x, y); shader.setResolution(w, h); shader.setSecondsOffset(0); shader.setRadius(radius); shader.setProgress(.0f); properties = new RippleAnimationSession.AnimationProperties<>( x, y, radius, p, 0f, shader); if (mMaskShader == null) { shader.setHasMask(false); shader.setShader(null); } else { shader.setShader(mMaskShader); shader.setHasMask(true); } p.setShader(shader); p.setColorFilter(null); Loading Loading @@ -1160,7 +1160,7 @@ public class RippleDrawable extends LayerDrawable { // The ripple timing depends on the paint's alpha value, so we need // to push just the alpha channel into the paint and let the filter // handle the full-alpha color. int maskColor = color | 0xFF000000; int maskColor = mState.mRippleStyle == STYLE_PATTERNED ? color : color | 0xFF000000; if (mMaskColorFilter.getColor() != maskColor) { mMaskColorFilter = new PorterDuffColorFilter(maskColor, mMaskColorFilter.getMode()); } Loading Loading @@ -1276,7 +1276,7 @@ public class RippleDrawable extends LayerDrawable { @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) ColorStateList mColor = ColorStateList.valueOf(Color.MAGENTA); int mMaxRadius = RADIUS_AUTO; int mRippleStyle = STYLE_SOLID; int mRippleStyle = FORCE_PATTERNED_STYLE ? STYLE_PATTERNED : STYLE_SOLID; public RippleState(LayerState orig, RippleDrawable owner, Resources res) { super(orig, owner, res); Loading
graphics/java/android/graphics/drawable/RippleShader.java +135 −37 Original line number Diff line number Diff line Loading @@ -17,58 +17,153 @@ package android.graphics.drawable; import android.annotation.ColorInt; import android.annotation.NonNull; import android.graphics.Color; import android.graphics.RuntimeShader; import android.graphics.Shader; final class RippleShader extends RuntimeShader { private static final String SHADER = "uniform float2 in_origin;\n" + "uniform float in_maxRadius;\n" private static final String SHADER_UNIFORMS = "uniform vec2 in_origin;\n" + "uniform float in_progress;\n" + "uniform float in_maxRadius;\n" + "uniform vec2 in_resolution;\n" + "uniform float in_hasMask;\n" + "uniform float4 in_color;\n" + "uniform shader in_shader;\n" + "float dist2(float2 p0, float2 pf) { return sqrt((pf.x - p0.x) * (pf.x - p0.x) + " + "(pf.y - p0.y) * (pf.y - p0.y)); }\n" + "float mod2(float a, float b) { return a - (b * floor(a / b)); }\n" + "float rand(float2 src) { return fract(sin(dot(src.xy, float2(12.9898, 78.233))) * " + "43758.5453123); }\n" + "float4 main(float2 p)\n" + "{\n" + " float fraction = in_progress;\n" + " float2 fragCoord = p;//sk_FragCoord.xy;\n" + " float maxDist = in_maxRadius;\n" + " float fragDist = dist2(in_origin, fragCoord.xy);\n" + " float circleRadius = maxDist * fraction;\n" + " float colorVal = (fragDist - circleRadius) / maxDist;\n" + " float d = fragDist < circleRadius \n" + " ? 1. - abs(colorVal * 3. * smoothstep(0., 1., fraction)) \n" + " : 1. - abs(colorVal * 5.);\n" + " d = smoothstep(0., 1., d);\n" + " float divider = 2.;\n" + " float x = floor(fragCoord.x / divider);\n" + " float y = floor(fragCoord.y / divider);\n" + " float density = .95;\n" + " d = rand(float2(x, y)) > density ? d : d * .2;\n" + " d = d * rand(float2(fraction, x * y));\n" + " float alpha = 1. - pow(fraction, 2.);\n" + " if (in_hasMask != 0.) {return sample(in_shader).a * in_color * d * alpha;}\n" + " return in_color * d * alpha;\n" + "uniform float in_secondsOffset;\n" + "uniform vec4 in_color;\n" + "uniform shader in_shader;\n"; private static final String SHADER_LIB = "float triangleNoise(vec2 n) {\n" + " n = fract(n * vec2(5.3987, 5.4421));\n" + " n += dot(n.yx, n.xy + vec2(21.5351, 14.3137));\n" + " float xy = n.x * n.y;\n" + " return fract(xy * 95.4307) + fract(xy * 75.04961) - 1.0;\n" + "}" + "const float PI = 3.1415926535897932384626;\n" + "\n" + "float threshold(float v, float l, float h) {\n" + " return step(l, v) * (1.0 - step(h, v));\n" + "}\n" + "\n" + "float sparkles(vec2 uv, float t) {\n" + " float n = triangleNoise(uv);\n" + " float s = 0.0;\n" + " for (float i = 0; i < 4; i += 1) {\n" + " float l = i * 0.25;\n" + " float h = l + 0.025;\n" + " float o = abs(sin(0.1 * PI * (t + i)));\n" + " s += threshold(n + o, l, h);\n" + " }\n" + " return saturate(s);\n" + "}\n" + "\n" + "float softCircle(vec2 uv, vec2 xy, float radius, float blur) {\n" + " float blurHalf = blur * 0.5;\n" + " float d = distance(uv, xy);\n" + " return 1. - smoothstep(1. - blurHalf, 1. + blurHalf, d / radius);\n" + "}\n" + "\n" + "float softRing(vec2 uv, vec2 xy, float radius, float blur) {\n" + " float thickness = 0.4;\n" + " float circle_outer = softCircle(uv, xy, radius + thickness * 0.5, blur);\n" + " float circle_inner = softCircle(uv, xy, radius - thickness * 0.5, blur);\n" + " return circle_outer - circle_inner;\n" + "}\n" + "\n" + "struct Viewport {\n" + " float aspect;\n" + " vec2 uv;\n" + " vec2 resolution_pixels;\n" + "};\n" + "\n" + "Viewport getViewport(vec2 frag_coord, vec2 resolution_pixels) {\n" + " Viewport v;\n" + " v.aspect = resolution_pixels.y / resolution_pixels.x;\n" + " v.uv = frag_coord / resolution_pixels;\n" + " v.uv.y = (1.0 - v.uv.y) * v.aspect;\n" + " v.resolution_pixels = resolution_pixels;\n" + " return v;\n" + "}\n" + "\n" + "vec2 getTouch(vec2 touch_position_pixels, Viewport viewport) {\n" + " vec2 touch = touch_position_pixels / viewport.resolution_pixels;\n" + " touch.y *= viewport.aspect;\n" + " return touch;\n" + "}\n" + "\n" + "struct Wave {\n" + " float ring;\n" + " float circle;\n" + "};\n" + "\n" + "Wave getWave(Viewport viewport, vec2 touch, float progress) {\n" + " float fade = pow((clamp(progress, 0.8, 1.0)), 8.);\n" + " Wave w;\n" + " w.ring = max(softRing(viewport.uv, touch, progress, 0.45) - fade, 0.);\n" + " w.circle = softCircle(viewport.uv, touch, 2.0 * progress, 0.2) - progress;\n" + " return w;\n" + "}\n" + "\n" + "vec4 getRipple(vec4 color, float loudness, float sparkle, Wave wave) {\n" + " float alpha = wave.ring * sparkle * loudness\n" + " + wave.circle * color.a;\n" + " return vec4(color.rgb, saturate(alpha));\n" + "}\n" + "\n" + "float getRingMask(vec2 frag, vec2 center, float r, float progress) {\n" + " float dist = distance(frag, center);\n" + " float expansion = r * .6;\n" + " r = r * min(1.,progress);\n" + " float minD = max(r - expansion, 0.);\n" + " float maxD = r + expansion;\n" + " if (dist > maxD || dist < minD) return .0;\n" + " return min(maxD - dist, dist - minD) / expansion; \n" + "}\n" + "\n" + "float subProgress(float start, float end, float progress) {\n" + " float sub = clamp(progress, start, end);\n" + " return (sub - start) / (end - start); \n" + "}\n"; private static final String SHADER_MAIN = "vec4 main(vec2 p) {\n" + " float fadeIn = subProgress(0., 0.175, in_progress);\n" + " float fadeOutNoise = subProgress(0.375, 1., in_progress);\n" + " float fadeOutRipple = subProgress(0.375, 0.75, in_progress);\n" + " Viewport vp = getViewport(p, in_resolution);\n" + " vec2 touch = getTouch(in_origin, vp);\n" + " Wave w = getWave(vp, touch, in_progress * 0.25);\n" + " float ring = getRingMask(p, in_origin, in_maxRadius, fadeIn);\n" + " float alpha = min(fadeIn, 1. - fadeOutNoise);\n" + " float sparkle = sparkles(p, in_progress * 0.25 + in_secondsOffset)\n" + " * ring * alpha;\n" + " vec4 r = getRipple(in_color, 1., sparkle, w);\n" + " float fade = min(fadeIn, 1.-fadeOutRipple);\n" + " vec4 circle = vec4(in_color.rgb, softCircle(p, in_origin, in_maxRadius " + " * fadeIn, 0.2) * fade * in_color.a);\n" + " float mask = in_hasMask == 1. ? sample(in_shader).a > 0. ? 1. : 0. : 1.;\n" + " return mix(circle, vec4(1.), sparkle * mask);\n" + "}"; private static final String SHADER = SHADER_UNIFORMS + SHADER_LIB + SHADER_MAIN; RippleShader() { super(SHADER, false); } public void setShader(@NonNull Shader s) { setInputShader("in_shader", s); public void setShader(Shader shader) { if (shader != null) { setInputShader("in_shader", shader); } setUniform("in_hasMask", shader == null ? 0 : 1); } public void setRadius(float radius) { setUniform("in_maxRadius", radius); } /** * Continuous offset used as noise phase. */ public void setSecondsOffset(float t) { setUniform("in_secondsOffset", t); } public void setOrigin(float x, float y) { setUniform("in_origin", new float[] {x, y}); } Loading @@ -77,13 +172,16 @@ final class RippleShader extends RuntimeShader { setUniform("in_progress", progress); } public void setHasMask(boolean hasMask) { setUniform("in_hasMask", hasMask ? 1 : 0); } /** * Color of the circle that's under the sparkles. Sparkles will always be white. */ public void setColor(@ColorInt int colorIn) { Color color = Color.valueOf(colorIn); this.setUniform("in_color", new float[] {color.red(), color.green(), color.blue(), color.alpha()}); } public void setResolution(float w, float h) { setUniform("in_resolution", w, h); } }