1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:flutter/gestures.dart';
6import 'package:flutter/rendering.dart';
7
8import 'focus_manager.dart';
9import 'focus_scope.dart';
10import 'framework.dart';
11import 'notification_listener.dart';
12import 'primary_scroll_controller.dart';
13import 'scroll_controller.dart';
14import 'scroll_delegate.dart';
15import 'scroll_notification.dart';
16import 'scroll_physics.dart';
17import 'scroll_view.dart';
18import 'scrollable.dart';
19import 'scrollable_helpers.dart';
20import 'two_dimensional_viewport.dart';
21
22/// A widget that combines a [TwoDimensionalScrollable] and a
23/// [TwoDimensionalViewport] to create an interactive scrolling pane of content
24/// in both vertical and horizontal dimensions.
25///
26/// A two-way scrollable widget consist of three pieces:
27///
28/// 1. A [TwoDimensionalScrollable] widget, which listens for various user
29/// gestures and implements the interaction design for scrolling.
30/// 2. A [TwoDimensionalViewport] widget, which implements the visual design
31/// for scrolling by displaying only a portion
32/// of the widgets inside the scroll view.
33/// 3. A [TwoDimensionalChildDelegate], which provides the children visible in
34/// the scroll view.
35///
36/// [TwoDimensionalScrollView] helps orchestrate these pieces by creating the
37/// [TwoDimensionalScrollable] and deferring to its subclass to implement
38/// [buildViewport], which builds a subclass of [TwoDimensionalViewport]. The
39/// [TwoDimensionalChildDelegate] is provided by the [delegate] parameter.
40///
41/// A [TwoDimensionalScrollView] has two different [ScrollPosition]s, one for
42/// each [Axis]. This means that there are also two unique [ScrollController]s
43/// for these positions. To provide a ScrollController to access the
44/// ScrollPosition, use the [ScrollableDetails.controller] property of the
45/// associated axis that is provided to this scroll view.
46abstract class TwoDimensionalScrollView extends StatelessWidget {
47 /// Creates a widget that scrolls in both dimensions.
48 ///
49 /// The [primary] argument is associated with the [mainAxis]. The main axis
50 /// [ScrollableDetails.controller] must be null if [primary] is configured for
51 /// that axis. If [primary] is true, the nearest [PrimaryScrollController]
52 /// surrounding the widget is attached to the scroll position of that axis.
53 const TwoDimensionalScrollView({
54 super.key,
55 this.primary,
56 this.mainAxis = Axis.vertical,
57 this.verticalDetails = const ScrollableDetails.vertical(),
58 this.horizontalDetails = const ScrollableDetails.horizontal(),
59 required this.delegate,
60 this.cacheExtent,
61 this.diagonalDragBehavior = DiagonalDragBehavior.none,
62 this.dragStartBehavior = DragStartBehavior.start,
63 this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
64 this.clipBehavior = Clip.hardEdge,
65 });
66
67 /// A delegate that provides the children for the [TwoDimensionalScrollView].
68 final TwoDimensionalChildDelegate delegate;
69
70 /// {@macro flutter.rendering.RenderViewportBase.cacheExtent}
71 final double? cacheExtent;
72
73 /// Whether scrolling gestures should lock to one axes, allow free movement
74 /// in both axes, or be evaluated on a weighted scale.
75 ///
76 /// Defaults to [DiagonalDragBehavior.none], locking axes to receive input one
77 /// at a time.
78 final DiagonalDragBehavior diagonalDragBehavior;
79
80 /// {@macro flutter.widgets.scroll_view.primary}
81 final bool? primary;
82
83 /// The main axis of the two.
84 ///
85 /// Used to determine how to apply [primary] when true.
86 ///
87 /// This value should also be provided to the subclass of
88 /// [TwoDimensionalViewport], where it is used to determine paint order of
89 /// children.
90 final Axis mainAxis;
91
92 /// The configuration of the vertical Scrollable.
93 ///
94 /// These [ScrollableDetails] can be used to set the [AxisDirection],
95 /// [ScrollController], [ScrollPhysics] and more for the vertical axis.
96 final ScrollableDetails verticalDetails;
97
98 /// The configuration of the horizontal Scrollable.
99 ///
100 /// These [ScrollableDetails] can be used to set the [AxisDirection],
101 /// [ScrollController], [ScrollPhysics] and more for the horizontal axis.
102 final ScrollableDetails horizontalDetails;
103
104 /// {@macro flutter.widgets.scrollable.dragStartBehavior}
105 final DragStartBehavior dragStartBehavior;
106
107 /// {@macro flutter.widgets.scroll_view.keyboardDismissBehavior}
108 final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
109
110 /// {@macro flutter.material.Material.clipBehavior}
111 ///
112 /// Defaults to [Clip.hardEdge].
113 final Clip clipBehavior;
114
115 /// Build the two dimensional viewport.
116 ///
117 /// Subclasses may override this method to change how the viewport is built,
118 /// likely a subclass of [TwoDimensionalViewport].
119 ///
120 /// The `verticalOffset` and `horizontalOffset` arguments are the values
121 /// obtained from [TwoDimensionalScrollable.viewportBuilder].
122 Widget buildViewport(
123 BuildContext context,
124 ViewportOffset verticalOffset,
125 ViewportOffset horizontalOffset,
126 );
127
128 @override
129 Widget build(BuildContext context) {
130 assert(
131 axisDirectionToAxis(verticalDetails.direction) == Axis.vertical,
132 'TwoDimensionalScrollView.verticalDetails are not Axis.vertical.'
133 );
134 assert(
135 axisDirectionToAxis(horizontalDetails.direction) == Axis.horizontal,
136 'TwoDimensionalScrollView.horizontalDetails are not Axis.horizontal.'
137 );
138
139 ScrollableDetails mainAxisDetails = switch (mainAxis) {
140 Axis.vertical => verticalDetails,
141 Axis.horizontal => horizontalDetails,
142 };
143
144 final bool effectivePrimary = primary
145 ?? mainAxisDetails.controller == null && PrimaryScrollController.shouldInherit(
146 context,
147 mainAxis,
148 );
149
150 if (effectivePrimary) {
151 // Using PrimaryScrollController for mainAxis.
152 assert(
153 mainAxisDetails.controller == null,
154 'TwoDimensionalScrollView.primary was explicitly set to true, but a '
155 'ScrollController was provided in the ScrollableDetails of the '
156 'TwoDimensionalScrollView.mainAxis.'
157 );
158 mainAxisDetails = mainAxisDetails.copyWith(
159 controller: PrimaryScrollController.of(context),
160 );
161 }
162
163 final TwoDimensionalScrollable scrollable = TwoDimensionalScrollable(
164 horizontalDetails : switch (mainAxis) {
165 Axis.horizontal => mainAxisDetails,
166 Axis.vertical => horizontalDetails,
167 },
168 verticalDetails: switch (mainAxis) {
169 Axis.vertical => mainAxisDetails,
170 Axis.horizontal => verticalDetails,
171 },
172 diagonalDragBehavior: diagonalDragBehavior,
173 viewportBuilder: buildViewport,
174 dragStartBehavior: dragStartBehavior,
175 );
176
177 final Widget scrollableResult = effectivePrimary
178 // Further descendant ScrollViews will not inherit the same PrimaryScrollController
179 ? PrimaryScrollController.none(child: scrollable)
180 : scrollable;
181
182 if (keyboardDismissBehavior == ScrollViewKeyboardDismissBehavior.onDrag) {
183 return NotificationListener<ScrollUpdateNotification>(
184 child: scrollableResult,
185 onNotification: (ScrollUpdateNotification notification) {
186 final FocusScopeNode focusScope = FocusScope.of(context);
187 if (notification.dragDetails != null && focusScope.hasFocus) {
188 focusScope.unfocus();
189 }
190 return false;
191 },
192 );
193 }
194 return scrollableResult;
195 }
196
197 @override
198 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
199 super.debugFillProperties(properties);
200 properties.add(EnumProperty<Axis>('mainAxis', mainAxis));
201 properties.add(EnumProperty<DiagonalDragBehavior>('diagonalDragBehavior', diagonalDragBehavior));
202 properties.add(FlagProperty('primary', value: primary, ifTrue: 'using primary controller', showName: true));
203 properties.add(DiagnosticsProperty<ScrollableDetails>('verticalDetails', verticalDetails, showName: false));
204 properties.add(DiagnosticsProperty<ScrollableDetails>('horizontalDetails', horizontalDetails, showName: false));
205 }
206}
207