Skip to content

Commit 784520b

Browse files
authored
Updating PrimaryScrollController for Desktop (#102099)
1 parent b73be72 commit 784520b

31 files changed

+497
-87
lines changed

dev/integration_tests/flutter_gallery/lib/demo/colors_demo.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ class PaletteTabView extends StatelessWidget {
9696
final TextStyle blackTextStyle = textTheme.bodyText2!.copyWith(color: Colors.black);
9797
return Scrollbar(
9898
child: ListView(
99+
primary: true,
99100
itemExtent: kColorItemHeight,
100101
children: <Widget>[
101102
...primaryKeys.map<Widget>((int index) {

dev/integration_tests/flutter_gallery/lib/demo/cupertino/cupertino_alert_demo.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ class _CupertinoAlertDemoState extends State<CupertinoAlertDemo> {
6060
children: <Widget>[
6161
CupertinoScrollbar(
6262
child: ListView(
63+
primary: true,
6364
// Add more padding to the normal safe area.
6465
padding: const EdgeInsets.symmetric(vertical: 24.0, horizontal: 72.0)
6566
+ MediaQuery.of(context).padding,

dev/integration_tests/flutter_gallery/lib/demo/cupertino/cupertino_navigation_demo.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,7 @@ class CupertinoDemoTab2 extends StatelessWidget {
446446
),
447447
child: CupertinoScrollbar(
448448
child: ListView(
449+
primary: true,
449450
children: <Widget>[
450451
const CupertinoUserInterfaceLevel(
451452
data: CupertinoUserInterfaceLevelData.elevated,

dev/integration_tests/flutter_gallery/lib/demo/cupertino/cupertino_text_field_demo.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ class _CupertinoTextFieldDemoState extends State<CupertinoTextFieldDemo> {
168168
),
169169
child: CupertinoScrollbar(
170170
child: ListView(
171+
primary: true,
171172
children: <Widget>[
172173
Padding(
173174
padding: const EdgeInsets.symmetric(vertical: 32.0, horizontal: 16.0),

dev/integration_tests/flutter_gallery/lib/demo/material/backdrop_demo.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ class CategoryView extends StatelessWidget {
104104
final ThemeData theme = Theme.of(context);
105105
return Scrollbar(
106106
child: ListView(
107+
primary: true,
107108
key: PageStorageKey<Category?>(category),
108109
padding: const EdgeInsets.symmetric(
109110
vertical: 16.0,

dev/integration_tests/flutter_gallery/lib/demo/material/bottom_app_bar_demo.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ class _BottomAppBarDemoState extends State<BottomAppBarDemo> {
161161
),
162162
body: Scrollbar(
163163
child: ListView(
164+
primary: true,
164165
padding: const EdgeInsets.only(bottom: 88.0),
165166
children: <Widget>[
166167
const _Heading('FAB Shape'),

dev/integration_tests/flutter_gallery/lib/demo/material/cards_demo.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,7 @@ class _CardsDemoState extends State<CardsDemo> {
375375
),
376376
body: Scrollbar(
377377
child: ListView(
378+
primary: true,
378379
padding: const EdgeInsets.only(top: 8.0, left: 8.0, right: 8.0),
379380
children: destinations.map<Widget>((TravelDestination destination) {
380381
Widget? child;

dev/integration_tests/flutter_gallery/lib/demo/material/chip_demo.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,12 @@ class _ChipDemoState extends State<ChipDemo> {
347347
borderRadius: BorderRadius.circular(10.0),
348348
))
349349
: theme.chipTheme,
350-
child: Scrollbar(child: ListView(children: tiles)),
350+
child: Scrollbar(
351+
child: ListView(
352+
primary: true,
353+
children: tiles,
354+
)
355+
),
351356
),
352357
floatingActionButton: FloatingActionButton(
353358
onPressed: () => setState(_reset),

dev/integration_tests/flutter_gallery/lib/demo/material/data_table_demo.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ class _DataTableDemoState extends State<DataTableDemo> {
175175
),
176176
body: Scrollbar(
177177
child: ListView(
178+
primary: true,
178179
padding: const EdgeInsets.all(20.0),
179180
children: <Widget>[
180181
PaginatedDataTable(

dev/integration_tests/flutter_gallery/lib/demo/material/elevation_demo.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,12 @@ class _ElevationDemoState extends State<ElevationDemo> {
6464
),
6565
],
6666
),
67-
body: Scrollbar(child: ListView(children: buildCards())),
67+
body: Scrollbar(
68+
child: ListView(
69+
primary: true,
70+
children: buildCards(),
71+
),
72+
),
6873
);
6974
}
7075
}

dev/integration_tests/flutter_gallery/lib/demo/material/expansion_tile_list_demo.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class ExpansionTileListDemo extends StatelessWidget {
2020
),
2121
body: Scrollbar(
2222
child: ListView(
23+
primary: true,
2324
children: <Widget>[
2425
const ListTile(title: Text('Top')),
2526
ExpansionTile(

dev/integration_tests/flutter_gallery/lib/demo/material/full_screen_dialog_demo.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ class FullScreenDialogDemoState extends State<FullScreenDialogDemo> {
162162
onWillPop: _onWillPop,
163163
child: Scrollbar(
164164
child: ListView(
165+
primary: true,
165166
padding: const EdgeInsets.all(16.0),
166167
children: <Widget>[
167168
Container(

dev/integration_tests/flutter_gallery/lib/demo/material/icons_demo.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ class IconsDemoState extends State<IconsDemo> {
6262
bottom: false,
6363
child: Scrollbar(
6464
child: ListView(
65+
primary: true,
6566
padding: const EdgeInsets.all(24.0),
6667
children: <Widget>[
6768
_IconsDemoCard(handleIconButtonPress, Icons.face), // direction-agnostic icon

dev/integration_tests/flutter_gallery/lib/demo/material/leave_behind_demo.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ class LeaveBehindDemoState extends State<LeaveBehindDemo> {
131131
} else {
132132
body = Scrollbar(
133133
child: ListView(
134+
primary: true,
134135
children: leaveBehindItems.map<Widget>((LeaveBehindItem item) {
135136
return _LeaveBehindListItem(
136137
confirmDismiss: _confirmDismiss,

dev/integration_tests/flutter_gallery/lib/demo/material/list_demo.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ class _ListDemoState extends State<ListDemo> {
256256
),
257257
body: Scrollbar(
258258
child: ListView(
259+
primary: true,
259260
padding: EdgeInsets.symmetric(vertical: _dense != null ? 4.0 : 8.0),
260261
children: listTiles.toList(),
261262
),

dev/integration_tests/flutter_gallery/lib/demo/material/overscroll_demo.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ class OverscrollDemoState extends State<OverscrollDemo> {
6464
onRefresh: _handleRefresh,
6565
child: Scrollbar(
6666
child: ListView.builder(
67+
primary: true,
6768
padding: kMaterialListPadding,
6869
itemCount: _items.length,
6970
itemBuilder: (BuildContext context, int index) {

dev/integration_tests/flutter_gallery/lib/demo/material/reorderable_list_demo.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ class _ListDemoState extends State<ReorderableListDemo> {
208208
),
209209
body: Scrollbar(
210210
child: ReorderableListView(
211+
primary: true,
211212
header: _itemType != _ReorderableListType.threeLine
212213
? Padding(
213214
padding: const EdgeInsets.all(8.0),

dev/integration_tests/flutter_gallery/lib/demo/material/text_form_field_demo.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ class TextFormFieldDemoState extends State<TextFormFieldDemo> {
182182
onWillPop: _warnUserAboutInvalidData,
183183
child: Scrollbar(
184184
child: SingleChildScrollView(
185+
primary: true,
185186
dragStartBehavior: DragStartBehavior.down,
186187
padding: const EdgeInsets.symmetric(horizontal: 16.0),
187188
child: Column(

dev/integration_tests/flutter_gallery/lib/demo/typography_demo.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,12 @@ class TypographyDemo extends StatelessWidget {
6666
body: SafeArea(
6767
top: false,
6868
bottom: false,
69-
child: Scrollbar(child: ListView(children: styleItems)),
69+
child: Scrollbar(
70+
child: ListView(
71+
primary: true,
72+
children: styleItems,
73+
),
74+
),
7075
),
7176
);
7277
}

dev/integration_tests/flutter_gallery/lib/demo/video_demo.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,7 @@ class _VideoDemoState extends State<VideoDemo> with SingleTickerProviderStateMix
418418
connectedCompleter: connectedCompleter,
419419
child: Scrollbar(
420420
child: ListView(
421+
primary: true,
421422
children: <Widget>[
422423
VideoCard(
423424
title: 'Butterfly',

examples/api/lib/widgets/scrollbar/raw_scrollbar.0.dart

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@ class _MyStatefulWidgetState extends State<MyStatefulWidget> {
4545
children: <Widget>[
4646
SizedBox(
4747
width: constraints.maxWidth / 2,
48-
// Only one scroll position can be attached to the
49-
// PrimaryScrollController if using Scrollbars. Providing a
48+
// When using the PrimaryScrollController and a Scrollbar
49+
// together, only one ScrollPosition can be attached to the
50+
// PrimaryScrollController at a time. Providing a
5051
// unique scroll controller to this scroll view prevents it
5152
// from attaching to the PrimaryScrollController.
5253
child: Scrollbar(
@@ -64,12 +65,15 @@ class _MyStatefulWidgetState extends State<MyStatefulWidget> {
6465
)),
6566
SizedBox(
6667
width: constraints.maxWidth / 2,
67-
// This vertical scroll view has not been provided a
68-
// ScrollController, so it is using the
69-
// PrimaryScrollController.
68+
// This vertical scroll view has primary set to true, so it is
69+
// using the PrimaryScrollController. On mobile platforms, the
70+
// PrimaryScrollController automatically attaches to vertical
71+
// ScrollViews, unlike on Desktop platforms, where the primary
72+
// parameter is required.
7073
child: Scrollbar(
7174
thumbVisibility: true,
7275
child: ListView.builder(
76+
primary: true,
7377
itemCount: 100,
7478
itemBuilder: (BuildContext context, int index) {
7579
return Container(

examples/api/lib/widgets/scrollbar/raw_scrollbar.shape.0.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ class MyStatelessWidget extends StatelessWidget {
3535
thumbColor: Colors.blue,
3636
thumbVisibility: true,
3737
child: ListView(
38+
// On mobile platforms, setting primary to true is not required, as
39+
// the PrimaryScrollController automatically attaches to vertical
40+
// ScrollPositions. On desktop platforms however, using the
41+
// PrimaryScrollController requires ScrollView.primary be set.
42+
primary: true,
3843
physics: const BouncingScrollPhysics(),
3944
children: List<Text>.generate(
4045
100, (int index) => Text((index * index).toString())),

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,8 @@ class _DropdownMenuState<T> extends State<_DropdownMenu<T>> {
309309
child: Scrollbar(
310310
thumbVisibility: true,
311311
child: ListView(
312+
// Ensure this always inherits the PrimaryScrollController
313+
primary: true,
312314
padding: kMaterialListPadding,
313315
shrinkWrap: true,
314316
children: children,

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,19 @@ class NestedScrollView extends StatefulWidget {
312312
return <Widget>[
313313
...headerSliverBuilder(context, bodyIsScrolled),
314314
SliverFillRemaining(
315+
// The inner (body) scroll view must use this scroll controller so that
316+
// the independent scroll positions can be kept in sync.
315317
child: PrimaryScrollController(
318+
// The inner scroll view should always inherit this
319+
// PrimaryScrollController, on every platform.
320+
automaticallyInheritForPlatforms: TargetPlatform.values.toSet(),
321+
// `PrimaryScrollController.scrollDirection` is not set, and so it is
322+
// restricted to the default Axis.vertical.
323+
// Ideally the inner and outer views would have the same
324+
// scroll direction, and so we could assume
325+
// `NestedScrollView.scrollDirection` for the PrimaryScrollController,
326+
// but use cases already exist where the axes are mismatched.
327+
// https://github.com/flutter/flutter/issues/102001
316328
controller: innerController,
317329
child: body,
318330
),

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

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,33 @@
33
// found in the LICENSE file.
44

55
import 'package:flutter/foundation.dart';
6+
import 'package:flutter/painting.dart';
67

78
import 'framework.dart';
9+
import 'scroll_configuration.dart';
810
import 'scroll_controller.dart';
911

12+
const Set<TargetPlatform> _kMobilePlatforms = <TargetPlatform>{
13+
TargetPlatform.android,
14+
TargetPlatform.iOS,
15+
TargetPlatform.fuchsia,
16+
};
17+
1018
/// Associates a [ScrollController] with a subtree.
1119
///
12-
/// When a [ScrollView] has [ScrollView.primary] set to true and is not given
13-
/// an explicit [ScrollController], the [ScrollView] uses [of] to find the
14-
/// [ScrollController] associated with its subtree.
20+
/// When a [ScrollView] has [ScrollView.primary] set to true, the [ScrollView]
21+
/// uses [of] to inherit the [PrimaryScrollController] associated with its
22+
/// subtree.
23+
///
24+
/// A ScrollView that doesn't have a controller or the primary flag set will
25+
/// inherit the PrimarySCrollController, if [shouldInherit] allows it. By
26+
/// default [shouldInherit] is true for mobile platforms when the ScrollView has
27+
/// a scroll direction of [Axis.vertical]. This automatic inheritance can be
28+
/// configured with [automaticallyInheritForPlatforms] and [scrollDirection].
1529
///
16-
/// This mechanism can be used to provide default behavior for scroll views in a
17-
/// subtree. For example, the [Scaffold] uses this mechanism to implement the
18-
/// scroll-to-top gesture on iOS.
30+
/// Inheriting this ScrollController can provide default behavior for scroll
31+
/// views in a subtree. For example, the [Scaffold] uses this mechanism to
32+
/// implement the scroll-to-top gesture on iOS.
1933
///
2034
/// Another default behavior handled by the PrimaryScrollController is default
2135
/// [ScrollAction]s. If a ScrollAction is not handled by an otherwise focused
@@ -34,14 +48,18 @@ class PrimaryScrollController extends InheritedWidget {
3448
const PrimaryScrollController({
3549
super.key,
3650
required ScrollController this.controller,
51+
this.automaticallyInheritForPlatforms = _kMobilePlatforms,
52+
this.scrollDirection = Axis.vertical,
3753
required super.child,
3854
}) : assert(controller != null);
3955

4056
/// Creates a subtree without an associated [ScrollController].
4157
const PrimaryScrollController.none({
4258
super.key,
4359
required super.child,
44-
}) : controller = null;
60+
}) : automaticallyInheritForPlatforms = const <TargetPlatform>{},
61+
scrollDirection = null,
62+
controller = null;
4563

4664
/// The [ScrollController] associated with the subtree.
4765
///
@@ -51,6 +69,57 @@ class PrimaryScrollController extends InheritedWidget {
5169
/// scroll controller.
5270
final ScrollController? controller;
5371

72+
/// The [Axis] this controller is configured for [ScrollView]s to
73+
/// automatically inherit.
74+
///
75+
/// Used in conjunction with [automaticallyInheritForPlatforms]. If the
76+
/// current [TargetPlatform] is not included in
77+
/// [automaticallyInheritForPlatforms], this is ignored.
78+
///
79+
/// When null, no [ScrollView] in any Axis will automatically inherit this
80+
/// controller. This is dissimilar to [PrimaryScrollController.none]. When a
81+
/// PrimaryScrollController is inherited, ScrollView will insert
82+
/// PrimaryScrollController.none into the tree to prevent further descendant
83+
/// ScrollViews from inheriting the current PrimaryScrollController.
84+
///
85+
/// Defaults to [Axis.vertical].
86+
final Axis? scrollDirection;
87+
88+
/// The [TargetPlatform]s this controller is configured for [ScrollView]s to
89+
/// automatically inherit.
90+
///
91+
/// Used in conjunction with [scrollDirection]. If the [Axis] provided to
92+
/// [shouldInherit] is not [scrollDirection], this is ignored.
93+
///
94+
/// When empty, no ScrollView in any Axis will automatically inherit this
95+
/// controller. Defaults to [TargetPlatformVariant.mobile].
96+
final Set<TargetPlatform> automaticallyInheritForPlatforms;
97+
98+
/// Returns true if this PrimaryScrollController is configured to be
99+
/// automatically inherited for the current [TargetPlatform] and the given
100+
/// [Axis].
101+
///
102+
/// This method is typically not called directly. [ScrollView] will call this
103+
/// method if it has not been provided a [ScrollController] and
104+
/// [ScrollView.primary] is unset.
105+
///
106+
/// If a ScrollController has already been provided to
107+
/// [ScrollView.controller], or [ScrollView.primary] is set, this is method is
108+
/// not called by ScrollView as it will have determined whether or not to
109+
/// inherit the PrimaryScrollController.
110+
static bool shouldInherit(BuildContext context, Axis scrollDirection) {
111+
final PrimaryScrollController? result = context.findAncestorWidgetOfExactType<PrimaryScrollController>();
112+
if (result == null) {
113+
return false;
114+
}
115+
116+
final TargetPlatform platform = ScrollConfiguration.of(context).getPlatform(context);
117+
if (result.automaticallyInheritForPlatforms.contains(platform)) {
118+
return result.scrollDirection == scrollDirection;
119+
}
120+
return false;
121+
}
122+
54123
/// Returns the [ScrollController] most closely associated with the given
55124
/// context.
56125
///

0 commit comments

Comments
 (0)