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 | import 'package:flutter/gestures.dart'; |
6 | import 'package:flutter/rendering.dart'; |
7 | |
8 | import 'focus_manager.dart'; |
9 | import 'focus_scope.dart'; |
10 | import 'framework.dart'; |
11 | import 'notification_listener.dart'; |
12 | import 'primary_scroll_controller.dart'; |
13 | import 'scroll_controller.dart'; |
14 | import 'scroll_delegate.dart'; |
15 | import 'scroll_notification.dart'; |
16 | import 'scroll_physics.dart'; |
17 | import 'scroll_view.dart'; |
18 | import 'scrollable.dart'; |
19 | import 'scrollable_helpers.dart'; |
20 | import '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. |
46 | abstract 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 | |