summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDanny Lin <danny@kdrag0n.dev>2021-10-05 21:00:42 -0700
committeralk3pInjection <webmaster@raspii.tech>2022-05-05 00:39:05 +0800
commitc9b288f98d5811d267d4652af7c6eeb01d6cf6cc (patch)
treefdd478ce8b9dadb95ca772faa2d114c508b289a5
parent2924fb0e9462b8768f90eace669884eb70a68e7a (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.java19
-rw-r--r--graphics/java/android/graphics/drawable/RippleShader.java144
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;