diff --git a/packages/flutter/lib/src/material/shaders/stretch_effect.frag b/packages/flutter/lib/src/material/shaders/stretch_effect.frag deleted file mode 100644 index 53be5a6971e..00000000000 --- a/packages/flutter/lib/src/material/shaders/stretch_effect.frag +++ /dev/null @@ -1,159 +0,0 @@ -#version 320 es -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// This shader was created based on or with reference to the implementation found at: -// https://cs.android.com/android/platform/superproject/main/+/512046e84bcc51cc241bc6599f83ab345e93ab12:frameworks/base/libs/hwui/effects/StretchEffect.cpp - -#include - -uniform vec2 u_size; -uniform sampler2D u_texture; - -// Multiplier to apply to scale effect. -uniform float u_max_stretch_intensity; - -// Normalized overscroll amount in the horizontal direction. -uniform float u_overscroll_x; - -// Normalized overscroll amount in the vertical direction. -uniform float u_overscroll_y; - -// u_interpolation_strength is the intensity of the interpolation. -uniform float u_interpolation_strength; - -float ease_in(float t, float d) { - return t * d; -} - -float compute_overscroll_start( - float in_pos, - float overscroll, - float u_stretch_affected_dist, - float u_inverse_stretch_affected_dist, - float distance_stretched, - float interpolation_strength -) { - float offset_pos = u_stretch_affected_dist - in_pos; - float pos_based_variation = mix( - 1.0, - ease_in(offset_pos, u_inverse_stretch_affected_dist), - interpolation_strength - ); - float stretch_intensity = overscroll * pos_based_variation; - return distance_stretched - (offset_pos / (1.0 + stretch_intensity)); -} - -float compute_overscroll_end( - float in_pos, - float overscroll, - float reverse_stretch_dist, - float u_stretch_affected_dist, - float u_inverse_stretch_affected_dist, - float distance_stretched, - float interpolation_strength, - float viewport_dimension -) { - float offset_pos = in_pos - reverse_stretch_dist; - float pos_based_variation = mix( - 1.0, - ease_in(offset_pos, u_inverse_stretch_affected_dist), - interpolation_strength - ); - float stretch_intensity = (-overscroll) * pos_based_variation; - return viewport_dimension - (distance_stretched - (offset_pos / (1.0 + stretch_intensity))); -} - -float compute_streched_effect( - float in_pos, - float overscroll, - float u_stretch_affected_dist, - float u_inverse_stretch_affected_dist, - float distance_stretched, - float distance_diff, - float interpolation_strength, - float viewport_dimension -) { - if (overscroll > 0.0) { - if (in_pos <= u_stretch_affected_dist) { - return compute_overscroll_start( - in_pos, overscroll, u_stretch_affected_dist, - u_inverse_stretch_affected_dist, distance_stretched, - interpolation_strength - ); - } else { - return distance_diff + in_pos; - } - } else if (overscroll < 0.0) { - float stretch_affected_dist_calc = viewport_dimension - u_stretch_affected_dist; - if (in_pos >= stretch_affected_dist_calc) { - return compute_overscroll_end( - in_pos, - overscroll, - stretch_affected_dist_calc, - u_stretch_affected_dist, - u_inverse_stretch_affected_dist, - distance_stretched, - interpolation_strength, - viewport_dimension - ); - } else { - return -distance_diff + in_pos; - } - } else { - return in_pos; - } -} - -out vec4 frag_color; - -void main() { - vec2 uv = FlutterFragCoord().xy / u_size; - float in_u_norm = uv.x; - float in_v_norm = uv.y; - - float out_u_norm; - float out_v_norm; - - bool isVertical = u_overscroll_y != 0; - float overscroll = isVertical ? u_overscroll_y : u_overscroll_x; - - float norm_distance_stretched = 1.0 / (1.0 + abs(overscroll)); - float norm_dist_diff = norm_distance_stretched - 1.0; - - const float norm_viewport = 1.0; - const float norm_stretch_affected_dist = 1.0; - const float norm_inverse_stretch_affected_dist = 1.0; - - out_u_norm = isVertical ? in_u_norm : compute_streched_effect( - in_u_norm, - overscroll, - norm_stretch_affected_dist, - norm_inverse_stretch_affected_dist, - norm_distance_stretched, - norm_dist_diff, - u_interpolation_strength, - norm_viewport - ); - - out_v_norm = isVertical ? compute_streched_effect( - in_v_norm, - overscroll, - norm_stretch_affected_dist, - norm_inverse_stretch_affected_dist, - norm_distance_stretched, - norm_dist_diff, - u_interpolation_strength, - norm_viewport - ) : in_v_norm; - - uv.x = out_u_norm; - #ifdef IMPELLER_TARGET_OPENGLES - uv.y = 1.0 - out_v_norm; - #else - uv.y = out_v_norm; - #endif - - frag_color = texture(u_texture, uv); -} diff --git a/packages/flutter/lib/src/widgets/overscroll_indicator.dart b/packages/flutter/lib/src/widgets/overscroll_indicator.dart index 952214bfd4e..7e1751a5cc2 100644 --- a/packages/flutter/lib/src/widgets/overscroll_indicator.dart +++ b/packages/flutter/lib/src/widgets/overscroll_indicator.dart @@ -23,7 +23,6 @@ import 'framework.dart'; import 'media_query.dart'; import 'notification_listener.dart'; import 'scroll_notification.dart'; -import 'stretch_effect.dart'; import 'ticker_provider.dart'; import 'transitions.dart'; @@ -775,6 +774,20 @@ class _StretchingOverscrollIndicatorState extends State widget.axisDirection, + _StretchDirection.leading => flipAxisDirection(widget.axisDirection), + }; + return switch (direction) { + AxisDirection.up => AlignmentDirectional.topCenter, + AxisDirection.down => AlignmentDirectional.bottomCenter, + AxisDirection.left => Alignment.centerLeft, + AxisDirection.right => Alignment.centerRight, + }; + } + @override void dispose() { _stretchController.dispose(); @@ -789,34 +802,30 @@ class _StretchingOverscrollIndicatorState extends State= -1.0 && stretchStrength <= 1.0, - 'stretchStrength must be between -1.0 and 1.0', - ); - - /// The overscroll strength applied for the stretching effect. - /// - /// The value should be between -1.0 and 1.0 inclusive. - /// - /// For the horizontal axis: - /// - Positive values apply a pull/stretch from left to right, - /// where 1.0 represents the maximum stretch to the right. - /// - Negative values apply a pull/stretch from right to left, - /// where -1.0 represents the maximum stretch to the left. - /// - /// For the vertical axis: - /// - Positive values apply a pull/stretch from top to bottom, - /// where 1.0 represents the maximum stretch downward. - /// - Negative values apply a pull/stretch from bottom to top, - /// where -1.0 represents the maximum stretch upward. - /// - /// {@tool snippet} - /// This example shows how to set the horizontal stretch strength to pull right. - /// - /// ```dart - /// const StretchEffect( - /// stretchStrength: 0.5, - /// axis: Axis.horizontal, - /// child: Text('Hello, World!'), - /// ); - /// ``` - /// {@end-tool} - final double stretchStrength; - - /// The axis along which the stretching overscroll effect is applied. - /// - /// Determines the direction of the stretch, either horizontal or vertical. - final Axis axis; - - /// The child widget that the stretching overscroll effect applies to. - final Widget child; - - AlignmentGeometry _getAlignment(TextDirection direction) { - final bool isForward = stretchStrength > 0; - - if (axis == Axis.vertical) { - return isForward ? AlignmentDirectional.topCenter : AlignmentDirectional.bottomCenter; - } - - // RTL horizontal. - if (direction == TextDirection.rtl) { - return isForward ? AlignmentDirectional.centerEnd : AlignmentDirectional.centerStart; - } else { - return isForward ? AlignmentDirectional.centerStart : AlignmentDirectional.centerEnd; - } - } - - @override - Widget build(BuildContext context) { - if (ui.ImageFilter.isShaderFilterSupported) { - return _StretchOverscrollEffect(stretchStrength: stretchStrength, axis: axis, child: child); - } - - final TextDirection textDirection = Directionality.of(context); - double x = 1.0; - double y = 1.0; - - switch (axis) { - case Axis.horizontal: - x += stretchStrength.abs(); - case Axis.vertical: - y += stretchStrength.abs(); - } - - return Transform( - alignment: _getAlignment(textDirection), - transform: Matrix4.diagonal3Values(x, y, 1.0), - filterQuality: stretchStrength == 0 ? null : FilterQuality.medium, - child: child, - ); - } -} - -/// A widget that replicates the native Android stretch overscroll effect. -/// -/// This widget is used in the [StretchEffect] widget and creates -/// a stretch visual feedback when the user overscrolls at the edges. -/// -/// Only supported when using the Impeller rendering engine. -class _StretchOverscrollEffect extends StatefulWidget { - /// Creates a [_StretchOverscrollEffect] widget that applies a stretch - /// effect when the user overscrolls horizontally or vertically. - const _StretchOverscrollEffect({ - this.stretchStrength = 0.0, - required this.axis, - required this.child, - }) : assert( - stretchStrength >= -1.0 && stretchStrength <= 1.0, - 'stretchStrength must be between -1.0 and 1.0', - ); - - /// The overscroll strength applied for the stretching effect. - /// - /// The value should be between -1.0 and 1.0 inclusive. - /// For horizontal axis, Positive values apply a pull from - /// left to right, while negative values pull from right to left. - final double stretchStrength; - - /// The axis along which the stretching overscroll effect is applied. - /// - /// Determines the direction of the stretch, either horizontal or vertical. - final Axis axis; - - /// The child widget that the stretching overscroll effect applies to. - final Widget child; - - @override - State<_StretchOverscrollEffect> createState() => _StretchOverscrollEffectState(); -} - -class _StretchOverscrollEffectState extends State<_StretchOverscrollEffect> { - ui.FragmentShader? _fragmentShader; - - /// The maximum scale multiplier applied during a stretch effect. - static const double maxStretchIntensity = 1.0; - - /// The strength of the interpolation used for smoothing the effect. - static const double interpolationStrength = 0.7; - - /// A no-op [ui.ImageFilter] that uses the identity matrix. - static final ui.ImageFilter _emptyFilter = ui.ImageFilter.matrix(Matrix4.identity().storage); - - @override - void dispose() { - _fragmentShader?.dispose(); - super.dispose(); - } - - @override - void initState() { - super.initState(); - _StretchEffectShader.initializeShader(); - } - - @override - Widget build(BuildContext context) { - final bool isShaderNeeded = widget.stretchStrength.abs() > precisionErrorTolerance; - - final ui.ImageFilter imageFilter; - - if (_StretchEffectShader._initialized) { - _fragmentShader?.dispose(); - _fragmentShader = _StretchEffectShader._program!.fragmentShader(); - _fragmentShader!.setFloat(2, maxStretchIntensity); - if (widget.axis == Axis.vertical) { - _fragmentShader!.setFloat(3, 0.0); - _fragmentShader!.setFloat(4, widget.stretchStrength); - } else { - _fragmentShader!.setFloat(3, widget.stretchStrength); - _fragmentShader!.setFloat(4, 0.0); - } - _fragmentShader!.setFloat(5, interpolationStrength); - - imageFilter = ui.ImageFilter.shader(_fragmentShader!); - } else { - _fragmentShader?.dispose(); - _fragmentShader = null; - - imageFilter = _emptyFilter; - } - - return ImageFiltered( - imageFilter: imageFilter, - enabled: isShaderNeeded, - // A nearly-transparent pixels is used to ensure the shader gets applied, - // even when the child is visually transparent or has no paint operations. - child: CustomPaint( - painter: isShaderNeeded ? _StretchEffectPainter() : null, - child: widget.child, - ), - ); - } -} - -/// A [CustomPainter] that draws nearly transparent pixels at the four corners. -/// -/// This ensures the fragment shader covers the entire canvas by forcing -/// painting operations on all edges, preventing shader optimization skips. -class _StretchEffectPainter extends CustomPainter { - @override - void paint(Canvas canvas, Size size) { - final Paint paint = Paint() - ..color = const Color.fromARGB(1, 0, 0, 0) - ..style = PaintingStyle.fill; - - canvas.drawPoints(ui.PointMode.points, [ - Offset.zero, - Offset(size.width - 1, 0), - Offset(0, size.height - 1), - Offset(size.width - 1, size.height - 1), - ], paint); - } - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; -} - -class _StretchEffectShader { - static bool _initCalled = false; - static bool _initialized = false; - static ui.FragmentProgram? _program; - - static void initializeShader() { - if (!_initCalled) { - ui.FragmentProgram.fromAsset('shaders/stretch_effect.frag').then(( - ui.FragmentProgram program, - ) { - _program = program; - _initialized = true; - }); - _initCalled = true; - } - } -} diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index 81486a12f6c..bcb359732a1 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -154,7 +154,6 @@ export 'src/widgets/spacer.dart'; export 'src/widgets/spell_check.dart'; export 'src/widgets/standard_component_type.dart'; export 'src/widgets/status_transitions.dart'; -export 'src/widgets/stretch_effect.dart'; export 'src/widgets/system_context_menu.dart'; export 'src/widgets/table.dart'; export 'src/widgets/tap_region.dart'; diff --git a/packages/flutter/test/widgets/overscroll_stretch_indicator_test.dart b/packages/flutter/test/widgets/overscroll_stretch_indicator_test.dart index 5a4e6854440..eec79a4e941 100644 --- a/packages/flutter/test/widgets/overscroll_stretch_indicator_test.dart +++ b/packages/flutter/test/widgets/overscroll_stretch_indicator_test.dart @@ -7,18 +7,11 @@ @Tags(['reduced-test-set']) library; -import 'dart:ui' as ui; - import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { - // `StretchingOverscrollIndicator` uses a different algorithm when - // shader is available, therefore the tests must be different depending - // on shader support. - final bool shaderSupported = ui.ImageFilter.isShaderFilterSupported; - Widget buildTest( GlobalKey box1Key, GlobalKey box2Key, @@ -153,401 +146,373 @@ void main() { expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0)); }); - testWidgets( - 'Stretch overscroll vertically', - (WidgetTester tester) async { - final GlobalKey box1Key = GlobalKey(); - final GlobalKey box2Key = GlobalKey(); - final GlobalKey box3Key = GlobalKey(); - final ScrollController controller = ScrollController(); - addTearDown(controller.dispose); + testWidgets('Stretch overscroll vertically', (WidgetTester tester) async { + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); - await tester.pumpWidget(buildTest(box1Key, box2Key, box3Key, controller)); + await tester.pumpWidget(buildTest(box1Key, box2Key, box3Key, controller)); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); - final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); - final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); + final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); + final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 500.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.vertical.start.png'), - ); + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 500.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.vertical.start.png'), + ); - TestGesture gesture = await tester.startGesture( - tester.getCenter(find.byType(CustomScrollView)), - ); - // Overscroll the start - await gesture.moveBy(const Offset(0.0, 200.0)); - await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero).dy, greaterThan(255.0)); - expect(box3.localToGlobal(Offset.zero).dy, greaterThan(510.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.vertical.start.stretched.png'), - ); + TestGesture gesture = await tester.startGesture( + tester.getCenter(find.byType(CustomScrollView)), + ); + // Overscroll the start + await gesture.moveBy(const Offset(0.0, 200.0)); + await tester.pumpAndSettle(); + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero).dy, greaterThan(255.0)); + expect(box3.localToGlobal(Offset.zero).dy, greaterThan(510.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.vertical.start.stretched.png'), + ); - await gesture.up(); - await tester.pumpAndSettle(); + await gesture.up(); + await tester.pumpAndSettle(); - // Stretch released back to the start - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 500.0)); + // Stretch released back to the start + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 500.0)); - // Jump to end of the list - controller.jumpTo(controller.position.maxScrollExtent); - await tester.pumpAndSettle(); - expect(controller.offset, 150.0); - expect(box1.localToGlobal(Offset.zero).dy, -150.0); - expect(box2.localToGlobal(Offset.zero).dy, 100.0); - expect(box3.localToGlobal(Offset.zero).dy, 350.0); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.vertical.end.png'), - ); + // Jump to end of the list + controller.jumpTo(controller.position.maxScrollExtent); + await tester.pumpAndSettle(); + expect(controller.offset, 150.0); + expect(box1.localToGlobal(Offset.zero).dy, -150.0); + expect(box2.localToGlobal(Offset.zero).dy, 100.0); + expect(box3.localToGlobal(Offset.zero).dy, 350.0); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.vertical.end.png'), + ); - gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); - // Overscroll the end - await gesture.moveBy(const Offset(0.0, -200.0)); - await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero).dy, lessThan(-165)); - expect(box2.localToGlobal(Offset.zero).dy, lessThan(90.0)); - expect(box3.localToGlobal(Offset.zero).dy, lessThan(350.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.vertical.end.stretched.png'), - ); + gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); + // Overscroll the end + await gesture.moveBy(const Offset(0.0, -200.0)); + await tester.pumpAndSettle(); + expect(box1.localToGlobal(Offset.zero).dy, lessThan(-165)); + expect(box2.localToGlobal(Offset.zero).dy, lessThan(90.0)); + expect(box3.localToGlobal(Offset.zero).dy, lessThan(350.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.vertical.end.stretched.png'), + ); - await gesture.up(); - await tester.pumpAndSettle(); + await gesture.up(); + await tester.pumpAndSettle(); - // Stretch released back - expect(box1.localToGlobal(Offset.zero).dy, -150.0); - expect(box2.localToGlobal(Offset.zero).dy, 100.0); - expect(box3.localToGlobal(Offset.zero).dy, 350.0); - }, - // Skips this test when fragment shaders are used. - skip: shaderSupported, - ); + // Stretch released back + expect(box1.localToGlobal(Offset.zero).dy, -150.0); + expect(box2.localToGlobal(Offset.zero).dy, 100.0); + expect(box3.localToGlobal(Offset.zero).dy, 350.0); + }); - testWidgets( - 'Stretch overscroll works in reverse - vertical', - (WidgetTester tester) async { - final GlobalKey box1Key = GlobalKey(); - final GlobalKey box2Key = GlobalKey(); - final GlobalKey box3Key = GlobalKey(); - final ScrollController controller = ScrollController(); - addTearDown(controller.dispose); + testWidgets('Stretch overscroll works in reverse - vertical', (WidgetTester tester) async { + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); - await tester.pumpWidget(buildTest(box1Key, box2Key, box3Key, controller, reverse: true)); + await tester.pumpWidget(buildTest(box1Key, box2Key, box3Key, controller, reverse: true)); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); - final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); - final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); + final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); + final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero), const Offset(0.0, 350.0)); - expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 100.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(0.0, -150.0)); + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero), const Offset(0.0, 350.0)); + expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 100.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(0.0, -150.0)); - final TestGesture gesture = await tester.startGesture( - tester.getCenter(find.byType(CustomScrollView)), - ); - // Overscroll - await gesture.moveBy(const Offset(0.0, -200.0)); - await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero).dy, lessThan(350.0)); - expect(box2.localToGlobal(Offset.zero).dy, lessThan(100.0)); - expect(box3.localToGlobal(Offset.zero).dy, lessThan(-150.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.vertical.reverse.png'), - ); - }, - // Skips this test when fragment shaders are used. - skip: shaderSupported, - ); + final TestGesture gesture = await tester.startGesture( + tester.getCenter(find.byType(CustomScrollView)), + ); + // Overscroll + await gesture.moveBy(const Offset(0.0, -200.0)); + await tester.pumpAndSettle(); + expect(box1.localToGlobal(Offset.zero).dy, lessThan(350.0)); + expect(box2.localToGlobal(Offset.zero).dy, lessThan(100.0)); + expect(box3.localToGlobal(Offset.zero).dy, lessThan(-150.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.vertical.reverse.png'), + ); + }); - testWidgets( - 'Stretch overscroll works in reverse - horizontal', - (WidgetTester tester) async { - final GlobalKey box1Key = GlobalKey(); - final GlobalKey box2Key = GlobalKey(); - final GlobalKey box3Key = GlobalKey(); - final ScrollController controller = ScrollController(); - addTearDown(controller.dispose); + testWidgets('Stretch overscroll works in reverse - horizontal', (WidgetTester tester) async { + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); - await tester.pumpWidget( - buildTest(box1Key, box2Key, box3Key, controller, axis: Axis.horizontal, reverse: true), - ); + await tester.pumpWidget( + buildTest(box1Key, box2Key, box3Key, controller, axis: Axis.horizontal, reverse: true), + ); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); - final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); - final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); + final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); + final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero), const Offset(500.0, 0.0)); - expect(box2.localToGlobal(Offset.zero), const Offset(200.0, 0.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(-100.0, 0.0)); + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero), const Offset(500.0, 0.0)); + expect(box2.localToGlobal(Offset.zero), const Offset(200.0, 0.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(-100.0, 0.0)); - final TestGesture gesture = await tester.startGesture( - tester.getCenter(find.byType(CustomScrollView)), - ); - // Overscroll - await gesture.moveBy(const Offset(-200.0, 0.0)); - await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero).dx, lessThan(500.0)); - expect(box2.localToGlobal(Offset.zero).dx, lessThan(200.0)); - expect(box3.localToGlobal(Offset.zero).dx, lessThan(-100.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.horizontal.reverse.png'), - ); - }, - // Skips this test when fragment shaders are used. - skip: shaderSupported, - ); + final TestGesture gesture = await tester.startGesture( + tester.getCenter(find.byType(CustomScrollView)), + ); + // Overscroll + await gesture.moveBy(const Offset(-200.0, 0.0)); + await tester.pumpAndSettle(); + expect(box1.localToGlobal(Offset.zero).dx, lessThan(500.0)); + expect(box2.localToGlobal(Offset.zero).dx, lessThan(200.0)); + expect(box3.localToGlobal(Offset.zero).dx, lessThan(-100.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.horizontal.reverse.png'), + ); + }); - testWidgets( - 'Stretch overscroll works in reverse - horizontal - RTL', - (WidgetTester tester) async { - final GlobalKey box1Key = GlobalKey(); - final GlobalKey box2Key = GlobalKey(); - final GlobalKey box3Key = GlobalKey(); - final ScrollController controller = ScrollController(); - addTearDown(controller.dispose); + testWidgets('Stretch overscroll works in reverse - horizontal - RTL', ( + WidgetTester tester, + ) async { + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); - await tester.pumpWidget( - buildTest( - box1Key, - box2Key, - box3Key, - controller, - axis: Axis.horizontal, - reverse: true, - textDirection: TextDirection.rtl, - ), - ); + await tester.pumpWidget( + buildTest( + box1Key, + box2Key, + box3Key, + controller, + axis: Axis.horizontal, + reverse: true, + textDirection: TextDirection.rtl, + ), + ); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); - final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); - final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); + final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); + final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(300.0, 0.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(600.0, 0.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.horizontal.reverse.rtl.start.png'), - ); + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(300.0, 0.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(600.0, 0.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.horizontal.reverse.rtl.start.png'), + ); - TestGesture gesture = await tester.startGesture( - tester.getCenter(find.byType(CustomScrollView)), - ); - // Overscroll the start - await gesture.moveBy(const Offset(200.0, 0.0)); - await tester.pumpAndSettle(); + TestGesture gesture = await tester.startGesture( + tester.getCenter(find.byType(CustomScrollView)), + ); + // Overscroll the start + await gesture.moveBy(const Offset(200.0, 0.0)); + await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero).dx, greaterThan(305.0)); - expect(box3.localToGlobal(Offset.zero).dx, greaterThan(610.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.horizontal.reverse.rtl.start.stretched.png'), - ); + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero).dx, greaterThan(305.0)); + expect(box3.localToGlobal(Offset.zero).dx, greaterThan(610.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.horizontal.reverse.rtl.start.stretched.png'), + ); - await gesture.up(); - await tester.pumpAndSettle(); + await gesture.up(); + await tester.pumpAndSettle(); - // Stretch released back to the start - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(300.0, 0.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(600.0, 0.0)); + // Stretch released back to the start + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(300.0, 0.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(600.0, 0.0)); - // Jump to end of the list - controller.jumpTo(controller.position.maxScrollExtent); - await tester.pumpAndSettle(); - expect(controller.offset, 100.0); - expect(box1.localToGlobal(Offset.zero).dx, -100.0); - expect(box2.localToGlobal(Offset.zero).dx, 200.0); - expect(box3.localToGlobal(Offset.zero).dx, 500.0); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.horizontal.reverse.rtl.end.png'), - ); + // Jump to end of the list + controller.jumpTo(controller.position.maxScrollExtent); + await tester.pumpAndSettle(); + expect(controller.offset, 100.0); + expect(box1.localToGlobal(Offset.zero).dx, -100.0); + expect(box2.localToGlobal(Offset.zero).dx, 200.0); + expect(box3.localToGlobal(Offset.zero).dx, 500.0); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.horizontal.reverse.rtl.end.png'), + ); - gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); - // Overscroll the end - await gesture.moveBy(const Offset(-200.0, 0.0)); - await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero).dx, lessThan(-116.0)); - expect(box2.localToGlobal(Offset.zero).dx, lessThan(190.0)); - expect(box3.localToGlobal(Offset.zero).dx, lessThan(500.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.horizontal.reverse.rtl.end.stretched.png'), - ); + gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); + // Overscroll the end + await gesture.moveBy(const Offset(-200.0, 0.0)); + await tester.pumpAndSettle(); + expect(box1.localToGlobal(Offset.zero).dx, lessThan(-116.0)); + expect(box2.localToGlobal(Offset.zero).dx, lessThan(190.0)); + expect(box3.localToGlobal(Offset.zero).dx, lessThan(500.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.horizontal.reverse.rtl.end.stretched.png'), + ); - await gesture.up(); - await tester.pumpAndSettle(); + await gesture.up(); + await tester.pumpAndSettle(); - // Stretch released back - expect(box1.localToGlobal(Offset.zero).dx, -100.0); - expect(box2.localToGlobal(Offset.zero).dx, 200.0); - expect(box3.localToGlobal(Offset.zero).dx, 500.0); - }, - // Skips this test when fragment shaders are used. - skip: shaderSupported, - ); + // Stretch released back + expect(box1.localToGlobal(Offset.zero).dx, -100.0); + expect(box2.localToGlobal(Offset.zero).dx, 200.0); + expect(box3.localToGlobal(Offset.zero).dx, 500.0); + }); - testWidgets( - 'Stretch overscroll horizontally', - (WidgetTester tester) async { - final GlobalKey box1Key = GlobalKey(); - final GlobalKey box2Key = GlobalKey(); - final GlobalKey box3Key = GlobalKey(); - final ScrollController controller = ScrollController(); - addTearDown(controller.dispose); + testWidgets('Stretch overscroll horizontally', (WidgetTester tester) async { + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); - await tester.pumpWidget( - buildTest(box1Key, box2Key, box3Key, controller, axis: Axis.horizontal), - ); + await tester.pumpWidget( + buildTest(box1Key, box2Key, box3Key, controller, axis: Axis.horizontal), + ); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); - final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); - final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); + final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); + final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(300.0, 0.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(600.0, 0.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.horizontal.start.png'), - ); + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(300.0, 0.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(600.0, 0.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.horizontal.start.png'), + ); - TestGesture gesture = await tester.startGesture( - tester.getCenter(find.byType(CustomScrollView)), - ); - // Overscroll the start - await gesture.moveBy(const Offset(200.0, 0.0)); - await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero).dx, greaterThan(305.0)); - expect(box3.localToGlobal(Offset.zero).dx, greaterThan(610.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.horizontal.start.stretched.png'), - ); + TestGesture gesture = await tester.startGesture( + tester.getCenter(find.byType(CustomScrollView)), + ); + // Overscroll the start + await gesture.moveBy(const Offset(200.0, 0.0)); + await tester.pumpAndSettle(); + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero).dx, greaterThan(305.0)); + expect(box3.localToGlobal(Offset.zero).dx, greaterThan(610.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.horizontal.start.stretched.png'), + ); - await gesture.up(); - await tester.pumpAndSettle(); + await gesture.up(); + await tester.pumpAndSettle(); - // Stretch released back to the start - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(300.0, 0.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(600.0, 0.0)); + // Stretch released back to the start + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(300.0, 0.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(600.0, 0.0)); - // Jump to end of the list - controller.jumpTo(controller.position.maxScrollExtent); - await tester.pumpAndSettle(); - expect(controller.offset, 100.0); - expect(box1.localToGlobal(Offset.zero).dx, -100.0); - expect(box2.localToGlobal(Offset.zero).dx, 200.0); - expect(box3.localToGlobal(Offset.zero).dx, 500.0); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.horizontal.end.png'), - ); + // Jump to end of the list + controller.jumpTo(controller.position.maxScrollExtent); + await tester.pumpAndSettle(); + expect(controller.offset, 100.0); + expect(box1.localToGlobal(Offset.zero).dx, -100.0); + expect(box2.localToGlobal(Offset.zero).dx, 200.0); + expect(box3.localToGlobal(Offset.zero).dx, 500.0); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.horizontal.end.png'), + ); - gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); - // Overscroll the end - await gesture.moveBy(const Offset(-200.0, 0.0)); - await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero).dx, lessThan(-116.0)); - expect(box2.localToGlobal(Offset.zero).dx, lessThan(190.0)); - expect(box3.localToGlobal(Offset.zero).dx, lessThan(500.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.horizontal.end.stretched.png'), - ); + gesture = await tester.startGesture(tester.getCenter(find.byType(CustomScrollView))); + // Overscroll the end + await gesture.moveBy(const Offset(-200.0, 0.0)); + await tester.pumpAndSettle(); + expect(box1.localToGlobal(Offset.zero).dx, lessThan(-116.0)); + expect(box2.localToGlobal(Offset.zero).dx, lessThan(190.0)); + expect(box3.localToGlobal(Offset.zero).dx, lessThan(500.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.horizontal.end.stretched.png'), + ); - await gesture.up(); - await tester.pumpAndSettle(); + await gesture.up(); + await tester.pumpAndSettle(); - // Stretch released back - expect(box1.localToGlobal(Offset.zero).dx, -100.0); - expect(box2.localToGlobal(Offset.zero).dx, 200.0); - expect(box3.localToGlobal(Offset.zero).dx, 500.0); - }, - // Skips this test when fragment shaders are used. - skip: shaderSupported, - ); + // Stretch released back + expect(box1.localToGlobal(Offset.zero).dx, -100.0); + expect(box2.localToGlobal(Offset.zero).dx, 200.0); + expect(box3.localToGlobal(Offset.zero).dx, 500.0); + }); - testWidgets( - 'Stretch overscroll horizontally RTL', - (WidgetTester tester) async { - final GlobalKey box1Key = GlobalKey(); - final GlobalKey box2Key = GlobalKey(); - final GlobalKey box3Key = GlobalKey(); - final ScrollController controller = ScrollController(); - addTearDown(controller.dispose); + testWidgets('Stretch overscroll horizontally RTL', (WidgetTester tester) async { + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); - await tester.pumpWidget( - buildTest( - box1Key, - box2Key, - box3Key, - controller, - axis: Axis.horizontal, - textDirection: TextDirection.rtl, - ), - ); + await tester.pumpWidget( + buildTest( + box1Key, + box2Key, + box3Key, + controller, + axis: Axis.horizontal, + textDirection: TextDirection.rtl, + ), + ); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); - final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); - final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); + final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); + final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero), const Offset(500.0, 0.0)); - expect(box2.localToGlobal(Offset.zero), const Offset(200.0, 0.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(-100.0, 0.0)); + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero), const Offset(500.0, 0.0)); + expect(box2.localToGlobal(Offset.zero), const Offset(200.0, 0.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(-100.0, 0.0)); - final TestGesture gesture = await tester.startGesture( - tester.getCenter(find.byType(CustomScrollView)), - ); - // Overscroll - await gesture.moveBy(const Offset(-200.0, 0.0)); - await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero).dx, lessThan(500.0)); - expect(box2.localToGlobal(Offset.zero).dx, lessThan(200.0)); - expect(box3.localToGlobal(Offset.zero).dx, lessThan(-100.0)); - await expectLater( - find.byType(CustomScrollView), - matchesGoldenFile('overscroll_stretch.horizontal.rtl.png'), - ); - }, - // Skips this test when fragment shaders are used. - skip: shaderSupported, - ); + final TestGesture gesture = await tester.startGesture( + tester.getCenter(find.byType(CustomScrollView)), + ); + // Overscroll + await gesture.moveBy(const Offset(-200.0, 0.0)); + await tester.pumpAndSettle(); + expect(box1.localToGlobal(Offset.zero).dx, lessThan(500.0)); + expect(box2.localToGlobal(Offset.zero).dx, lessThan(200.0)); + expect(box3.localToGlobal(Offset.zero).dx, lessThan(-100.0)); + await expectLater( + find.byType(CustomScrollView), + matchesGoldenFile('overscroll_stretch.horizontal.rtl.png'), + ); + }); testWidgets('Disallow stretching overscroll', (WidgetTester tester) async { final GlobalKey box1Key = GlobalKey(); @@ -768,403 +733,373 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets( - 'Stretch limit', - (WidgetTester tester) async { - // Regression test for https://github.com/flutter/flutter/issues/99264 - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: MediaQuery( - data: const MediaQueryData(), - child: ScrollConfiguration( - behavior: const ScrollBehavior().copyWith(overscroll: false), - child: StretchingOverscrollIndicator( - axisDirection: AxisDirection.down, - child: SizedBox( - height: 300, - child: ListView.builder( - itemCount: 20, - itemBuilder: (BuildContext context, int index) { - return Padding( - padding: const EdgeInsets.all(10.0), - child: Text('Index $index'), - ); - }, - ), + testWidgets('Stretch limit', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/99264 + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: const MediaQueryData(), + child: ScrollConfiguration( + behavior: const ScrollBehavior().copyWith(overscroll: false), + child: StretchingOverscrollIndicator( + axisDirection: AxisDirection.down, + child: SizedBox( + height: 300, + child: ListView.builder( + itemCount: 20, + itemBuilder: (BuildContext context, int index) { + return Padding( + padding: const EdgeInsets.all(10.0), + child: Text('Index $index'), + ); + }, ), ), ), ), ), - ); - const double maxStretchLocation = 52.63178407049861; + ), + ); + const double maxStretchLocation = 52.63178407049861; - expect(find.text('Index 1'), findsOneWidget); - expect(tester.getCenter(find.text('Index 1')).dy, 51.0); + expect(find.text('Index 1'), findsOneWidget); + expect(tester.getCenter(find.text('Index 1')).dy, 51.0); - TestGesture pointer = await tester.startGesture(tester.getCenter(find.text('Index 1'))); - // Overscroll beyond the limit (the viewport is 600.0). - await pointer.moveBy(const Offset(0.0, 610.0)); - await tester.pumpAndSettle(); - expect(find.text('Index 1'), findsOneWidget); - expect(tester.getCenter(find.text('Index 1')).dy, maxStretchLocation); + TestGesture pointer = await tester.startGesture(tester.getCenter(find.text('Index 1'))); + // Overscroll beyond the limit (the viewport is 600.0). + await pointer.moveBy(const Offset(0.0, 610.0)); + await tester.pumpAndSettle(); + expect(find.text('Index 1'), findsOneWidget); + expect(tester.getCenter(find.text('Index 1')).dy, maxStretchLocation); - pointer = await tester.startGesture(tester.getCenter(find.text('Index 1'))); - // Overscroll way way beyond the limit - await pointer.moveBy(const Offset(0.0, 1000.0)); - await tester.pumpAndSettle(); - expect(find.text('Index 1'), findsOneWidget); - expect(tester.getCenter(find.text('Index 1')).dy, maxStretchLocation); + pointer = await tester.startGesture(tester.getCenter(find.text('Index 1'))); + // Overscroll way way beyond the limit + await pointer.moveBy(const Offset(0.0, 1000.0)); + await tester.pumpAndSettle(); + expect(find.text('Index 1'), findsOneWidget); + expect(tester.getCenter(find.text('Index 1')).dy, maxStretchLocation); - await pointer.up(); - await tester.pumpAndSettle(); - }, - // Skips this test when fragment shaders are used. - skip: shaderSupported, - ); + await pointer.up(); + await tester.pumpAndSettle(); + }); - testWidgets( - 'Multiple pointers will not exceed stretch limit', - (WidgetTester tester) async { - // Regression test for https://github.com/flutter/flutter/issues/99264 - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: MediaQuery( - data: const MediaQueryData(), - child: ScrollConfiguration( - behavior: const ScrollBehavior().copyWith(overscroll: false), - child: StretchingOverscrollIndicator( - axisDirection: AxisDirection.down, - child: SizedBox( - height: 300, - child: ListView.builder( - itemCount: 20, - itemBuilder: (BuildContext context, int index) { - return Padding( - padding: const EdgeInsets.all(10.0), - child: Text('Index $index'), - ); - }, - ), + testWidgets('Multiple pointers will not exceed stretch limit', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/99264 + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: const MediaQueryData(), + child: ScrollConfiguration( + behavior: const ScrollBehavior().copyWith(overscroll: false), + child: StretchingOverscrollIndicator( + axisDirection: AxisDirection.down, + child: SizedBox( + height: 300, + child: ListView.builder( + itemCount: 20, + itemBuilder: (BuildContext context, int index) { + return Padding( + padding: const EdgeInsets.all(10.0), + child: Text('Index $index'), + ); + }, ), ), ), ), ), - ); - expect(find.text('Index 1'), findsOneWidget); - expect(tester.getCenter(find.text('Index 1')).dy, 51.0); + ), + ); + expect(find.text('Index 1'), findsOneWidget); + expect(tester.getCenter(find.text('Index 1')).dy, 51.0); - final TestGesture pointer1 = await tester.startGesture( - tester.getCenter(find.text('Index 1')), - ); - // Overscroll the start. - await pointer1.moveBy(const Offset(0.0, 210.0)); - await tester.pumpAndSettle(); - expect(find.text('Index 1'), findsOneWidget); - double lastStretchedLocation = tester.getCenter(find.text('Index 1')).dy; - expect(lastStretchedLocation, greaterThan(51.0)); + final TestGesture pointer1 = await tester.startGesture(tester.getCenter(find.text('Index 1'))); + // Overscroll the start. + await pointer1.moveBy(const Offset(0.0, 210.0)); + await tester.pumpAndSettle(); + expect(find.text('Index 1'), findsOneWidget); + double lastStretchedLocation = tester.getCenter(find.text('Index 1')).dy; + expect(lastStretchedLocation, greaterThan(51.0)); - final TestGesture pointer2 = await tester.startGesture( - tester.getCenter(find.text('Index 1')), - ); - // Add overscroll from an additional pointer - await pointer2.moveBy(const Offset(0.0, 210.0)); - await tester.pumpAndSettle(); - expect(find.text('Index 1'), findsOneWidget); - expect(tester.getCenter(find.text('Index 1')).dy, greaterThan(lastStretchedLocation)); - lastStretchedLocation = tester.getCenter(find.text('Index 1')).dy; + final TestGesture pointer2 = await tester.startGesture(tester.getCenter(find.text('Index 1'))); + // Add overscroll from an additional pointer + await pointer2.moveBy(const Offset(0.0, 210.0)); + await tester.pumpAndSettle(); + expect(find.text('Index 1'), findsOneWidget); + expect(tester.getCenter(find.text('Index 1')).dy, greaterThan(lastStretchedLocation)); + lastStretchedLocation = tester.getCenter(find.text('Index 1')).dy; - final TestGesture pointer3 = await tester.startGesture( - tester.getCenter(find.text('Index 1')), - ); - // Add overscroll from an additional pointer, exceeding the max stretch (600) - await pointer3.moveBy(const Offset(0.0, 210.0)); - await tester.pumpAndSettle(); - expect(find.text('Index 1'), findsOneWidget); - expect(tester.getCenter(find.text('Index 1')).dy, greaterThan(lastStretchedLocation)); - lastStretchedLocation = tester.getCenter(find.text('Index 1')).dy; + final TestGesture pointer3 = await tester.startGesture(tester.getCenter(find.text('Index 1'))); + // Add overscroll from an additional pointer, exceeding the max stretch (600) + await pointer3.moveBy(const Offset(0.0, 210.0)); + await tester.pumpAndSettle(); + expect(find.text('Index 1'), findsOneWidget); + expect(tester.getCenter(find.text('Index 1')).dy, greaterThan(lastStretchedLocation)); + lastStretchedLocation = tester.getCenter(find.text('Index 1')).dy; - final TestGesture pointer4 = await tester.startGesture( - tester.getCenter(find.text('Index 1')), - ); - // Since we have maxed out the overscroll, it should not have stretched - // further, regardless of the number of pointers. - await pointer4.moveBy(const Offset(0.0, 210.0)); - await tester.pumpAndSettle(); - expect(find.text('Index 1'), findsOneWidget); - expect(tester.getCenter(find.text('Index 1')).dy, lastStretchedLocation); + final TestGesture pointer4 = await tester.startGesture(tester.getCenter(find.text('Index 1'))); + // Since we have maxed out the overscroll, it should not have stretched + // further, regardless of the number of pointers. + await pointer4.moveBy(const Offset(0.0, 210.0)); + await tester.pumpAndSettle(); + expect(find.text('Index 1'), findsOneWidget); + expect(tester.getCenter(find.text('Index 1')).dy, lastStretchedLocation); - await pointer1.up(); - await pointer2.up(); - await pointer3.up(); - await pointer4.up(); - await tester.pumpAndSettle(); - }, - // Skips this test when fragment shaders are used. - skip: shaderSupported, - ); + await pointer1.up(); + await pointer2.up(); + await pointer3.up(); + await pointer4.up(); + await tester.pumpAndSettle(); + }); - testWidgets( - 'Stretch overscroll vertically, change direction mid scroll', - (WidgetTester tester) async { - final GlobalKey box1Key = GlobalKey(); - final GlobalKey box2Key = GlobalKey(); - final GlobalKey box3Key = GlobalKey(); - final ScrollController controller = ScrollController(); - addTearDown(controller.dispose); + testWidgets('Stretch overscroll vertically, change direction mid scroll', ( + WidgetTester tester, + ) async { + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); - await tester.pumpWidget( - buildTest( - box1Key, - box2Key, - box3Key, - controller, - // Setting the `boxHeight` to 100.0 will make the boxes fit in the - // scrollable viewport. - boxHeight: 100, - // To make the scroll view in the test still scrollable, we need to add - // the `AlwaysScrollableScrollPhysics`. - physics: const AlwaysScrollableScrollPhysics(), - ), - ); + await tester.pumpWidget( + buildTest( + box1Key, + box2Key, + box3Key, + controller, + // Setting the `boxHeight` to 100.0 will make the boxes fit in the + // scrollable viewport. + boxHeight: 100, + // To make the scroll view in the test still scrollable, we need to add + // the `AlwaysScrollableScrollPhysics`. + physics: const AlwaysScrollableScrollPhysics(), + ), + ); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); - final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); - final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); + final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); + final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 100.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 200.0)); + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 100.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 200.0)); - final TestGesture gesture = await tester.startGesture( - tester.getCenter(find.byType(CustomScrollView)), - ); - // Overscroll the start - await gesture.moveBy(const Offset(0.0, 600.0)); - await tester.pumpAndSettle(); + final TestGesture gesture = await tester.startGesture( + tester.getCenter(find.byType(CustomScrollView)), + ); + // Overscroll the start + await gesture.moveBy(const Offset(0.0, 600.0)); + await tester.pumpAndSettle(); - // The boxes should now be at different locations because of the scaling. - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero).dy, greaterThan(103.0)); - expect(box3.localToGlobal(Offset.zero).dy, greaterThan(206.0)); + // The boxes should now be at different locations because of the scaling. + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero).dy, greaterThan(103.0)); + expect(box3.localToGlobal(Offset.zero).dy, greaterThan(206.0)); - // Move the pointer up a miniscule amount to trigger a directional change. - await gesture.moveBy(const Offset(0.0, -20.0)); - await tester.pumpAndSettle(); + // Move the pointer up a miniscule amount to trigger a directional change. + await gesture.moveBy(const Offset(0.0, -20.0)); + await tester.pumpAndSettle(); - // The boxes should remain roughly at the same locations, since the pointer - // didn't move far. - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero).dy, greaterThan(103.0)); - expect(box3.localToGlobal(Offset.zero).dy, greaterThan(206.0)); + // The boxes should remain roughly at the same locations, since the pointer + // didn't move far. + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero).dy, greaterThan(103.0)); + expect(box3.localToGlobal(Offset.zero).dy, greaterThan(206.0)); - // Now make the pointer overscroll to the end - await gesture.moveBy(const Offset(0.0, -1200.0)); - await tester.pumpAndSettle(); + // Now make the pointer overscroll to the end + await gesture.moveBy(const Offset(0.0, -1200.0)); + await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero).dy, lessThan(-19.0)); - expect(box2.localToGlobal(Offset.zero).dy, lessThan(85.0)); - expect(box3.localToGlobal(Offset.zero).dy, lessThan(188.0)); + expect(box1.localToGlobal(Offset.zero).dy, lessThan(-19.0)); + expect(box2.localToGlobal(Offset.zero).dy, lessThan(85.0)); + expect(box3.localToGlobal(Offset.zero).dy, lessThan(188.0)); - // Release the pointer - await gesture.up(); - await tester.pumpAndSettle(); + // Release the pointer + await gesture.up(); + await tester.pumpAndSettle(); - // Now the boxes should be back to their original locations. - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 100.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 200.0)); - }, - // Skips this test when fragment shaders are used. - skip: shaderSupported, - ); + // Now the boxes should be back to their original locations. + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 100.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 200.0)); + }); - testWidgets( - 'Stretch overscroll horizontally, change direction mid scroll', - (WidgetTester tester) async { - final GlobalKey box1Key = GlobalKey(); - final GlobalKey box2Key = GlobalKey(); - final GlobalKey box3Key = GlobalKey(); - final ScrollController controller = ScrollController(); - addTearDown(controller.dispose); + testWidgets('Stretch overscroll horizontally, change direction mid scroll', ( + WidgetTester tester, + ) async { + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); - await tester.pumpWidget( - buildTest( - box1Key, - box2Key, - box3Key, - controller, - // Setting the `boxWidth` to 100.0 will make the boxes fit in the - // scrollable viewport. - boxWidth: 100, - // To make the scroll view in the test still scrollable, we need to add - // the `AlwaysScrollableScrollPhysics`. - physics: const AlwaysScrollableScrollPhysics(), - axis: Axis.horizontal, - ), - ); + await tester.pumpWidget( + buildTest( + box1Key, + box2Key, + box3Key, + controller, + // Setting the `boxWidth` to 100.0 will make the boxes fit in the + // scrollable viewport. + boxWidth: 100, + // To make the scroll view in the test still scrollable, we need to add + // the `AlwaysScrollableScrollPhysics`. + physics: const AlwaysScrollableScrollPhysics(), + axis: Axis.horizontal, + ), + ); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); - final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); - final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); + final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); + final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(100.0, 0.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(200.0, 0.0)); + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(100.0, 0.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(200.0, 0.0)); - final TestGesture gesture = await tester.startGesture( - tester.getCenter(find.byType(CustomScrollView)), - ); - // Overscroll the start - await gesture.moveBy(const Offset(600.0, 0.0)); - await tester.pumpAndSettle(); + final TestGesture gesture = await tester.startGesture( + tester.getCenter(find.byType(CustomScrollView)), + ); + // Overscroll the start + await gesture.moveBy(const Offset(600.0, 0.0)); + await tester.pumpAndSettle(); - // The boxes should now be at different locations because of the scaling. - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero).dx, greaterThan(102.0)); - expect(box3.localToGlobal(Offset.zero).dx, greaterThan(205.0)); + // The boxes should now be at different locations because of the scaling. + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero).dx, greaterThan(102.0)); + expect(box3.localToGlobal(Offset.zero).dx, greaterThan(205.0)); - // Move the pointer up a miniscule amount to trigger a directional change. - await gesture.moveBy(const Offset(-20.0, 0.0)); - await tester.pumpAndSettle(); + // Move the pointer up a miniscule amount to trigger a directional change. + await gesture.moveBy(const Offset(-20.0, 0.0)); + await tester.pumpAndSettle(); - // The boxes should remain roughly at the same locations, since the pointer - // didn't move far. - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero).dx, greaterThan(102.0)); - expect(box3.localToGlobal(Offset.zero).dx, greaterThan(205.0)); + // The boxes should remain roughly at the same locations, since the pointer + // didn't move far. + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero).dx, greaterThan(102.0)); + expect(box3.localToGlobal(Offset.zero).dx, greaterThan(205.0)); - // Now make the pointer overscroll to the end - await gesture.moveBy(const Offset(-1200.0, 0.0)); - await tester.pumpAndSettle(); + // Now make the pointer overscroll to the end + await gesture.moveBy(const Offset(-1200.0, 0.0)); + await tester.pumpAndSettle(); - expect(box1.localToGlobal(Offset.zero).dx, lessThan(-19.0)); - expect(box2.localToGlobal(Offset.zero).dx, lessThan(85.0)); - expect(box3.localToGlobal(Offset.zero).dx, lessThan(188.0)); + expect(box1.localToGlobal(Offset.zero).dx, lessThan(-19.0)); + expect(box2.localToGlobal(Offset.zero).dx, lessThan(85.0)); + expect(box3.localToGlobal(Offset.zero).dx, lessThan(188.0)); - // Release the pointer - await gesture.up(); - await tester.pumpAndSettle(); + // Release the pointer + await gesture.up(); + await tester.pumpAndSettle(); - // Now the boxes should be back to their original locations. - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(100.0, 0.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(200.0, 0.0)); - }, - // Skips this test when fragment shaders are used. - skip: shaderSupported, - ); + // Now the boxes should be back to their original locations. + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(100.0, 0.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(200.0, 0.0)); + }); - testWidgets( - 'Fling toward the trailing edge causes stretch toward the leading edge', - (WidgetTester tester) async { - final GlobalKey box1Key = GlobalKey(); - final GlobalKey box2Key = GlobalKey(); - final GlobalKey box3Key = GlobalKey(); - final ScrollController controller = ScrollController(); - addTearDown(controller.dispose); + testWidgets('Fling toward the trailing edge causes stretch toward the leading edge', ( + WidgetTester tester, + ) async { + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); - await tester.pumpWidget(buildTest(box1Key, box2Key, box3Key, controller)); + await tester.pumpWidget(buildTest(box1Key, box2Key, box3Key, controller)); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); - final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); - final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); + final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); + final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 500.0)); + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 500.0)); - await tester.fling(find.byType(CustomScrollView), const Offset(0.0, -50.0), 10000.0); - await tester.pump(const Duration(milliseconds: 100)); - await tester.pump(const Duration(milliseconds: 100)); - await tester.pump(const Duration(milliseconds: 100)); + await tester.fling(find.byType(CustomScrollView), const Offset(0.0, -50.0), 10000.0); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); - // The boxes should now be at different locations because of the scaling. - expect(controller.offset, 150.0); - expect(box1.localToGlobal(Offset.zero).dy, lessThan(-160.0)); - expect(box2.localToGlobal(Offset.zero).dy, lessThan(93.0)); - expect(box3.localToGlobal(Offset.zero).dy, lessThan(347.0)); + // The boxes should now be at different locations because of the scaling. + expect(controller.offset, 150.0); + expect(box1.localToGlobal(Offset.zero).dy, lessThan(-160.0)); + expect(box2.localToGlobal(Offset.zero).dy, lessThan(93.0)); + expect(box3.localToGlobal(Offset.zero).dy, lessThan(347.0)); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(); - // The boxes should now be at their final position. - expect(controller.offset, 150.0); - expect(box1.localToGlobal(Offset.zero).dy, -150.0); - expect(box2.localToGlobal(Offset.zero).dy, 100.0); - expect(box3.localToGlobal(Offset.zero).dy, 350.0); - }, - // Skips this test when fragment shaders are used. - skip: shaderSupported, - ); + // The boxes should now be at their final position. + expect(controller.offset, 150.0); + expect(box1.localToGlobal(Offset.zero).dy, -150.0); + expect(box2.localToGlobal(Offset.zero).dy, 100.0); + expect(box3.localToGlobal(Offset.zero).dy, 350.0); + }); - testWidgets( - 'Fling toward the leading edge causes stretch toward the trailing edge', - (WidgetTester tester) async { - final GlobalKey box1Key = GlobalKey(); - final GlobalKey box2Key = GlobalKey(); - final GlobalKey box3Key = GlobalKey(); - final ScrollController controller = ScrollController(); - addTearDown(controller.dispose); + testWidgets('Fling toward the leading edge causes stretch toward the trailing edge', ( + WidgetTester tester, + ) async { + final GlobalKey box1Key = GlobalKey(); + final GlobalKey box2Key = GlobalKey(); + final GlobalKey box3Key = GlobalKey(); + final ScrollController controller = ScrollController(); + addTearDown(controller.dispose); - await tester.pumpWidget(buildTest(box1Key, box2Key, box3Key, controller)); + await tester.pumpWidget(buildTest(box1Key, box2Key, box3Key, controller)); - expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); - expect(find.byType(GlowingOverscrollIndicator), findsNothing); - final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); - final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); - final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); + expect(find.byType(StretchingOverscrollIndicator), findsOneWidget); + expect(find.byType(GlowingOverscrollIndicator), findsNothing); + final RenderBox box1 = tester.renderObject(find.byKey(box1Key)); + final RenderBox box2 = tester.renderObject(find.byKey(box2Key)); + final RenderBox box3 = tester.renderObject(find.byKey(box3Key)); - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero), Offset.zero); - expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0)); - expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 500.0)); + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero), Offset.zero); + expect(box2.localToGlobal(Offset.zero), const Offset(0.0, 250.0)); + expect(box3.localToGlobal(Offset.zero), const Offset(0.0, 500.0)); - // We fling to the trailing edge and let it settle. - await tester.fling(find.byType(CustomScrollView), const Offset(0.0, -50.0), 10000.0); - await tester.pumpAndSettle(); + // We fling to the trailing edge and let it settle. + await tester.fling(find.byType(CustomScrollView), const Offset(0.0, -50.0), 10000.0); + await tester.pumpAndSettle(); - // We are now at the trailing edge - expect(controller.offset, 150.0); - expect(box1.localToGlobal(Offset.zero).dy, -150.0); - expect(box2.localToGlobal(Offset.zero).dy, 100.0); - expect(box3.localToGlobal(Offset.zero).dy, 350.0); + // We are now at the trailing edge + expect(controller.offset, 150.0); + expect(box1.localToGlobal(Offset.zero).dy, -150.0); + expect(box2.localToGlobal(Offset.zero).dy, 100.0); + expect(box3.localToGlobal(Offset.zero).dy, 350.0); - // Now fling to the leading edge - await tester.fling(find.byType(CustomScrollView), const Offset(0.0, 50.0), 10000.0); - await tester.pump(const Duration(milliseconds: 100)); - await tester.pump(const Duration(milliseconds: 100)); - await tester.pump(const Duration(milliseconds: 100)); - await tester.pump(const Duration(milliseconds: 100)); + // Now fling to the leading edge + await tester.fling(find.byType(CustomScrollView), const Offset(0.0, 50.0), 10000.0); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); - // The boxes should now be at different locations because of the scaling. - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero).dy, 0.0); - expect(box2.localToGlobal(Offset.zero).dy, greaterThan(254.0)); - expect(box3.localToGlobal(Offset.zero).dy, greaterThan(508.0)); + // The boxes should now be at different locations because of the scaling. + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero).dy, 0.0); + expect(box2.localToGlobal(Offset.zero).dy, greaterThan(254.0)); + expect(box3.localToGlobal(Offset.zero).dy, greaterThan(508.0)); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(); - // The boxes should now be at their final position. - expect(controller.offset, 0.0); - expect(box1.localToGlobal(Offset.zero).dy, 0.0); - expect(box2.localToGlobal(Offset.zero).dy, 250.0); - expect(box3.localToGlobal(Offset.zero).dy, 500.0); - }, - // Skips this test when fragment shaders are used. - skip: shaderSupported, - ); + // The boxes should now be at their final position. + expect(controller.offset, 0.0); + expect(box1.localToGlobal(Offset.zero).dy, 0.0); + expect(box2.localToGlobal(Offset.zero).dy, 250.0); + expect(box3.localToGlobal(Offset.zero).dy, 500.0); + }); testWidgets( 'changing scroll direction during recede animation will not change the stretch direction', @@ -1231,8 +1166,6 @@ void main() { await gesture.up(); }, - // Skips this test when fragment shaders are used. - skip: shaderSupported, ); testWidgets('Stretch overscroll only uses image filter during stretch effect', ( @@ -1300,7 +1233,7 @@ void main() { // We fling to the trailing edge and let it settle. await tester.fling(find.byType(CustomScrollView), const Offset(0.0, -50.0), 10000.0); - await tester.pumpAndSettle(const Duration(milliseconds: 300)); + await tester.pumpAndSettle(); // We are now at the trailing edge expect(overscrollNotification.velocity, lessThan(25)); diff --git a/packages/flutter/test/widgets/page_view_test.dart b/packages/flutter/test/widgets/page_view_test.dart index 3d127de0b42..92aa4509efb 100644 --- a/packages/flutter/test/widgets/page_view_test.dart +++ b/packages/flutter/test/widgets/page_view_test.dart @@ -2,11 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// This file is run as part of a reduced test set in CI on Mac and Windows -// machines. -@Tags(['reduced-test-set']) -library; - import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/material.dart'; @@ -1269,10 +1264,19 @@ void main() { controller.animateToPage(2, duration: const Duration(milliseconds: 300), curve: Curves.ease); await tester.pumpAndSettle(); - await expectLater( - find.byType(PageView), - matchesGoldenFile('page_view_no_stretch_precision_error.png'), + final Finder transformFinder = find.descendant( + of: find.byType(PageView), + matching: find.byType(Transform), ); + expect(transformFinder, findsOneWidget); + + // Get the Transform widget that stretches the PageView. + final Transform transform = tester.firstWidget( + find.descendant(of: find.byType(PageView), matching: find.byType(Transform)), + ); + + // Check the stretch factor in the first element of the transform matrix. + expect(transform.transform.storage.first, 1.0); }); testWidgets('PageController onAttach, onDetach', (WidgetTester tester) async { diff --git a/packages/flutter/test/widgets/stretch_effect_test.dart b/packages/flutter/test/widgets/stretch_effect_test.dart deleted file mode 100644 index 9f519c1e42f..00000000000 --- a/packages/flutter/test/widgets/stretch_effect_test.dart +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// This file is run as part of a reduced test set in CI on Mac and Windows -// machines. -@Tags(['reduced-test-set']) -library; - -import 'dart:ui' as ui; - -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - // `StretchingOverscrollIndicator` uses a different algorithm when - // shader is available, therefore the tests must be different depending - // on shader support. - final bool shaderSupported = ui.ImageFilter.isShaderFilterSupported; - - testWidgets( - 'Stretch effect covers full viewport', - (WidgetTester tester) async { - // This test verifies that when the stretch effect is applied to a scrollable widget, - // it should cover the entire scrollable area (e.g., full height of the scroll view), - // even if the actual content inside has a smaller height. - // - // Without this behavior, the shader is clipped only to the content area, - // causing the stretch effect to render incorrectly or be invisible - // when the content doesn't fill the scroll view. - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: StretchEffect( - stretchStrength: 1, - axis: Axis.vertical, - child: Stack( - alignment: Alignment.topCenter, - children: [ - Container(height: 100), - Container(height: 50, color: const Color.fromRGBO(255, 0, 0, 1)), - ], - ), - ), - ), - ); - - await expectLater( - find.byType(StretchEffect), - matchesGoldenFile('stretch_effect_covers_full_viewport.png'), - ); - }, - // Skips this test when fragment shaders are not used. - skip: !shaderSupported, - ); -} diff --git a/packages/flutter_tools/lib/src/asset.dart b/packages/flutter_tools/lib/src/asset.dart index 406ae53d825..393f882f381 100644 --- a/packages/flutter_tools/lib/src/asset.dart +++ b/packages/flutter_tools/lib/src/asset.dart @@ -50,7 +50,7 @@ const kMaterialFonts = >[ }, ]; -const kMaterialShaders = ['shaders/ink_sparkle.frag', 'shaders/stretch_effect.frag']; +const kMaterialShaders = ['shaders/ink_sparkle.frag']; /// Injected factory class for spawning [AssetBundle] instances. abstract class AssetBundleFactory { diff --git a/packages/flutter_tools/test/general.shard/asset_bundle_test.dart b/packages/flutter_tools/test/general.shard/asset_bundle_test.dart index 39c11fded0d..4edbc1b89cb 100644 --- a/packages/flutter_tools/test/general.shard/asset_bundle_test.dart +++ b/packages/flutter_tools/test/general.shard/asset_bundle_test.dart @@ -1017,30 +1017,26 @@ flutter: materialDir.childFile(shader).createSync(recursive: true); } - final testShaders = ['ink_sparkle.frag', 'stretch_effect.frag']; - - for (final shader in testShaders) { - (globals.processManager as FakeProcessManager).addCommand( - FakeCommand( - command: [ - impellerc, - '--sksl', - '--iplr', - '--json', - '--sl=${fileSystem.path.join(output.path, 'shaders', shader)}', - '--spirv=${fileSystem.path.join(output.path, 'shaders', '$shader.spirv')}', - '--input=${fileSystem.path.join(materialDir.path, 'shaders', shader)}', - '--input-type=frag', - '--include=${fileSystem.path.join(materialDir.path, 'shaders')}', - '--include=$shaderLibDir', - ], - onRun: (_) { - fileSystem.file(outputPath).createSync(recursive: true); - fileSystem.file('$outputPath.spirv').createSync(recursive: true); - }, - ), - ); - } + (globals.processManager as FakeProcessManager).addCommand( + FakeCommand( + command: [ + impellerc, + '--sksl', + '--iplr', + '--json', + '--sl=${fileSystem.path.join(output.path, 'shaders', 'ink_sparkle.frag')}', + '--spirv=${fileSystem.path.join(output.path, 'shaders', 'ink_sparkle.frag.spirv')}', + '--input=${fileSystem.path.join(materialDir.path, 'shaders', 'ink_sparkle.frag')}', + '--input-type=frag', + '--include=${fileSystem.path.join(materialDir.path, 'shaders')}', + '--include=$shaderLibDir', + ], + onRun: (_) { + fileSystem.file(outputPath).createSync(recursive: true); + fileSystem.file('$outputPath.spirv').createSync(recursive: true); + }, + ), + ); fileSystem.file('pubspec.yaml') ..createSync() diff --git a/packages/flutter_tools/test/general.shard/asset_test.dart b/packages/flutter_tools/test/general.shard/asset_test.dart index 3e1f99ccedb..f64c64f6234 100644 --- a/packages/flutter_tools/test/general.shard/asset_test.dart +++ b/packages/flutter_tools/test/general.shard/asset_test.dart @@ -304,23 +304,18 @@ flutter: expect(assetBundle.inputFiles.map((File f) => f.path), []); }); - final testShaders = ['ink_sparkle.frag', 'stretch_effect.frag']; - testWithoutContext('bundles material shaders on non-web platforms', () async { - for (final shader in testShaders) { - final String shaderPath = fileSystem.path.join( - flutterRoot, - 'packages', - 'flutter', - 'lib', - 'src', - 'material', - 'shaders', - shader, - ); - fileSystem.file(shaderPath).createSync(recursive: true); - } - + final String shaderPath = fileSystem.path.join( + flutterRoot, + 'packages', + 'flutter', + 'lib', + 'src', + 'material', + 'shaders', + 'ink_sparkle.frag', + ); + fileSystem.file(shaderPath).createSync(recursive: true); writePackageConfigFiles(directory: fileSystem.currentDirectory, mainLibName: 'my_package'); fileSystem.file('pubspec.yaml').writeAsStringSync('name: my_package'); final assetBundle = ManifestAssetBundle( @@ -336,26 +331,21 @@ flutter: flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), ); - for (final shader in testShaders) { - expect(assetBundle.entries.keys, contains('shaders/$shader')); - } + expect(assetBundle.entries.keys, contains('shaders/ink_sparkle.frag')); }); testWithoutContext('bundles material shaders on web platforms', () async { - for (final shader in testShaders) { - final String shaderPath = fileSystem.path.join( - flutterRoot, - 'packages', - 'flutter', - 'lib', - 'src', - 'material', - 'shaders', - shader, - ); - fileSystem.file(shaderPath).createSync(recursive: true); - } - + final String shaderPath = fileSystem.path.join( + flutterRoot, + 'packages', + 'flutter', + 'lib', + 'src', + 'material', + 'shaders', + 'ink_sparkle.frag', + ); + fileSystem.file(shaderPath).createSync(recursive: true); writePackageConfigFiles(directory: fileSystem.currentDirectory, mainLibName: 'my_package'); fileSystem.file('pubspec.yaml').writeAsStringSync('name: my_package'); final assetBundle = ManifestAssetBundle( @@ -371,9 +361,7 @@ flutter: flutterProject: FlutterProject.fromDirectoryTest(fileSystem.currentDirectory), ); - for (final shader in testShaders) { - expect(assetBundle.entries.keys, contains('shaders/$shader')); - } + expect(assetBundle.entries.keys, contains('shaders/ink_sparkle.frag')); }); }); }