Skip to content

Commit 03736a2

Browse files
MTtankkeodkwingsmtPiinks
authored
Implements the Android native stretch effect as a fragment shader (Impeller-only). (flutter#169293)
> Sorry, Closing PR flutter#169196 and reopening this in a new PR for clarity and a cleaner commit history. Fixes flutter#82906 In the existing Flutter implementation, the Android stretch overscroll effect is achieved using Transform. However, this approach does not evenly stretch the screen as it does in the native Android environment. Therefore, in the Impeller environment, add or modify files to implement the effect using a fragment shader identical to the one used in native Android. > [!NOTE] > - The addition of a separate test file for StretchOverscrollEffect was not included because it would likely duplicate coverage already provided by the changes made in overscroll_stretch_indicator_test.dart within this PR. >> However, since determining whether stretching occurred by referencing global coordinates is currently considered impossible with the new Fragment Shader approach, the code was modified to partially exclude the relevant test logic in the Impeller. >> >> For reference, in the page_view_test.dart test, there was an issue with referencing the child Transform widget, but because the StretchOverscrollEffect widget is defined instead of the Transform widget in the Impeller environment, the code logic was adjusted accordingly. > > - Golden image tests were updated as the visual effect changes. ## Reference Source - https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/libs/hwui/effects/StretchEffect.cpp ## `Old` Skia (Using Transform) https://github.com/user-attachments/assets/22d8ff96-d875-4722-bf6f-f0ad15b9077d ## `New` Impeller (Using fragment shader) https://github.com/user-attachments/assets/73b6ef18-06b2-42ea-9793-c391ec2ce282 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: Tong Mu <[email protected]> Co-authored-by: Kate Lovett <[email protected]>
1 parent 20563f9 commit 03736a2

File tree

10 files changed

+1250
-710
lines changed

10 files changed

+1250
-710
lines changed
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
#version 320 es
2+
// Copyright 2014 The Flutter Authors. All rights reserved.
3+
// Use of this source code is governed by a BSD-style license that can be
4+
// found in the LICENSE file.
5+
6+
// This shader was created based on or with reference to the implementation found at:
7+
// https://cs.android.com/android/platform/superproject/main/+/512046e84bcc51cc241bc6599f83ab345e93ab12:frameworks/base/libs/hwui/effects/StretchEffect.cpp
8+
9+
#include <flutter/runtime_effect.glsl>
10+
11+
uniform vec2 u_size;
12+
uniform sampler2D u_texture;
13+
14+
// Multiplier to apply to scale effect.
15+
uniform float u_max_stretch_intensity;
16+
17+
// Normalized overscroll amount in the horizontal direction.
18+
uniform float u_overscroll_x;
19+
20+
// Normalized overscroll amount in the vertical direction.
21+
uniform float u_overscroll_y;
22+
23+
// u_interpolation_strength is the intensity of the interpolation.
24+
uniform float u_interpolation_strength;
25+
26+
float ease_in(float t, float d) {
27+
return t * d;
28+
}
29+
30+
float compute_overscroll_start(
31+
float in_pos,
32+
float overscroll,
33+
float u_stretch_affected_dist,
34+
float u_inverse_stretch_affected_dist,
35+
float distance_stretched,
36+
float interpolation_strength
37+
) {
38+
float offset_pos = u_stretch_affected_dist - in_pos;
39+
float pos_based_variation = mix(
40+
1.0,
41+
ease_in(offset_pos, u_inverse_stretch_affected_dist),
42+
interpolation_strength
43+
);
44+
float stretch_intensity = overscroll * pos_based_variation;
45+
return distance_stretched - (offset_pos / (1.0 + stretch_intensity));
46+
}
47+
48+
float compute_overscroll_end(
49+
float in_pos,
50+
float overscroll,
51+
float reverse_stretch_dist,
52+
float u_stretch_affected_dist,
53+
float u_inverse_stretch_affected_dist,
54+
float distance_stretched,
55+
float interpolation_strength,
56+
float viewport_dimension
57+
) {
58+
float offset_pos = in_pos - reverse_stretch_dist;
59+
float pos_based_variation = mix(
60+
1.0,
61+
ease_in(offset_pos, u_inverse_stretch_affected_dist),
62+
interpolation_strength
63+
);
64+
float stretch_intensity = (-overscroll) * pos_based_variation;
65+
return viewport_dimension - (distance_stretched - (offset_pos / (1.0 + stretch_intensity)));
66+
}
67+
68+
float compute_streched_effect(
69+
float in_pos,
70+
float overscroll,
71+
float u_stretch_affected_dist,
72+
float u_inverse_stretch_affected_dist,
73+
float distance_stretched,
74+
float distance_diff,
75+
float interpolation_strength,
76+
float viewport_dimension
77+
) {
78+
if (overscroll > 0.0) {
79+
if (in_pos <= u_stretch_affected_dist) {
80+
return compute_overscroll_start(
81+
in_pos, overscroll, u_stretch_affected_dist,
82+
u_inverse_stretch_affected_dist, distance_stretched,
83+
interpolation_strength
84+
);
85+
} else {
86+
return distance_diff + in_pos;
87+
}
88+
} else if (overscroll < 0.0) {
89+
float stretch_affected_dist_calc = viewport_dimension - u_stretch_affected_dist;
90+
if (in_pos >= stretch_affected_dist_calc) {
91+
return compute_overscroll_end(
92+
in_pos,
93+
overscroll,
94+
stretch_affected_dist_calc,
95+
u_stretch_affected_dist,
96+
u_inverse_stretch_affected_dist,
97+
distance_stretched,
98+
interpolation_strength,
99+
viewport_dimension
100+
);
101+
} else {
102+
return -distance_diff + in_pos;
103+
}
104+
} else {
105+
return in_pos;
106+
}
107+
}
108+
109+
out vec4 frag_color;
110+
111+
void main() {
112+
vec2 uv = FlutterFragCoord().xy / u_size;
113+
float in_u_norm = uv.x;
114+
float in_v_norm = uv.y;
115+
116+
float out_u_norm;
117+
float out_v_norm;
118+
119+
bool isVertical = u_overscroll_y != 0;
120+
float overscroll = isVertical ? u_overscroll_y : u_overscroll_x;
121+
122+
float norm_distance_stretched = 1.0 / (1.0 + abs(overscroll));
123+
float norm_dist_diff = norm_distance_stretched - 1.0;
124+
125+
const float norm_viewport = 1.0;
126+
const float norm_stretch_affected_dist = 1.0;
127+
const float norm_inverse_stretch_affected_dist = 1.0;
128+
129+
out_u_norm = isVertical ? in_u_norm : compute_streched_effect(
130+
in_u_norm,
131+
overscroll,
132+
norm_stretch_affected_dist,
133+
norm_inverse_stretch_affected_dist,
134+
norm_distance_stretched,
135+
norm_dist_diff,
136+
u_interpolation_strength,
137+
norm_viewport
138+
);
139+
140+
out_v_norm = isVertical ? compute_streched_effect(
141+
in_v_norm,
142+
overscroll,
143+
norm_stretch_affected_dist,
144+
norm_inverse_stretch_affected_dist,
145+
norm_distance_stretched,
146+
norm_dist_diff,
147+
u_interpolation_strength,
148+
norm_viewport
149+
) : in_v_norm;
150+
151+
uv.x = out_u_norm;
152+
#ifdef IMPELLER_TARGET_OPENGLES
153+
uv.y = 1.0 - out_v_norm;
154+
#else
155+
uv.y = out_v_norm;
156+
#endif
157+
158+
frag_color = texture(u_texture, uv);
159+
}

packages/flutter/lib/src/widgets/overscroll_indicator.dart

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import 'framework.dart';
2323
import 'media_query.dart';
2424
import 'notification_listener.dart';
2525
import 'scroll_notification.dart';
26+
import 'stretch_effect.dart';
2627
import 'ticker_provider.dart';
2728
import 'transitions.dart';
2829

@@ -774,20 +775,6 @@ class _StretchingOverscrollIndicatorState extends State<StretchingOverscrollIndi
774775
return false;
775776
}
776777

