Skip to content

Commit

Permalink
ripple: Replace with Fluent Design-inspired ripple animation
Browse files Browse the repository at this point in the history
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
  • Loading branch information
kdrag0n committed Jun 7, 2022
1 parent 7234d99 commit 193b76a
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 86 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@
*/
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;
Expand Down Expand Up @@ -111,7 +111,7 @@ private boolean useRTAnimations(Canvas canvas) {
}

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 -> {
Expand Down Expand Up @@ -165,9 +165,6 @@ public void onAnimationEnd(Animator animation) {
});
exit.setTarget(canvas);
exit.setInterpolator(LINEAR_INTERPOLATOR);

long delay = computeDelay();
exit.setStartDelay(delay);
exit.start();
mCurrentAnimation = exit;
}
Expand All @@ -176,7 +173,7 @@ private void enterHardware(RecordingCanvas canvas) {
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);
Expand All @@ -188,7 +185,7 @@ private void enterHardware(RecordingCanvas canvas) {
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) {
Expand All @@ -205,7 +202,7 @@ public void onAnimationEnd(Animator animation) {
}

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());
Expand Down
144 changes: 69 additions & 75 deletions graphics/java/android/graphics/drawable/RippleShader.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 193b76a

Please sign in to comment.