Skip to content

Commit 9f4b9bf

Browse files
authored
Apply PrimaryScrollController updates to SingleChildScrollView (flutter#106430)
1 parent 3022db2 commit 9f4b9bf

File tree

4 files changed

+56
-28
lines changed

4 files changed

+56
-28
lines changed

examples/api/lib/widgets/scroll_position/scroll_metrics_notification.0.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class ScrollMetricsDemoState extends State<ScrollMetricsDemo> {
4646
height: windowSize,
4747
width: double.infinity,
4848
child: const SingleChildScrollView(
49+
primary: true,
4950
child: FlutterLogo(
5051
size: 300.0,
5152
),

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

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ class SingleChildScrollView extends StatelessWidget {
142142
this.scrollDirection = Axis.vertical,
143143
this.reverse = false,
144144
this.padding,
145-
bool? primary,
145+
this.primary,
146146
this.physics,
147147
this.controller,
148148
this.child,
@@ -153,11 +153,12 @@ class SingleChildScrollView extends StatelessWidget {
153153
}) : assert(scrollDirection != null),
154154
assert(dragStartBehavior != null),
155155
assert(clipBehavior != null),
156-
assert(!(controller != null && (primary ?? false)),
157-
'Primary ScrollViews obtain their ScrollController via inheritance from a PrimaryScrollController widget. '
158-
'You cannot both set primary to true and pass an explicit controller.',
159-
),
160-
primary = primary ?? controller == null && identical(scrollDirection, Axis.vertical);
156+
assert(
157+
!(controller != null && (primary ?? false)),
158+
'Primary ScrollViews obtain their ScrollController via inheritance '
159+
'from a PrimaryScrollController widget. You cannot both set primary to '
160+
'true and pass an explicit controller.',
161+
);
161162

162163
/// The axis along which the scroll view scrolls.
163164
///
@@ -195,20 +196,8 @@ class SingleChildScrollView extends StatelessWidget {
195196
/// [ScrollController.animateTo]).
196197
final ScrollController? controller;
197198

198-
/// Whether this is the primary scroll view associated with the parent
199-
/// [PrimaryScrollController].
200-
///
201-
/// When true, the scroll view is used for default [ScrollAction]s. If a
202-
/// ScrollAction is not handled by an otherwise focused part of the application,
203-
/// the ScrollAction will be evaluated using this scroll view, for example,
204-
/// when executing [Shortcuts] key events like page up and down.
205-
///
206-
/// On iOS, this identifies the scroll view that will scroll to top in
207-
/// response to a tap in the status bar.
208-
///
209-
/// Defaults to true when [scrollDirection] is vertical and [controller] is
210-
/// not specified.
211-
final bool primary;
199+
/// {@macro flutter.widgets.scroll_view.primary}
200+
final bool? primary;
212201

213202
/// How the scroll view should respond to user input.
214203
///
@@ -248,9 +237,13 @@ class SingleChildScrollView extends StatelessWidget {
248237
if (padding != null) {
249238
contents = Padding(padding: padding!, child: contents);
250239
}
251-
final ScrollController? scrollController = primary
240+
final bool effectivePrimary = primary
241+
?? controller == null && PrimaryScrollController.shouldInherit(context, scrollDirection);
242+
243+
final ScrollController? scrollController = effectivePrimary
252244
? PrimaryScrollController.of(context)
253245
: controller;
246+
254247
Widget scrollable = Scrollable(
255248
dragStartBehavior: dragStartBehavior,
256249
axisDirection: axisDirection,
@@ -280,7 +273,9 @@ class SingleChildScrollView extends StatelessWidget {
280273
);
281274
}
282275

283-
return primary && scrollController != null
276+
return effectivePrimary && scrollController != null
277+
// Further descendant ScrollViews will not inherit the same
278+
// PrimaryScrollController
284279
? PrimaryScrollController.none(child: scrollable)
285280
: scrollable;
286281
}

packages/flutter/test/widgets/scrollbar_test.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,6 +1079,7 @@ void main() {
10791079
isAlwaysShown: true,
10801080
controller: scrollController,
10811081
child: const SingleChildScrollView(
1082+
primary: true,
10821083
child: SizedBox(width: 4000.0, height: 4000.0),
10831084
),
10841085
),

packages/flutter/test/widgets/single_child_scroll_view_test.dart

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,19 @@ class TestScrollController extends ScrollController {
3434
}
3535
}
3636

37+
Widget primaryScrollControllerBoilerplate({ required Widget child, required ScrollController controller }) {
38+
return Directionality(
39+
textDirection: TextDirection.ltr,
40+
child: MediaQuery(
41+
data: const MediaQueryData(),
42+
child: PrimaryScrollController(
43+
controller: controller,
44+
child: child,
45+
),
46+
),
47+
);
48+
}
49+
3750
void main() {
3851
testWidgets('SingleChildScrollView overflow and clipRect test', (WidgetTester tester) async {
3952
// the test widowSize is Size(800.0, 600.0)
@@ -210,23 +223,41 @@ void main() {
210223
));
211224
});
212225

213-
testWidgets('Vertical SingleChildScrollViews are primary by default', (WidgetTester tester) async {
226+
testWidgets('Vertical SingleChildScrollViews are not primary by default', (WidgetTester tester) async {
214227
const SingleChildScrollView view = SingleChildScrollView();
215-
expect(view.primary, isTrue);
228+
expect(view.primary, isNull);
216229
});
217230

218-
testWidgets('Horizontal SingleChildScrollViews are non-primary by default', (WidgetTester tester) async {
231+
testWidgets('Horizontal SingleChildScrollViews are not primary by default', (WidgetTester tester) async {
219232
const SingleChildScrollView view = SingleChildScrollView(scrollDirection: Axis.horizontal);
220-
expect(view.primary, isFalse);
233+
expect(view.primary, isNull);
221234
});
222235

223-
testWidgets('SingleChildScrollViews with controllers are non-primary by default', (WidgetTester tester) async {
236+
testWidgets('SingleChildScrollViews with controllers are not primary by default', (WidgetTester tester) async {
224237
final SingleChildScrollView view = SingleChildScrollView(
225238
controller: ScrollController(),
226239
);
227-
expect(view.primary, isFalse);
240+
expect(view.primary, isNull);
228241
});
229242

243+
testWidgets('Vertical SingleChildScrollViews use PrimaryScrollController by default on mobile', (WidgetTester tester) async {
244+
final ScrollController controller = ScrollController();
245+
await tester.pumpWidget(primaryScrollControllerBoilerplate(
246+
child: const SingleChildScrollView(),
247+
controller: controller,
248+
));
249+
expect(controller.hasClients, isTrue);
250+
}, variant: TargetPlatformVariant.mobile());
251+
252+
testWidgets("Vertical SingleChildScrollViews don't use PrimaryScrollController by default on desktop", (WidgetTester tester) async {
253+
final ScrollController controller = ScrollController();
254+
await tester.pumpWidget(primaryScrollControllerBoilerplate(
255+
child: const SingleChildScrollView(),
256+
controller: controller,
257+
));
258+
expect(controller.hasClients, isFalse);
259+
}, variant: TargetPlatformVariant.desktop());
260+
230261
testWidgets('Nested scrollables have a null PrimaryScrollController', (WidgetTester tester) async {
231262
const Key innerKey = Key('inner');
232263
final ScrollController primaryScrollController = ScrollController();

0 commit comments

Comments
 (0)