777-
AlignmentGeometry _getAlignmentForAxisDirection(_StretchDirection stretchDirection) {
778-
// Accounts for reversed scrollables by checking the AxisDirection
779-
final AxisDirection direction = switch (stretchDirection) {
780-
_StretchDirection.trailing => widget.axisDirection,
781-
_StretchDirection.leading => flipAxisDirection(widget.axisDirection),
782-
};
783-
return switch (direction) {
784-
AxisDirection.up => AlignmentDirectional.topCenter,
785-
AxisDirection.down => AlignmentDirectional.bottomCenter,
786-
AxisDirection.left => Alignment.centerLeft,
787-
AxisDirection.right => Alignment.centerRight,
788-
};
789-
}
790-
791778
@override
792779
void dispose() {
793780
_stretchController.dispose();
@@ -802,30 +789,34 @@ class _StretchingOverscrollIndicatorState extends State<StretchingOverscrollIndi
802789
animation: _stretchController,
803790
builder: (BuildContext context, Widget? child) {
804791
final double stretch = _stretchController.value;
805-
double x = 1.0;
806-
double y = 1.0;
807792
final double mainAxisSize;
808793

809794
switch (widget.axis) {
810795
case Axis.horizontal:
811-
x += stretch;
812796
mainAxisSize = MediaQuery.widthOf(context);
813797
case Axis.vertical:
814-
y += stretch;
815798
mainAxisSize = MediaQuery.heightOf(context);
816799
}
817800

818-
final AlignmentGeometry alignment = _getAlignmentForAxisDirection(
819-
_stretchController.stretchDirection,
820-
);
821-
822801
final double viewportDimension =
823802
_lastOverscrollNotification?.metrics.viewportDimension ?? mainAxisSize;
824-
final Widget transform = Transform(
825-
alignment: alignment,
826-
transform: Matrix4.diagonal3Values(x, y, 1.0),
827-
filterQuality: stretch == 0 ? null : FilterQuality.medium,
828-
child: widget.child,
803+
804+
double overscroll = stretch;
805+
806+
if (_stretchController.stretchDirection == _StretchDirection.trailing) {
807+
overscroll = -overscroll;
808+
}
809+
810+
// Adjust overscroll for reverse scroll directions.
811+
if (widget.axisDirection == AxisDirection.up ||
812+
widget.axisDirection == AxisDirection.left) {
813+
overscroll = -overscroll;
814+
}
815+
816+
final Widget transform = StretchEffect(
817+
stretchStrength: overscroll,
818+
axis: widget.axis,
819+
child: widget.child!,
829820
);
830821

831822
// Only clip if the viewport dimension is smaller than that of the

0 commit comments

Comments
 (0)