Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit 8e8a1c8

Browse files
authored
Fix StretchingOverscrollIndicator clipping and add clipBehavior parameter (#105303)
1 parent 3f401a1 commit 8e8a1c8

File tree

6 files changed

+173
-3
lines changed

6 files changed

+173
-3
lines changed

packages/flutter/lib/src/material/app.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,7 @@ class MaterialScrollBehavior extends ScrollBehavior {
819819
case AndroidOverscrollIndicator.stretch:
820820
return StretchingOverscrollIndicator(
821821
axisDirection: details.direction,
822+
clipBehavior: details.clipBehavior,
822823
child: child,
823824
);
824825
case AndroidOverscrollIndicator.glow:

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -653,9 +653,11 @@ class StretchingOverscrollIndicator extends StatefulWidget {
653653
super.key,
654654
required this.axisDirection,
655655
this.notificationPredicate = defaultScrollNotificationPredicate,
656+
this.clipBehavior = Clip.hardEdge,
656657
this.child,
657658
}) : assert(axisDirection != null),
658-
assert(notificationPredicate != null);
659+
assert(notificationPredicate != null),
660+
assert(clipBehavior != null);
659661

660662
/// {@macro flutter.overscroll.axisDirection}
661663
final AxisDirection axisDirection;
@@ -666,6 +668,11 @@ class StretchingOverscrollIndicator extends StatefulWidget {
666668
/// {@macro flutter.overscroll.notificationPredicate}
667669
final ScrollNotificationPredicate notificationPredicate;
668670

671+
/// {@macro flutter.material.Material.clipBehavior}
672+
///
673+
/// Defaults to [Clip.hardEdge].
674+
final Clip clipBehavior;
675+
669676
/// The widget below this widget in the tree.
670677
///
671678
/// The overscroll indicator will apply a stretch effect to this child. This
@@ -806,7 +813,8 @@ class _StretchingOverscrollIndicatorState extends State<StretchingOverscrollIndi
806813
// screen, overflow from transforming the viewport is irrelevant.
807814
return ClipRect(
808815
clipBehavior: stretch != 0.0 && viewportDimension != mainAxisSize
809-
? Clip.hardEdge : Clip.none,
816+
? widget.clipBehavior
817+
: Clip.none,
810818
child: transform,
811819
);
812820
},

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,7 @@ abstract class ScrollView extends StatelessWidget {
425425
viewportBuilder: (BuildContext context, ViewportOffset offset) {
426426
return buildViewport(context, offset, axisDirection, slivers);
427427
},
428+
clipBehavior: clipBehavior,
428429
);
429430

430431
final Widget scrollableResult = effectivePrimary && scrollController != null

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ class Scrollable extends StatefulWidget {
9797
this.dragStartBehavior = DragStartBehavior.start,
9898
this.restorationId,
9999
this.scrollBehavior,
100+
this.clipBehavior = Clip.hardEdge,
100101
}) : assert(axisDirection != null),
101102
assert(dragStartBehavior != null),
102103
assert(viewportBuilder != null),
@@ -261,6 +262,14 @@ class Scrollable extends StatefulWidget {
261262
/// [ScrollBehavior].
262263
final ScrollBehavior? scrollBehavior;
263264

265+
/// {@macro flutter.material.Material.clipBehavior}
266+
///
267+
/// Defaults to [Clip.hardEdge].
268+
///
269+
/// Rather than clipping [Scrollable], this is passed to decorators in
270+
/// [ScrollableDetails].
271+
final Clip clipBehavior;
272+
264273
/// The axis along which the scroll view scrolls.
265274
///
266275
/// Determined by the [axisDirection].
@@ -797,6 +806,7 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
797806
final ScrollableDetails details = ScrollableDetails(
798807
direction: widget.axisDirection,
799808
controller: _effectiveScrollController,
809+
clipBehavior: widget.clipBehavior,
800810
);
801811

802812
result = _configuration.buildScrollbar(
@@ -812,7 +822,7 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
812822
state: this,
813823
position: position,
814824
registrar: registrar,
815-
child: result
825+
child: result,
816826
);
817827
}
818828

