Unverified Commit 784520bd authored by Kate Lovett's avatar Kate Lovett Committed by GitHub

Updating PrimaryScrollController for Desktop (#102099)

parent b73be72d
......@@ -96,6 +96,7 @@ class PaletteTabView extends StatelessWidget {
final TextStyle blackTextStyle = textTheme.bodyText2!.copyWith(color: Colors.black);
return Scrollbar(
child: ListView(
primary: true,
itemExtent: kColorItemHeight,
children: <Widget>[
...primaryKeys.map<Widget>((int index) {
......
......@@ -60,6 +60,7 @@ class _CupertinoAlertDemoState extends State<CupertinoAlertDemo> {
children: <Widget>[
CupertinoScrollbar(
child: ListView(
primary: true,
// Add more padding to the normal safe area.
padding: const EdgeInsets.symmetric(vertical: 24.0, horizontal: 72.0)
+ MediaQuery.of(context).padding,
......
......@@ -446,6 +446,7 @@ class CupertinoDemoTab2 extends StatelessWidget {
),
child: CupertinoScrollbar(
child: ListView(
primary: true,
children: <Widget>[
const CupertinoUserInterfaceLevel(
data: CupertinoUserInterfaceLevelData.elevated,
......
......@@ -168,6 +168,7 @@ class _CupertinoTextFieldDemoState extends State<CupertinoTextFieldDemo> {
),
child: CupertinoScrollbar(
child: ListView(
primary: true,
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(vertical: 32.0, horizontal: 16.0),
......
......@@ -104,6 +104,7 @@ class CategoryView extends StatelessWidget {
final ThemeData theme = Theme.of(context);
return Scrollbar(
child: ListView(
primary: true,
key: PageStorageKey<Category?>(category),
padding: const EdgeInsets.symmetric(
vertical: 16.0,
......
......@@ -161,6 +161,7 @@ class _BottomAppBarDemoState extends State<BottomAppBarDemo> {
),
body: Scrollbar(
child: ListView(
primary: true,
padding: const EdgeInsets.only(bottom: 88.0),
children: <Widget>[
const _Heading('FAB Shape'),
......
......@@ -375,6 +375,7 @@ class _CardsDemoState extends State<CardsDemo> {
),
body: Scrollbar(
child: ListView(
primary: true,
padding: const EdgeInsets.only(top: 8.0, left: 8.0, right: 8.0),
children: destinations.map<Widget>((TravelDestination destination) {
Widget? child;
......
......@@ -347,7 +347,12 @@ class _ChipDemoState extends State<ChipDemo> {
borderRadius: BorderRadius.circular(10.0),
))
: theme.chipTheme,
child: Scrollbar(child: ListView(children: tiles)),
child: Scrollbar(
child: ListView(
primary: true,
children: tiles,
)
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => setState(_reset),
......
......@@ -175,6 +175,7 @@ class _DataTableDemoState extends State<DataTableDemo> {
),
body: Scrollbar(
child: ListView(
primary: true,
padding: const EdgeInsets.all(20.0),
children: <Widget>[
PaginatedDataTable(
......
......@@ -64,7 +64,12 @@ class _ElevationDemoState extends State<ElevationDemo> {
),
],
),
body: Scrollbar(child: ListView(children: buildCards())),
body: Scrollbar(
child: ListView(
primary: true,
children: buildCards(),
),
),
);
}
}
......@@ -20,6 +20,7 @@ class ExpansionTileListDemo extends StatelessWidget {
),
body: Scrollbar(
child: ListView(
primary: true,
children: <Widget>[
const ListTile(title: Text('Top')),
ExpansionTile(
......
......@@ -162,6 +162,7 @@ class FullScreenDialogDemoState extends State<FullScreenDialogDemo> {
onWillPop: _onWillPop,
child: Scrollbar(
child: ListView(
primary: true,
padding: const EdgeInsets.all(16.0),
children: <Widget>[
Container(
......
......@@ -62,6 +62,7 @@ class IconsDemoState extends State<IconsDemo> {
bottom: false,
child: Scrollbar(
child: ListView(
primary: true,
padding: const EdgeInsets.all(24.0),
children: <Widget>[
_IconsDemoCard(handleIconButtonPress, Icons.face), // direction-agnostic icon
......
......@@ -131,6 +131,7 @@ class LeaveBehindDemoState extends State<LeaveBehindDemo> {
} else {
body = Scrollbar(
child: ListView(
primary: true,
children: leaveBehindItems.map<Widget>((LeaveBehindItem item) {
return _LeaveBehindListItem(
confirmDismiss: _confirmDismiss,
......
......@@ -256,6 +256,7 @@ class _ListDemoState extends State<ListDemo> {
),
body: Scrollbar(
child: ListView(
primary: true,
padding: EdgeInsets.symmetric(vertical: _dense != null ? 4.0 : 8.0),
children: listTiles.toList(),
),
......
......@@ -64,6 +64,7 @@ class OverscrollDemoState extends State<OverscrollDemo> {
onRefresh: _handleRefresh,
child: Scrollbar(
child: ListView.builder(
primary: true,
padding: kMaterialListPadding,
itemCount: _items.length,
itemBuilder: (BuildContext context, int index) {
......
......@@ -208,6 +208,7 @@ class _ListDemoState extends State<ReorderableListDemo> {
),
body: Scrollbar(
child: ReorderableListView(
primary: true,
header: _itemType != _ReorderableListType.threeLine
? Padding(
padding: const EdgeInsets.all(8.0),
......
......@@ -182,6 +182,7 @@ class TextFormFieldDemoState extends State<TextFormFieldDemo> {
onWillPop: _warnUserAboutInvalidData,
child: Scrollbar(
child: SingleChildScrollView(
primary: true,
dragStartBehavior: DragStartBehavior.down,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
......
......@@ -66,7 +66,12 @@ class TypographyDemo extends StatelessWidget {
body: SafeArea(
top: false,
bottom: false,
child: Scrollbar(child: ListView(children: styleItems)),
child: Scrollbar(
child: ListView(
primary: true,
children: styleItems,
),
),
),
);
}
......
......@@ -418,6 +418,7 @@ class _VideoDemoState extends State<VideoDemo> with SingleTickerProviderStateMix
connectedCompleter: connectedCompleter,
child: Scrollbar(
child: ListView(
primary: true,
children: <Widget>[
VideoCard(
title: 'Butterfly',
......
......@@ -45,8 +45,9 @@ class _MyStatefulWidgetState extends State<MyStatefulWidget> {
children: <Widget>[
SizedBox(
width: constraints.maxWidth / 2,
// Only one scroll position can be attached to the
// PrimaryScrollController if using Scrollbars. Providing a
// When using the PrimaryScrollController and a Scrollbar
// together, only one ScrollPosition can be attached to the
// PrimaryScrollController at a time. Providing a
// unique scroll controller to this scroll view prevents it
// from attaching to the PrimaryScrollController.
child: Scrollbar(
......@@ -64,12 +65,15 @@ class _MyStatefulWidgetState extends State<MyStatefulWidget> {
)),
SizedBox(
width: constraints.maxWidth / 2,
// This vertical scroll view has not been provided a
// ScrollController, so it is using the
// PrimaryScrollController.
// This vertical scroll view has primary set to true, so it is
// using the PrimaryScrollController. On mobile platforms, the
// PrimaryScrollController automatically attaches to vertical
// ScrollViews, unlike on Desktop platforms, where the primary
// parameter is required.
child: Scrollbar(
thumbVisibility: true,
child: ListView.builder(
primary: true,
itemCount: 100,
itemBuilder: (BuildContext context, int index) {
return Container(
......
......@@ -35,6 +35,11 @@ class MyStatelessWidget extends StatelessWidget {
thumbColor: Colors.blue,
thumbVisibility: true,
child: ListView(
// On mobile platforms, setting primary to true is not required, as
// the PrimaryScrollController automatically attaches to vertical
// ScrollPositions. On desktop platforms however, using the
// PrimaryScrollController requires ScrollView.primary be set.
primary: true,
physics: const BouncingScrollPhysics(),
children: List<Text>.generate(
100, (int index) => Text((index * index).toString())),
......
......@@ -309,6 +309,8 @@ class _DropdownMenuState<T> extends State<_DropdownMenu<T>> {
child: Scrollbar(
thumbVisibility: true,
child: ListView(
// Ensure this always inherits the PrimaryScrollController
primary: true,
padding: kMaterialListPadding,
shrinkWrap: true,
children: children,
......
......@@ -312,7 +312,19 @@ class NestedScrollView extends StatefulWidget {
return <Widget>[
...headerSliverBuilder(context, bodyIsScrolled),
SliverFillRemaining(
// The inner (body) scroll view must use this scroll controller so that
// the independent scroll positions can be kept in sync.
child: PrimaryScrollController(
// The inner scroll view should always inherit this
// PrimaryScrollController, on every platform.
automaticallyInheritForPlatforms: TargetPlatform.values.toSet(),
// `PrimaryScrollController.scrollDirection` is not set, and so it is
// restricted to the default Axis.vertical.
// Ideally the inner and outer views would have the same
// scroll direction, and so we could assume
// `NestedScrollView.scrollDirection` for the PrimaryScrollController,
// but use cases already exist where the axes are mismatched.
// https://github.com/flutter/flutter/issues/102001
controller: innerController,
child: body,
),
......
......@@ -3,19 +3,33 @@
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'framework.dart';
import 'scroll_configuration.dart';
import 'scroll_controller.dart';
const Set<TargetPlatform> _kMobilePlatforms = <TargetPlatform>{
TargetPlatform.android,
TargetPlatform.iOS,
TargetPlatform.fuchsia,
};
/// Associates a [ScrollController] with a subtree.
///
/// When a [ScrollView] has [ScrollView.primary] set to true and is not given
/// an explicit [ScrollController], the [ScrollView] uses [of] to find the
/// [ScrollController] associated with its subtree.
/// When a [ScrollView] has [ScrollView.primary] set to true, the [ScrollView]
/// uses [of] to inherit the [PrimaryScrollController] associated with its
/// subtree.
///
/// A ScrollView that doesn't have a controller or the primary flag set will
/// inherit the PrimarySCrollController, if [shouldInherit] allows it. By
/// default [shouldInherit] is true for mobile platforms when the ScrollView has
/// a scroll direction of [Axis.vertical]. This automatic inheritance can be
/// configured with [automaticallyInheritForPlatforms] and [scrollDirection].
///
/// This mechanism can be used to provide default behavior for scroll views in a
/// subtree. For example, the [Scaffold] uses this mechanism to implement the
/// scroll-to-top gesture on iOS.
/// Inheriting this ScrollController can provide default behavior for scroll
/// views in a subtree. For example, the [Scaffold] uses this mechanism to
/// implement the scroll-to-top gesture on iOS.
///
/// Another default behavior handled by the PrimaryScrollController is default
/// [ScrollAction]s. If a ScrollAction is not handled by an otherwise focused
......@@ -34,6 +48,8 @@ class PrimaryScrollController extends InheritedWidget {
const PrimaryScrollController({
super.key,
required ScrollController this.controller,
this.automaticallyInheritForPlatforms = _kMobilePlatforms,
this.scrollDirection = Axis.vertical,
required super.child,
}) : assert(controller != null);
......@@ -41,7 +57,9 @@ class PrimaryScrollController extends InheritedWidget {
const PrimaryScrollController.none({
super.key,
required super.child,
}) : controller = null;
}) : automaticallyInheritForPlatforms = const <TargetPlatform>{},
scrollDirection = null,
controller = null;
/// The [ScrollController] associated with the subtree.
///
......@@ -51,6 +69,57 @@ class PrimaryScrollController extends InheritedWidget {
/// scroll controller.
final ScrollController? controller;
/// The [Axis] this controller is configured for [ScrollView]s to
/// automatically inherit.
///
/// Used in conjunction with [automaticallyInheritForPlatforms]. If the
/// current [TargetPlatform] is not included in
/// [automaticallyInheritForPlatforms], this is ignored.
///
/// When null, no [ScrollView] in any Axis will automatically inherit this
/// controller. This is dissimilar to [PrimaryScrollController.none]. When a
/// PrimaryScrollController is inherited, ScrollView will insert
/// PrimaryScrollController.none into the tree to prevent further descendant
/// ScrollViews from inheriting the current PrimaryScrollController.
///
/// Defaults to [Axis.vertical].
final Axis? scrollDirection;
/// The [TargetPlatform]s this controller is configured for [ScrollView]s to
/// automatically inherit.
///
/// Used in conjunction with [scrollDirection]. If the [Axis] provided to
/// [shouldInherit] is not [scrollDirection], this is ignored.
///
/// When empty, no ScrollView in any Axis will automatically inherit this
/// controller. Defaults to [TargetPlatformVariant.mobile].
final Set<TargetPlatform> automaticallyInheritForPlatforms;
/// Returns true if this PrimaryScrollController is configured to be
/// automatically inherited for the current [TargetPlatform] and the given
/// [Axis].
///
/// This method is typically not called directly. [ScrollView] will call this
/// method if it has not been provided a [ScrollController] and
/// [ScrollView.primary] is unset.
///
/// If a ScrollController has already been provided to
/// [ScrollView.controller], or [ScrollView.primary] is set, this is method is
/// not called by ScrollView as it will have determined whether or not to
/// inherit the PrimaryScrollController.
static bool shouldInherit(BuildContext context, Axis scrollDirection) {
final PrimaryScrollController? result = context.findAncestorWidgetOfExactType<PrimaryScrollController>();
if (result == null) {
return false;
}
final TargetPlatform platform = ScrollConfiguration.of(context).getPlatform(context);
if (result.automaticallyInheritForPlatforms.contains(platform)) {
return result.scrollDirection == scrollDirection;
}
return false;
}
/// Returns the [ScrollController] most closely associated with the given
/// context.
///
......
......@@ -87,7 +87,7 @@ abstract class ScrollView extends StatelessWidget {
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.controller,
bool? primary,
this.primary,
ScrollPhysics? physics,
this.scrollBehavior,
this.shrinkWrap = false,
......@@ -104,15 +104,16 @@ abstract class ScrollView extends StatelessWidget {
assert(shrinkWrap != null),
assert(dragStartBehavior != null),
assert(clipBehavior != null),
assert(!(controller != null && (primary ?? false)),
'Primary ScrollViews obtain their ScrollController via inheritance from a PrimaryScrollController widget. '
'You cannot both set primary to true and pass an explicit controller.',
assert(
!(controller != null && (primary ?? false)),
'Primary ScrollViews obtain their ScrollController via inheritance '
'from a PrimaryScrollController widget. You cannot both set primary to '
'true and pass an explicit controller.',
),
assert(!shrinkWrap || center == null),
assert(anchor != null),
assert(anchor >= 0.0 && anchor <= 1.0),
assert(semanticChildCount == null || semanticChildCount >= 0),
primary = primary ?? controller == null && identical(scrollDirection, Axis.vertical),
physics = physics ?? ((primary ?? false) || (primary == null && controller == null && identical(scrollDirection, Axis.vertical)) ? const AlwaysScrollableScrollPhysics() : null);
/// {@template flutter.widgets.scroll_view.scrollDirection}
......@@ -169,11 +170,24 @@ abstract class ScrollView extends StatelessWidget {
///
/// On iOS, this also identifies the scroll view that will scroll to top in
/// response to a tap in the status bar.
/// {@endtemplate}
///
/// Defaults to true when [scrollDirection] is [Axis.vertical] and
/// [controller] is null.
final bool primary;
/// Cannot be true while a [ScrollController] is provided to `controller`,
/// only one ScrollController can be associated with a ScrollView.
///
/// Setting to false will explicitly prevent inheriting any
/// [PrimaryScrollController].
///
/// Defaults to null. When null, and a controller is not provided,
/// [PrimaryScrollController.shouldInherit] is used to decide automatic
/// inheritance.
///
/// By default, the [PrimaryScrollController] that is injected by each
/// [ModalRoute] is configured to automatically be inherited on
/// [TargetPlatformVariant.mobile] for ScrollViews in the [Axis.vertical]
/// scroll direction. Adding another to your app will override the
/// PrimaryScrollController above it.
/// {@endtemplate}
final bool? primary;
/// {@template flutter.widgets.scroll_view.physics}
/// How the scroll view should respond to user input.
......@@ -393,8 +407,13 @@ abstract class ScrollView extends StatelessWidget {
final List<Widget> slivers = buildSlivers(context);
final AxisDirection axisDirection = getDirection(context);
final ScrollController? scrollController =
primary ? PrimaryScrollController.of(context) : controller;
final bool effectivePrimary = primary
?? controller == null && PrimaryScrollController.shouldInherit(context, scrollDirection);
final ScrollController? scrollController = effectivePrimary
? PrimaryScrollController.of(context)
: controller;
final Scrollable scrollable = Scrollable(
dragStartBehavior: dragStartBehavior,
axisDirection: axisDirection,
......@@ -407,7 +426,9 @@ abstract class ScrollView extends StatelessWidget {
return buildViewport(context, offset, axisDirection, slivers);
},
);
final Widget scrollableResult = primary && scrollController != null
final Widget scrollableResult = effectivePrimary && scrollController != null
// Further descendant ScrollViews will not inherit the same PrimaryScrollController
? PrimaryScrollController.none(child: scrollable)
: scrollable;
......
......@@ -1586,13 +1586,8 @@ class ScrollAction extends Action<ScrollIntent> {
return true;
}
// Check for fallback scrollable with context from PrimaryScrollController
if (PrimaryScrollController.of(focus.context!) != null) {
final ScrollController? primaryScrollController = PrimaryScrollController.of(focus.context!);
return primaryScrollController != null
&& primaryScrollController.hasClients
&& primaryScrollController.position.context.notificationContext != null
&& Scrollable.of(primaryScrollController.position.context.notificationContext!) != null;
}
final ScrollController? primaryScrollController = PrimaryScrollController.of(focus.context!);
return primaryScrollController != null && primaryScrollController.hasClients;
}
return false;
}
......@@ -1681,7 +1676,34 @@ class ScrollAction extends Action<ScrollIntent> {
ScrollableState? state = Scrollable.of(primaryFocus!.context!);
if (state == null) {
final ScrollController? primaryScrollController = PrimaryScrollController.of(primaryFocus!.context!);
state = Scrollable.of(primaryScrollController!.position.context.notificationContext!);
assert (() {
if (primaryScrollController!.positions.length != 1) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'A ScrollAction was invoked with the PrimaryScrollController, but '
'more than one ScrollPosition is attached.',
),
ErrorDescription(
'Only one ScrollPosition can be manipulated by a ScrollAction at '
'a time.',
),
ErrorHint(
'The PrimaryScrollController can be inherited automatically by '
'descendant ScrollViews based on the TargetPlatform and scroll '
'direction. By default, the PrimaryScrollController is '
'automatically inherited on mobile platforms for vertical '
'ScrollViews. ScrollView.primary can also override this behavior.',
),
]);
}
return true;
}());
if (primaryScrollController!.position.context.notificationContext == null
&& Scrollable.of(primaryScrollController.position.context.notificationContext!) == null) {
return;
}
state = Scrollable.of(primaryScrollController.position.context.notificationContext!);
}
assert(state != null, '$ScrollAction was invoked on a context that has no scrollable parent');
assert(state!.position.hasPixels, 'Scrollable must be laid out before it can be scrolled via a ScrollAction');
......
......@@ -849,9 +849,12 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
/// This sample shows an app with two scrollables in the same route. Since by
/// default, there is one [PrimaryScrollController] per route, and they both have a
/// scroll direction of [Axis.vertical], they would both try to attach to that
/// controller. The [Scrollbar] cannot support multiple positions attached to
/// the same controller, so one [ListView], and its [Scrollbar] have been
/// provided a unique [ScrollController].
/// controller on mobile platforms. The [Scrollbar] cannot support multiple
/// positions attached to the same controller, so one [ListView], and its
/// [Scrollbar] have been provided a unique [ScrollController]. Desktop
/// platforms do not automatically attach to the PrimaryScrollController,
/// requiring [ScrollView.primary] to be true instead in order to use the
/// PrimaryScrollController.
///
/// Alternatively, a new PrimaryScrollController could be created above one of
/// the [ListView]s.
......@@ -1507,14 +1510,14 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
ErrorHint(
'The Scrollbar attempted to use the $controllerForError. This '
'ScrollController should be associated with the ScrollView that '
'the Scrollbar is being applied to. '
'the Scrollbar is being applied to.'
'${tryPrimary
? 'A ScrollView with an Axis.vertical '
'ScrollDirection will automatically use the '
? 'A ScrollView with an Axis.vertical ScrollDirection on mobile '
'platforms will automatically use the '
'PrimaryScrollController if the user has not provided a '
'ScrollController, but a ScrollDirection of Axis.horizontal will '
'not. To use the PrimaryScrollController explicitly, set ScrollView.primary '
'to true for the Scrollable widget.'
'ScrollController. To use the PrimaryScrollController '
'explicitly, set ScrollView.primary to true for the Scrollable '
'widget.'
: 'When providing your own ScrollController, ensure both the '
'Scrollbar and the Scrollable widget use the same one.'
}',
......@@ -1539,16 +1542,17 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
'The Scrollbar requires a single ScrollPosition in order to be painted.',
),
ErrorHint(
'When $when, the associated Scrollable '
'widgets must have unique ScrollControllers. '
'When $when, the associated ScrollController must only have one '
'ScrollPosition attached.'
'${tryPrimary
? 'The PrimaryScrollController is used by default for '
'ScrollViews with an Axis.vertical ScrollDirection, '
'unless the ScrollView has been provided its own '
'ScrollController. More than one Scrollable may have tried '
'to use the PrimaryScrollController of the current context.'
: 'The provided ScrollController must be unique to a '
'Scrollable widget.'
? 'If a ScrollController has not been provided, the '
'PrimaryScrollController is used by default on mobile platforms '
'for ScrollViews with an Axis.vertical scroll direction. More '
'than one ScrollView may have tried to use the '
'PrimaryScrollController of the current context. '
'ScrollView.primary can override this behavior.'
: 'The provided ScrollController must be unique to one '
'ScrollView widget.'
}',
),
]);
......
......@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
......@@ -377,7 +378,7 @@ class RangeMaintainingTestScrollBehavior extends ScrollBehavior {
const RangeMaintainingTestScrollBehavior();
@override
TargetPlatform getPlatform(BuildContext context) => throw 'should not be called';
TargetPlatform getPlatform(BuildContext context) => defaultTargetPlatform;
@override
Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
......
......@@ -54,6 +54,19 @@ Widget textFieldBoilerplate({ required Widget child }) {
);
}
Widget primaryScrollControllerBoilerplate({ required Widget child, required ScrollController controller }) {
return Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: PrimaryScrollController(
controller: controller,
child: child,
),
),
);
}
void main() {
testWidgets('ListView control test', (WidgetTester tester) async {
final List<String> log = <String>[];
......@@ -887,61 +900,155 @@ void main() {
expect(log, isEmpty);
});
testWidgets('Vertical CustomScrollViews are primary by default', (WidgetTester tester) async {
test('PrimaryScrollController.automaticallyInheritOnPlatforms defaults to all mobile platforms', (){
final PrimaryScrollController primaryScrollController = PrimaryScrollController(
controller: ScrollController(),
child: const SizedBox(),
);
expect(
primaryScrollController.automaticallyInheritForPlatforms,
TargetPlatformVariant.mobile().values,
);
});
testWidgets('Vertical CustomScrollViews are not primary by default', (WidgetTester tester) async {
const CustomScrollView view = CustomScrollView();
expect(view.primary, isTrue);
expect(view.primary, isNull);
});
testWidgets('Vertical ListViews are primary by default', (WidgetTester tester) async {
testWidgets('Vertical CustomScrollViews use PrimaryScrollController by default on mobile', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
await tester.pumpWidget(primaryScrollControllerBoilerplate(
child: const CustomScrollView(),
controller: controller,
));
expect(controller.hasClients, isTrue);
}, variant: TargetPlatformVariant.mobile());
testWidgets("Vertical CustomScrollViews don't use PrimaryScrollController by default on desktop", (WidgetTester tester) async {
final ScrollController controller = ScrollController();
await tester.pumpWidget(primaryScrollControllerBoilerplate(
child: const CustomScrollView(),
controller: controller,
));
expect(controller.hasClients, isFalse);
}, variant: TargetPlatformVariant.desktop());
testWidgets('Vertical ListViews are not primary by default', (WidgetTester tester) async {
final ListView view = ListView();
expect(view.primary, isTrue);
expect(view.primary, isNull);
});
testWidgets('Vertical GridViews are primary by default', (WidgetTester tester) async {
final GridView view = GridView.count(
crossAxisCount: 1,
);
expect(view.primary, isTrue);
testWidgets('Vertical ListViews use PrimaryScrollController by default on mobile', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
await tester.pumpWidget(primaryScrollControllerBoilerplate(
child: ListView(),
controller: controller,
));
expect(controller.hasClients, isTrue);
}, variant: TargetPlatformVariant.mobile());
testWidgets("Vertical ListViews don't use PrimaryScrollController by default on desktop", (WidgetTester tester) async {
final ScrollController controller = ScrollController();
await tester.pumpWidget(primaryScrollControllerBoilerplate(
child: ListView(),
controller: controller,
));
expect(controller.hasClients, isFalse);
}, variant: TargetPlatformVariant.desktop());
testWidgets('Vertical GridViews are not primary by default', (WidgetTester tester) async {
final GridView view = GridView.count(crossAxisCount: 1);
expect(view.primary, isNull);
});
testWidgets('Vertical GridViews use PrimaryScrollController by default on mobile', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
await tester.pumpWidget(primaryScrollControllerBoilerplate(
child: GridView.count(crossAxisCount: 1),
controller: controller,
));
expect(controller.hasClients, isTrue);
}, variant: TargetPlatformVariant.mobile());
testWidgets("Vertical GridViews don't use PrimaryScrollController by default on desktop", (WidgetTester tester) async {
final ScrollController controller = ScrollController();
await tester.pumpWidget(primaryScrollControllerBoilerplate(
child: GridView.count(crossAxisCount: 1),
controller: controller,
));
expect(controller.hasClients, isFalse);
}, variant: TargetPlatformVariant.desktop());
testWidgets('Horizontal CustomScrollViews are non-primary by default', (WidgetTester tester) async {
const CustomScrollView view = CustomScrollView(scrollDirection: Axis.horizontal);
expect(view.primary, isFalse);
final ScrollController controller = ScrollController();
await tester.pumpWidget(primaryScrollControllerBoilerplate(
child: CustomScrollView(
scrollDirection: Axis.horizontal,
controller: ScrollController(),
),
controller: controller,
));
expect(controller.hasClients, isFalse);
});
testWidgets('Horizontal ListViews are non-primary by default', (WidgetTester tester) async {
final ListView view = ListView(scrollDirection: Axis.horizontal);
expect(view.primary, isFalse);
final ScrollController controller = ScrollController();
await tester.pumpWidget(primaryScrollControllerBoilerplate(
child: ListView(
scrollDirection: Axis.horizontal,
controller: ScrollController(),
),
controller: controller,
));
expect(controller.hasClients, isFalse);
});
testWidgets('Horizontal GridViews are non-primary by default', (WidgetTester tester) async {
final GridView view = GridView.count(
scrollDirection: Axis.horizontal,
crossAxisCount: 1,
);
expect(view.primary, isFalse);
final ScrollController controller = ScrollController();
await tester.pumpWidget(primaryScrollControllerBoilerplate(
child: GridView.count(
scrollDirection: Axis.horizontal,
controller: ScrollController(),
crossAxisCount: 1,
),
controller: controller,
));
expect(controller.hasClients, isFalse);
});
testWidgets('CustomScrollViews with controllers are non-primary by default', (WidgetTester tester) async {
final CustomScrollView view = CustomScrollView(
controller: ScrollController(),
);
expect(view.primary, isFalse);
final ScrollController controller = ScrollController();
await tester.pumpWidget(primaryScrollControllerBoilerplate(
child: CustomScrollView(
controller: ScrollController(),
),
controller: controller,
));
expect(controller.hasClients, isFalse);
});
testWidgets('ListViews with controllers are non-primary by default', (WidgetTester tester) async {
final ListView view = ListView(
controller: ScrollController(),
);
expect(view.primary, isFalse);
final ScrollController controller = ScrollController();
await tester.pumpWidget(primaryScrollControllerBoilerplate(
child: ListView(
controller: ScrollController(),
),
controller: controller,
));
expect(controller.hasClients, isFalse);
});
testWidgets('GridViews with controllers are non-primary by default', (WidgetTester tester) async {
final GridView view = GridView.count(
controller: ScrollController(),
crossAxisCount: 1,
);
expect(view.primary, isFalse);
final ScrollController controller = ScrollController();
await tester.pumpWidget(primaryScrollControllerBoilerplate(
child: GridView.count(
controller: ScrollController(),
crossAxisCount: 1,
),
controller: controller,
));
expect(controller.hasClients, isFalse);
});
testWidgets('CustomScrollView sets PrimaryScrollController when primary', (WidgetTester tester) async {
......@@ -1318,6 +1425,55 @@ void main() {
);
});
testWidgets('Fallback ScrollActions handle too many positions with error message', (WidgetTester tester) async {
Widget getScrollView() {
return SizedBox(
width: 400.0,
child: CustomScrollView(
primary: true,
slivers: List<Widget>.generate(
20,
(int index) {
return SliverToBoxAdapter(
child: Focus(
child: SizedBox(key: ValueKey<String>('Box $index'), height: 50.0),
),
);
},
),
),
);
}
await tester.pumpWidget(
MaterialApp(
home: Row(
children: <Widget>[
getScrollView(),
getScrollView(),
],
),
),
);
await tester.pumpAndSettle();
expect(
tester.getRect(
find.byKey(const ValueKey<String>('Box 0'), skipOffstage: false).first
),
equals(const Rect.fromLTRB(0.0, 0.0, 400.0, 50.0)),
);
await tester.sendKeyEvent(LogicalKeyboardKey.pageDown);
final AssertionError exception = tester.takeException() as AssertionError;
expect(exception, isAssertionError);
expect(
exception.message,
contains(
'A ScrollAction was invoked with the PrimaryScrollController, but '
'more than one ScrollPosition is attached.'
),
);
});
testWidgets('if itemExtent is non-null, children have same extent in the scroll direction', (WidgetTester tester) async {
final List<int> numbers = <int>[0,1,2];
......
......@@ -2203,7 +2203,7 @@ void main() {
await tester.pumpAndSettle();
});
testWidgets('Scrollbar thumb can be dragged when the scrollable widget has a negative minScrollExtent', (WidgetTester tester) async {
testWidgets('Scrollbar thumb can be dragged when the scrollable widget has a negative minScrollExtent - desktop', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/95840
final ScrollController scrollController = ScrollController();
......@@ -2223,6 +2223,7 @@ void main() {
isAlwaysShown: true,
controller: scrollController,
child: CustomScrollView(
primary: true,
center: uniqueKey,
slivers: <Widget>[
SliverToBoxAdapter(
......@@ -2282,7 +2283,88 @@ void main() {
color: const Color(0x66BCBCBC),
),
);
}, variant: TargetPlatformVariant.all());
}, variant: TargetPlatformVariant.desktop());
testWidgets('Scrollbar thumb can be dragged when the scrollable widget has a negative minScrollExtent - mobile', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/95840
final ScrollController scrollController = ScrollController();
final UniqueKey uniqueKey = UniqueKey();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: ScrollConfiguration(
behavior: const ScrollBehavior().copyWith(
scrollbars: false,
),
child: PrimaryScrollController(
controller: scrollController,
child: RawScrollbar(
isAlwaysShown: true,
controller: scrollController,
child: CustomScrollView(
center: uniqueKey,
slivers: <Widget>[
SliverToBoxAdapter(
child: Container(
height: 600.0,
),
),
SliverToBoxAdapter(
key: uniqueKey,
child: Container(
height: 600.0,
),
),
SliverToBoxAdapter(
child: Container(
height: 600.0,
),
),
],
),
),
),
),
),
),
);
await tester.pumpAndSettle();
expect(scrollController.offset, 0.0);
expect(
find.byType(RawScrollbar),
paints
..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0))
..rect(
rect: const Rect.fromLTRB(794.0, 200.0, 800.0, 400.0),
color: const Color(0x66BCBCBC),
),
);
// Drag the thumb up to scroll up.
const double scrollAmount = -10.0;
final TestGesture dragScrollbarGesture = await tester.startGesture(const Offset(797.0, 300.0));
await tester.pumpAndSettle();
await dragScrollbarGesture.moveBy(const Offset(0.0, scrollAmount));
await tester.pumpAndSettle();
await dragScrollbarGesture.up();
await tester.pumpAndSettle();
// The view has scrolled more than it would have by a swipe gesture of the
// same distance.
expect(scrollController.offset, lessThan(scrollAmount * 2));
expect(
find.byType(RawScrollbar),
paints
..rect(rect: const Rect.fromLTRB(794.0, 0.0, 800.0, 600.0))
..rect(
rect: const Rect.fromLTRB(794.0, 190.0, 800.0, 390.0),
color: const Color(0x66BCBCBC),
),
);
}, variant: TargetPlatformVariant.mobile());
test('ScrollbarPainter.shouldRepaint returns true when any of the properties changes', () {
ScrollbarPainter createPainter({
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment