mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
[Reland]: Fix StretchingOverscrollIndicator
clipping and add clipBehavior
parameter (#106287)
This commit is contained in:
parent
9a3b9f62a9
commit
a74c48133a
@ -819,6 +819,7 @@ class MaterialScrollBehavior extends ScrollBehavior {
|
|||||||
case AndroidOverscrollIndicator.stretch:
|
case AndroidOverscrollIndicator.stretch:
|
||||||
return StretchingOverscrollIndicator(
|
return StretchingOverscrollIndicator(
|
||||||
axisDirection: details.direction,
|
axisDirection: details.direction,
|
||||||
|
clipBehavior: details.clipBehavior ?? Clip.hardEdge,
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
case AndroidOverscrollIndicator.glow:
|
case AndroidOverscrollIndicator.glow:
|
||||||
|
@ -653,9 +653,11 @@ class StretchingOverscrollIndicator extends StatefulWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.axisDirection,
|
required this.axisDirection,
|
||||||
this.notificationPredicate = defaultScrollNotificationPredicate,
|
this.notificationPredicate = defaultScrollNotificationPredicate,
|
||||||
|
this.clipBehavior = Clip.hardEdge,
|
||||||
this.child,
|
this.child,
|
||||||
}) : assert(axisDirection != null),
|
}) : assert(axisDirection != null),
|
||||||
assert(notificationPredicate != null);
|
assert(notificationPredicate != null),
|
||||||
|
assert(clipBehavior != null);
|
||||||
|
|
||||||
/// {@macro flutter.overscroll.axisDirection}
|
/// {@macro flutter.overscroll.axisDirection}
|
||||||
final AxisDirection axisDirection;
|
final AxisDirection axisDirection;
|
||||||
@ -666,6 +668,11 @@ class StretchingOverscrollIndicator extends StatefulWidget {
|
|||||||
/// {@macro flutter.overscroll.notificationPredicate}
|
/// {@macro flutter.overscroll.notificationPredicate}
|
||||||
final ScrollNotificationPredicate notificationPredicate;
|
final ScrollNotificationPredicate notificationPredicate;
|
||||||
|
|
||||||
|
/// {@macro flutter.material.Material.clipBehavior}
|
||||||
|
///
|
||||||
|
/// Defaults to [Clip.hardEdge].
|
||||||
|
final Clip clipBehavior;
|
||||||
|
|
||||||
/// The widget below this widget in the tree.
|
/// The widget below this widget in the tree.
|
||||||
///
|
///
|
||||||
/// The overscroll indicator will apply a stretch effect to this child. This
|
/// The overscroll indicator will apply a stretch effect to this child. This
|
||||||
@ -806,7 +813,8 @@ class _StretchingOverscrollIndicatorState extends State<StretchingOverscrollIndi
|
|||||||
// screen, overflow from transforming the viewport is irrelevant.
|
// screen, overflow from transforming the viewport is irrelevant.
|
||||||
return ClipRect(
|
return ClipRect(
|
||||||
clipBehavior: stretch != 0.0 && viewportDimension != mainAxisSize
|
clipBehavior: stretch != 0.0 && viewportDimension != mainAxisSize
|
||||||
? Clip.hardEdge : Clip.none,
|
? widget.clipBehavior
|
||||||
|
: Clip.none,
|
||||||
child: transform,
|
child: transform,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -425,6 +425,7 @@ abstract class ScrollView extends StatelessWidget {
|
|||||||
viewportBuilder: (BuildContext context, ViewportOffset offset) {
|
viewportBuilder: (BuildContext context, ViewportOffset offset) {
|
||||||
return buildViewport(context, offset, axisDirection, slivers);
|
return buildViewport(context, offset, axisDirection, slivers);
|
||||||
},
|
},
|
||||||
|
clipBehavior: clipBehavior,
|
||||||
);
|
);
|
||||||
|
|
||||||
final Widget scrollableResult = effectivePrimary && scrollController != null
|
final Widget scrollableResult = effectivePrimary && scrollController != null
|
||||||
|
@ -96,6 +96,7 @@ class Scrollable extends StatefulWidget {
|
|||||||
this.dragStartBehavior = DragStartBehavior.start,
|
this.dragStartBehavior = DragStartBehavior.start,
|
||||||
this.restorationId,
|
this.restorationId,
|
||||||
this.scrollBehavior,
|
this.scrollBehavior,
|
||||||
|
this.clipBehavior = Clip.hardEdge,
|
||||||
}) : assert(axisDirection != null),
|
}) : assert(axisDirection != null),
|
||||||
assert(dragStartBehavior != null),
|
assert(dragStartBehavior != null),
|
||||||
assert(viewportBuilder != null),
|
assert(viewportBuilder != null),
|
||||||
@ -260,6 +261,15 @@ class Scrollable extends StatefulWidget {
|
|||||||
/// [ScrollBehavior].
|
/// [ScrollBehavior].
|
||||||
final ScrollBehavior? scrollBehavior;
|
final ScrollBehavior? scrollBehavior;
|
||||||
|
|
||||||
|
/// {@macro flutter.material.Material.clipBehavior}
|
||||||
|
///
|
||||||
|
/// Defaults to [Clip.hardEdge].
|
||||||
|
///
|
||||||
|
/// This is passed to decorators in [ScrollableDetails], and does not directly affect
|
||||||
|
/// clipping of the [Scrollable]. This reflects the same [Clip] that is provided
|
||||||
|
/// to [ScrollView.clipBehavior] and is supplied to the [Viewport].
|
||||||
|
final Clip clipBehavior;
|
||||||
|
|
||||||
/// The axis along which the scroll view scrolls.
|
/// The axis along which the scroll view scrolls.
|
||||||
///
|
///
|
||||||
/// Determined by the [axisDirection].
|
/// Determined by the [axisDirection].
|
||||||
@ -796,6 +806,7 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
|
|||||||
final ScrollableDetails details = ScrollableDetails(
|
final ScrollableDetails details = ScrollableDetails(
|
||||||
direction: widget.axisDirection,
|
direction: widget.axisDirection,
|
||||||
controller: _effectiveScrollController,
|
controller: _effectiveScrollController,
|
||||||
|
clipBehavior: widget.clipBehavior,
|
||||||
);
|
);
|
||||||
|
|
||||||
result = _configuration.buildScrollbar(
|
result = _configuration.buildScrollbar(
|
||||||
@ -811,7 +822,7 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
|
|||||||
state: this,
|
state: this,
|
||||||
position: position,
|
position: position,
|
||||||
registrar: registrar,
|
registrar: registrar,
|
||||||
child: result
|
child: result,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1312,6 +1323,7 @@ class ScrollableDetails {
|
|||||||
const ScrollableDetails({
|
const ScrollableDetails({
|
||||||
required this.direction,
|
required this.direction,
|
||||||
required this.controller,
|
required this.controller,
|
||||||
|
this.clipBehavior,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The direction in which this widget scrolls.
|
/// The direction in which this widget scrolls.
|
||||||
@ -1325,6 +1337,13 @@ class ScrollableDetails {
|
|||||||
/// This can be used by [ScrollBehavior] to apply a [Scrollbar] to the associated
|
/// This can be used by [ScrollBehavior] to apply a [Scrollbar] to the associated
|
||||||
/// [Scrollable].
|
/// [Scrollable].
|
||||||
final ScrollController controller;
|
final ScrollController controller;
|
||||||
|
|
||||||
|
/// {@macro flutter.material.Material.clipBehavior}
|
||||||
|
///
|
||||||
|
/// This can be used by [MaterialScrollBehavior] to clip [StretchingOverscrollIndicator].
|
||||||
|
///
|
||||||
|
/// Defaults to null.
|
||||||
|
final Clip? clipBehavior;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// With [_ScrollSemantics] certain child [SemanticsNode]s can be
|
/// With [_ScrollSemantics] certain child [SemanticsNode]s can be
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
@ -1265,6 +1266,83 @@ void main() {
|
|||||||
expect(find.byType(GlowingOverscrollIndicator), findsNothing);
|
expect(find.byType(GlowingOverscrollIndicator), findsNothing);
|
||||||
}, variant: TargetPlatformVariant.only(TargetPlatform.android));
|
}, variant: TargetPlatformVariant.only(TargetPlatform.android));
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'ListView clip behavior updates overscroll indicator clip behavior', (WidgetTester tester) async {
|
||||||
|
Widget buildFrame(Clip clipBehavior) {
|
||||||
|
return MaterialApp(
|
||||||
|
theme: ThemeData(useMaterial3: true),
|
||||||
|
home: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
SizedBox(
|
||||||
|
height: 300,
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: 20,
|
||||||
|
clipBehavior: clipBehavior,
|
||||||
|
itemBuilder: (BuildContext context, int index){
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(10.0),
|
||||||
|
child: Text('Index $index'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Opacity(
|
||||||
|
opacity: 0.5,
|
||||||
|
child: Container(
|
||||||
|
color: const Color(0xD0FF0000),
|
||||||
|
height: 100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test default clip behavior.
|
||||||
|
await tester.pumpWidget(buildFrame(Clip.hardEdge));
|
||||||
|
|
||||||
|
expect(find.byType(StretchingOverscrollIndicator), findsOneWidget);
|
||||||
|
expect(find.byType(GlowingOverscrollIndicator), findsNothing);
|
||||||
|
expect(find.text('Index 1'), findsOneWidget);
|
||||||
|
|
||||||
|
RenderClipRect renderClip = tester.allRenderObjects.whereType<RenderClipRect>().first;
|
||||||
|
// Currently not clipping
|
||||||
|
expect(renderClip.clipBehavior, equals(Clip.none));
|
||||||
|
|
||||||
|
TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Index 1')));
|
||||||
|
// Overscroll the start.
|
||||||
|
await gesture.moveBy(const Offset(0.0, 200.0));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Index 1'), findsOneWidget);
|
||||||
|
expect(tester.getCenter(find.text('Index 1')).dy, greaterThan(0));
|
||||||
|
renderClip = tester.allRenderObjects.whereType<RenderClipRect>().first;
|
||||||
|
// Now clipping
|
||||||
|
expect(renderClip.clipBehavior, equals(Clip.hardEdge));
|
||||||
|
|
||||||
|
await gesture.up();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Test custom clip behavior.
|
||||||
|
await tester.pumpWidget(buildFrame(Clip.none));
|
||||||
|
|
||||||
|
renderClip = tester.allRenderObjects.whereType<RenderClipRect>().first;
|
||||||
|
// Currently not clipping
|
||||||
|
expect(renderClip.clipBehavior, equals(Clip.none));
|
||||||
|
|
||||||
|
gesture = await tester.startGesture(tester.getCenter(find.text('Index 1')));
|
||||||
|
// Overscroll the start.
|
||||||
|
await gesture.moveBy(const Offset(0.0, 200.0));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Index 1'), findsOneWidget);
|
||||||
|
expect(tester.getCenter(find.text('Index 1')).dy, greaterThan(0));
|
||||||
|
renderClip = tester.allRenderObjects.whereType<RenderClipRect>().first;
|
||||||
|
// Now clipping
|
||||||
|
expect(renderClip.clipBehavior, equals(Clip.none));
|
||||||
|
|
||||||
|
await gesture.up();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}, variant: TargetPlatformVariant.only(TargetPlatform.android));
|
||||||
|
|
||||||
testWidgets('When `useInheritedMediaQuery` is true an existing MediaQuery is used if one is available', (WidgetTester tester) async {
|
testWidgets('When `useInheritedMediaQuery` is true an existing MediaQuery is used if one is available', (WidgetTester tester) async {
|
||||||
late BuildContext capturedContext;
|
late BuildContext capturedContext;
|
||||||
final UniqueKey uniqueKey = UniqueKey();
|
final UniqueKey uniqueKey = UniqueKey();
|
||||||
|
@ -454,6 +454,70 @@ void main() {
|
|||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('clipBehavior parameter updates overscroll clipping behavior', (WidgetTester tester) async {
|
||||||
|
// Regression test for https://github.com/flutter/flutter/issues/103491
|
||||||
|
|
||||||
|
Widget buildFrame(Clip clipBehavior) {
|
||||||
|
return Directionality(
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
child: MediaQuery(
|
||||||
|
data: const MediaQueryData(size: Size(800.0, 600.0)),
|
||||||
|
child: ScrollConfiguration(
|
||||||
|
behavior: const ScrollBehavior().copyWith(overscroll: false),
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
StretchingOverscrollIndicator(
|
||||||
|
axisDirection: AxisDirection.down,
|
||||||
|
clipBehavior: clipBehavior,
|
||||||
|
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'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Opacity(
|
||||||
|
opacity: 0.5,
|
||||||
|
child: Container(
|
||||||
|
color: const Color(0xD0FF0000),
|
||||||
|
height: 100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildFrame(Clip.none));
|
||||||
|
|
||||||
|
expect(find.text('Index 1'), findsOneWidget);
|
||||||
|
expect(tester.getCenter(find.text('Index 1')).dy, 51.0);
|
||||||
|
RenderClipRect renderClip = tester.allRenderObjects.whereType<RenderClipRect>().first;
|
||||||
|
// Currently not clipping
|
||||||
|
expect(renderClip.clipBehavior, equals(Clip.none));
|
||||||
|
|
||||||
|
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Index 1')));
|
||||||
|
// Overscroll the start.
|
||||||
|
await gesture.moveBy(const Offset(0.0, 200.0));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Index 1'), findsOneWidget);
|
||||||
|
expect(tester.getCenter(find.text('Index 1')).dy, greaterThan(0));
|
||||||
|
renderClip = tester.allRenderObjects.whereType<RenderClipRect>().first;
|
||||||
|
// Now clipping
|
||||||
|
expect(renderClip.clipBehavior, equals(Clip.none));
|
||||||
|
|
||||||
|
await gesture.up();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('Stretch limit', (WidgetTester tester) async {
|
testWidgets('Stretch limit', (WidgetTester tester) async {
|
||||||
// Regression test for https://github.com/flutter/flutter/issues/99264
|
// Regression test for https://github.com/flutter/flutter/issues/99264
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
|
Loading…
Reference in New Issue
Block a user