@@ -1313,6 +1323,7 @@ class ScrollableDetails {
13131323
const ScrollableDetails({
13141324
required this.direction,
13151325
required this.controller,
1326+
required this.clipBehavior,
13161327
});
13171328

13181329
/// The direction in which this widget scrolls.
@@ -1326,6 +1337,13 @@ class ScrollableDetails {
13261337
/// This can be used by [ScrollBehavior] to apply a [Scrollbar] to the associated
13271338
/// [Scrollable].
13281339
final ScrollController controller;
1340+
1341+
/// {@macro flutter.material.Material.clipBehavior}
1342+
///
1343+
/// This can be used by [MaterialScrollBehavior] to clip [StretchingOverscrollIndicator].
1344+
///
1345+
/// Cannot be null.
1346+
final Clip clipBehavior;
13291347
}
13301348

13311349
/// With [_ScrollSemantics] certain child [SemanticsNode]s can be

packages/flutter/test/material/app_test.dart

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import 'package:flutter/cupertino.dart';
1212
import 'package:flutter/foundation.dart';
1313
import 'package:flutter/material.dart';
14+
import 'package:flutter/rendering.dart';
1415
import 'package:flutter/services.dart';
1516
import 'package:flutter_test/flutter_test.dart';
1617

@@ -1265,6 +1266,83 @@ void main() {
12651266
expect(find.byType(GlowingOverscrollIndicator), findsNothing);
12661267
}, variant: TargetPlatformVariant.only(TargetPlatform.android));
12671268

