diff options
author | Danny Lin <danny@kdrag0n.dev> | 2021-10-05 21:00:42 -0700 |
---|---|---|
committer | alk3pInjection <webmaster@raspii.tech> | 2022-05-05 00:39:05 +0800 |
commit | c9b288f98d5811d267d4652af7c6eeb01d6cf6cc (patch) | |
tree | fdd478ce8b9dadb95ca772faa2d114c508b289a5 | |
parent | 2924fb0e9462b8768f90eace669884eb70a68e7a (diff) |
ripple: Replace with Fluent Design-inspired ripple animation
This is a new GLSL ripple animation inspired by Microsoft's Fluent
Design, with an emphasis on responsiveness. The first frame of the
animation includes a solid base highlight and a visible portion of the
ripple circle, together serving as immediate feedback on finger up
(especially in cases where few additional frames can be rendered, e.g.
opening activities/fragment and dismissing dialogs).
After the initial frame, the animation consists of a blurred circle that
gradually expands (increasing radius), becomes less blurred, and finally
fades out at the end of the animation. The animation timing follows a
sine-based ease out curve, which is a decent balance between the
animation feeling too fast and too slow/unnatural.
Demo video: https://twitter.com/kdrag0n/status/1445806323535269893
Change-Id: I27192bd406490c39487dc84941f2f5c4a0fb33fe
-rw-r--r-- | graphics/java/android/graphics/drawable/RippleAnimationSession.java | 19 | ||||
-rw-r--r-- | graphics/java/android/graphics/drawable/RippleShader.java | 144 |
2 files changed, 77 insertions, 86 deletions
diff --git a/graphics/java/android/graphics/drawable/RippleAnimationSession.java b/graphics/java/android/graphics/drawable/RippleAnimationSession.java index 872331c82603..066339fc674c 100644 --- a/graphics/java/android/graphics/drawable/RippleAnimationSession.java +++ b/graphics/java/android/graphics/drawable/RippleAnimationSession.java @@ -39,13 +39,13 @@ import java.util.function.Consumer; */ 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 int ENTER_ANIM_DURATION = 350; + private static final int EXIT_ANIM_DURATION = 450; private static final long NOISE_ANIMATION_DURATION = 7000; private static final long MAX_NOISE_PHASE = NOISE_ANIMATION_DURATION / 214; + // Input progress that results in 0.5 after the ease-out sine curve + private static final float MID_PROGRESS = 1.0f / 3.0f; 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; @@ -111,7 +111,7 @@ public final class RippleAnimationSession { } private void exitSoftware() { - ValueAnimator expand = ValueAnimator.ofFloat(.5f, 1f); + ValueAnimator expand = ValueAnimator.ofFloat(MID_PROGRESS, 1f); expand.setDuration(EXIT_ANIM_DURATION); expand.setStartDelay(computeDelay()); expand.addUpdateListener(updatedAnimation -> { @@ -165,9 +165,6 @@ public final class RippleAnimationSession { }); exit.setTarget(canvas); exit.setInterpolator(LINEAR_INTERPOLATOR); - - long delay = computeDelay(); - exit.setStartDelay(delay); exit.start(); mCurrentAnimation = exit; } @@ -176,7 +173,7 @@ public final class RippleAnimationSession { AnimationProperties<CanvasProperty<Float>, CanvasProperty<Paint>> props = getCanvasProperties(); RenderNodeAnimator expand = - new RenderNodeAnimator(props.getProgress(), .5f); + new RenderNodeAnimator(props.getProgress(), MID_PROGRESS); expand.setTarget(canvas); RenderNodeAnimator loop = new RenderNodeAnimator(props.getNoisePhase(), mStartTime + MAX_NOISE_PHASE); @@ -188,7 +185,7 @@ public final class RippleAnimationSession { private void startAnimation(Animator expand, Animator loop) { expand.setDuration(ENTER_ANIM_DURATION); expand.addListener(new AnimatorListener(this)); - expand.setInterpolator(FAST_OUT_SLOW_IN); + expand.setInterpolator(LINEAR_INTERPOLATOR); expand.start(); loop.setDuration(NOISE_ANIMATION_DURATION); loop.addListener(new AnimatorListener(this) { @@ -205,7 +202,7 @@ public final class RippleAnimationSession { } private void enterSoftware() { - ValueAnimator expand = ValueAnimator.ofFloat(0f, 0.5f); + ValueAnimator expand = ValueAnimator.ofFloat(0f, MID_PROGRESS); expand.addUpdateListener(updatedAnimation -> { notifyUpdate(); mProperties.getShader().setProgress((float) expand.getAnimatedValue()); diff --git a/graphics/java/android/graphics/drawable/RippleShader.java b/graphics/java/android/graphics/drawable/RippleShader.java index 57b322334867..a3aef0327f61 100644 --- a/graphics/java/android/graphics/drawable/RippleShader.java +++ b/graphics/java/android/graphics/drawable/RippleShader.java @@ -41,86 +41,80 @@ final class RippleShader extends RuntimeShader { + "uniform vec4 in_sparkleColor;\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" - + "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.1;\n" - + " float h = l + 0.05;\n" - + " float o = sin(PI * (t + 0.35 * i));\n" - + " s += threshold(n + o, l, h);\n" - + " }\n" - + " return saturate(s) * in_sparkleColor.a;\n" + "// White noise with triangular distribution\n" + + "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" + "}\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" + + "// PDF for Gaussian blur\n" + + "// Specialized for mean=0 for performance\n" + + "const float SQRT_2PI = 2.506628274631000241612355;\n" + + "float gaussian_pdf(float stddev, float x) {\n" + + " float a = x / stddev;\n" + + " return exp(-0.5 * a*a) / (stddev * SQRT_2PI);\n" + "}\n" - + "float softRing(vec2 uv, vec2 xy, float radius, float progress, float blur) {\n" - + " float thickness = 0.05 * radius;\n" - + " float currentRadius = radius * progress;\n" - + " float circle_outer = softCircle(uv, xy, currentRadius + thickness, blur);\n" - + " float circle_inner = softCircle(uv, xy, max(currentRadius - thickness, 0.), " - + " blur);\n" - + " return saturate(circle_outer - circle_inner);\n" + + "\n" + + "// Circular wave with Gaussian blur\n" + + "float softWave(vec2 uv, vec2 center, float maxRadius, float radius, float " + + "blur) {\n" + + " // Distance from the center of the circle (touch point), normalized to" + + " [0, 1] radius)\n" + + " float dNorm = distance(uv, center) / maxRadius;\n" + + " // Position on the Gaussian PDF, clamped to 0 to fill the area of the circle\n" + + " float x = min(0.0, radius - dNorm);\n" + + " // Apply Gaussian blur with dynamic stddev and scale to reduce lightness\n" + + " return gaussian_pdf(0.05 + 0.15 * blur, x) * 0.4;\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" - + "mat2 rotate2d(vec2 rad){\n" - + " return mat2(rad.x, -rad.y, rad.y, rad.x);\n" + + " return saturate((progress - start) / (end - start));\n" + "}\n" - + "float circle_grid(vec2 resolution, vec2 coord, float time, vec2 center,\n" - + " vec2 rotation, float cell_diameter) {\n" - + " coord = rotate2d(rotation) * (center - coord) + center;\n" - + " coord = mod(coord, cell_diameter) / resolution;\n" - + " float normal_radius = cell_diameter / resolution.y * 0.5;\n" - + " float radius = 0.65 * normal_radius;\n" - + " return softCircle(coord, vec2(normal_radius), radius, radius * 50.0);\n" - + "}\n" - + "float turbulence(vec2 uv, float t) {\n" - + " const vec2 scale = vec2(0.8);\n" - + " uv = uv * scale;\n" - + " float g1 = circle_grid(scale, uv, t, in_tCircle1, in_tRotation1, 0.17);\n" - + " float g2 = circle_grid(scale, uv, t, in_tCircle2, in_tRotation2, 0.2);\n" - + " float g3 = circle_grid(scale, uv, t, in_tCircle3, in_tRotation3, 0.275);\n" - + " float v = (g1 * g1 + g2 - g3) * 0.5;\n" - + " return saturate(0.45 + 0.8 * v);\n" - + "}\n"; - private static final String SHADER_MAIN = "vec4 main(vec2 p) {\n" - + " float fadeIn = subProgress(0., 0.13, in_progress);\n" - + " float scaleIn = subProgress(0., 1.0, in_progress);\n" - + " float fadeOutNoise = subProgress(0.4, 0.5, in_progress);\n" - + " float fadeOutRipple = subProgress(0.4, 1., in_progress);\n" - + " vec2 center = mix(in_touch, in_origin, saturate(in_progress * 2.0));\n" - + " float ring = softRing(p, center, in_maxRadius, scaleIn, 1.);\n" - + " float alpha = min(fadeIn, 1. - fadeOutNoise);\n" - + " vec2 uv = p * in_resolutionScale;\n" - + " vec2 densityUv = uv - mod(uv, in_noiseScale);\n" - + " float turbulence = turbulence(uv, in_turbulencePhase);\n" - + " float sparkleAlpha = sparkles(densityUv, in_noisePhase) * ring * alpha " - + "* turbulence;\n" - + " float fade = min(fadeIn, 1. - fadeOutRipple);\n" - + " float waveAlpha = softCircle(p, center, in_maxRadius * scaleIn, 1.) * fade " - + "* in_color.a;\n" - + " vec4 waveColor = vec4(in_color.rgb * waveAlpha, waveAlpha);\n" - + " vec4 sparkleColor = vec4(in_sparkleColor.rgb * in_sparkleColor.a, " - + "in_sparkleColor.a);\n" - + " float mask = in_hasMask == 1. ? sample(in_shader, p).a > 0. ? 1. : 0. : 1.;\n" - + " return mix(waveColor, sparkleColor, sparkleAlpha) * mask;\n" + + "\n" + + "// Animation curve\n" + + "const float PI = 3.141592653589793;\n" + + "float easeOutSine(float x) {\n" + + " return sin((x * PI) / 2.0);\n" + + "}"; + private static final String SHADER_MAIN = "vec4 main(vec2 pos) {\n" + + " // Curve the linear animation progress for responsiveness\n" + + " float progress = easeOutSine(in_progress);\n" + + "\n" + + " // Show highlight immediately instead of fading in for instant feedback\n" + + " // Fade the entire ripple out, including base highlight\n" + + " float fadeOut = subProgress(0.5, 1.0, progress);\n" + + " float fade = 1.0 - fadeOut;\n" + + "\n" + + " // Turbulence phase = time. Unlike progress, it continues moving when the\n" + + " // ripple is held between enter and exit animations, so we can use it to\n" + + " // make a hold animation.\n" + + "\n" + + " // Hold time increases the radius slightly to progress the animation.\n" + + " float timeOffsetMs = 0.0;\n" + + " float waveProgress = progress + timeOffsetMs / 60.0;\n" + + " // Blur radius decreases as the animation progresses, but increases with hold " + + "time\n" + + " // as part of gradually spreading out.\n" + + " float waveBlur = 1.3 - waveProgress + (timeOffsetMs / 15.0);\n" + + " // The wave also fades out with hold time.\n" + + " float waveFade = saturate(1.0 - timeOffsetMs / 20.0);\n" + + " // Calculate wave color, excluding fade\n" + + " float waveAlpha = softWave(pos, in_touch, in_maxRadius / 2.3, waveProgress, " + + "waveBlur);\n" + + "\n" + + " // Dither with triangular white noise. Unfortunately, we can't use blue noise\n" + + " // because RuntimeShader doesn't allow us to add custom textures.\n" + + " float dither = triangleNoise(pos) / 128.0;\n" + + "\n" + + " // 0.5 base highlight + foreground ring\n" + + " float finalAlpha = (0.5 + waveAlpha * waveFade) * fade * in_color.a + dither;\n" + + " vec4 finalColor = vec4(in_color.rgb * finalAlpha, finalAlpha);\n" + + "\n" + + " float mask = in_hasMask == 1.0 ? sample(in_shader, pos).a > 0.0 ? 1.0 : 0.0 : " + + "1.0;\n" + + " return finalColor * mask;\n" + "}"; private static final String SHADER = SHADER_UNIFORMS + SHADER_LIB + SHADER_MAIN; private static final double PI_ROTATE_RIGHT = Math.PI * 0.0078125; |