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