1269+
testWidgets(
1270+
'ListView clip behavior updates overscroll indicator clip behavior', (WidgetTester tester) async {
1271+
Widget buildFrame(Clip clipBehavior) {
1272+
return MaterialApp(
1273+
theme: ThemeData(useMaterial3: true),
1274+
home: Column(
1275+
children: <Widget>[
1276+
SizedBox(
1277+
height: 300,
1278+
child: ListView.builder(
1279+
itemCount: 20,
1280+
clipBehavior: clipBehavior,
1281+
itemBuilder: (BuildContext context, int index){
1282+
return Padding(
1283+
padding: const EdgeInsets.all(10.0),
1284+
child: Text('Index $index'),
1285+
);
1286+
},
1287+
),
1288+
),
1289+
Opacity(
1290+
opacity: 0.5,
1291+
child: Container(
1292+
color: const Color(0xD0FF0000),
1293+
height: 100,
1294+
),
1295+
),
1296+
],
1297+
),
1298+
);
1299+
}
1300+
1301+
// Test default clip behavior.
1302+
await tester.pumpWidget(buildFrame(Clip.hardEdge));
1303+
1304+
expect(find.byType(StretchingOverscrollIndicator), findsOneWidget);
1305+
expect(find.byType(GlowingOverscrollIndicator), findsNothing);
1306+
expect(find.text('Index 1'), findsOneWidget);
1307+
1308+
RenderClipRect renderClip = tester.allRenderObjects.whereType<RenderClipRect>().first;
1309+
// Currently not clipping
1310+
expect(renderClip.clipBehavior, equals(Clip.none));
1311+
1312+
TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Index 1')));
1313+
// Overscroll the start.
1314+
await gesture.moveBy(const Offset(0.0, 200.0));
1315+
await tester.pumpAndSettle();
1316+
expect(find.text('Index 1'), findsOneWidget);
1317+
expect(tester.getCenter(find.text('Index 1')).dy, greaterThan(0));
1318+
renderClip = tester.allRenderObjects.whereType<RenderClipRect>().first;
1319+
// Now clipping
1320+
expect(renderClip.clipBehavior, equals(Clip.hardEdge));
1321+
1322+
await gesture.up();
1323+
await tester.pumpAndSettle();
1324+
1325+
// Test custom clip behavior.
1326+
await tester.pumpWidget(buildFrame(Clip.none));
1327+
1328+
renderClip = tester.allRenderObjects.whereType<RenderClipRect>().first;
1329+
// Currently not clipping
1330+
expect(renderClip.clipBehavior, equals(Clip.none));
1331+
1332+
gesture = await tester.startGesture(tester.getCenter(find.text('Index 1')));
1333+
// Overscroll the start.
1334+
await gesture.moveBy(const Offset(0.0, 200.0));
1335+
await tester.pumpAndSettle();
1336+
expect(find.text('Index 1'), findsOneWidget);
1337+
expect(tester.getCenter(find.text('Index 1')).dy, greaterThan(0));
1338+
renderClip = tester.allRenderObjects.whereType<RenderClipRect>().first;
1339+
// Now clipping
1340+
expect(renderClip.clipBehavior, equals(Clip.none));
1341+
1342+
await gesture.up();
1343+
await tester.pumpAndSettle();
1344+
}, variant: TargetPlatformVariant.only(TargetPlatform.android));
1345+
12681346
testWidgets('When `useInheritedMediaQuery` is true an existing MediaQuery is used if one is available', (WidgetTester tester) async {
12691347
late BuildContext capturedContext;
12701348
final UniqueKey uniqueKey = UniqueKey();

packages/flutter/test/widgets/overscroll_stretch_indicator_test.dart

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,70 @@ void main() {
454454
await tester.pumpAndSettle();
455455
});
456456

457+
testWidgets('clipBehavior parameter updates overscroll clipping behavior', (WidgetTester tester) async {
458+
// Regression test for https://github.com/flutter/flutter/issues/103491
459+
460+
Widget buildFrame(Clip clipBehavior) {
461+
return Directionality(
462+
textDirection: TextDirection.ltr,
463+
child: MediaQuery(
464+
data: const MediaQueryData(size: Size(800.0, 600.0)),
465+
child: ScrollConfiguration(
466+
behavior: const ScrollBehavior().copyWith(overscroll: false),
467+
child: Column(
468+
children: <Widget>[
469+
StretchingOverscrollIndicator(
470+
axisDirection: AxisDirection.down,
471+
clipBehavior: clipBehavior,
472+
child: SizedBox(
473+
height: 300,
474+
child: ListView.builder(
475+
itemCount: 20,
476+
itemBuilder: (BuildContext context, int index){
477+
return Padding(
478+
padding: const EdgeInsets.all(10.0),
479+
child: Text('Index $index'),
480+
);
481+
},
482+
),
483+
),
484+
),
485+
Opacity(
486+
opacity: 0.5,
487+
child: Container(
488+
color: const Color(0xD0FF0000),
489+
height: 100,
490+
),
491+
),
492+
],
493+
),
494+
),
495+
),
496+
);
497+
}
498+
499+
await tester.pumpWidget(buildFrame(Clip.none));
500+
501+
expect(find.text('Index 1'), findsOneWidget);
502+
expect(tester.getCenter(find.text('Index 1')).dy, 51.0);
503+
RenderClipRect renderClip = tester.allRenderObjects.whereType<RenderClipRect>().first;
504+
// Currently not clipping
505+
expect(renderClip.clipBehavior, equals(Clip.none));
506+
507+
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Index 1')));
508+
// Overscroll the start.
509+
await gesture.moveBy(const Offset(0.0, 200.0));
510+
await tester.pumpAndSettle();
511+
expect(find.text('Index 1'), findsOneWidget);
512+
expect(tester.getCenter(find.text('Index 1')).dy, greaterThan(0));
513+
renderClip = tester.allRenderObjects.whereType<RenderClipRect>().first;
514+
// Now clipping
515+
expect(renderClip.clipBehavior, equals(Clip.none));
516+
517+
await gesture.up();
518+
await tester.pumpAndSettle();
519+
});
520+
457521
testWidgets('Stretch limit', (WidgetTester tester) async {
458522
// Regression test for https://github.com/flutter/flutter/issues/99264
459523
await tester.pumpWidget(

0 commit comments

Comments
 (0)