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/rendering.dart';
6
7import 'basic.dart';
8import 'debug.dart';
9import 'framework.dart';
10import 'scroll_notification.dart';
11
12export 'package:flutter/rendering.dart' show
13 AxisDirection,
14 GrowthDirection;
15
16/// A widget through which a portion of larger content can be viewed, typically
17/// in combination with a [Scrollable].
18///
19/// [Viewport] is the visual workhorse of the scrolling machinery. It displays a
20/// subset of its children according to its own dimensions and the given
21/// [offset]. As the offset varies, different children are visible through
22/// the viewport.
23///
24/// [Viewport] hosts a bidirectional list of slivers, anchored on a [center]
25/// sliver, which is placed at the zero scroll offset. The center widget is
26/// displayed in the viewport according to the [anchor] property.
27///
28/// Slivers that are earlier in the child list than [center] are displayed in
29/// reverse order in the reverse [axisDirection] starting from the [center]. For
30/// example, if the [axisDirection] is [AxisDirection.down], the first sliver
31/// before [center] is placed above the [center]. The slivers that are later in
32/// the child list than [center] are placed in order in the [axisDirection]. For
33/// example, in the preceding scenario, the first sliver after [center] is
34/// placed below the [center].
35///
36/// [Viewport] cannot contain box children directly. Instead, use a
37/// [SliverList], [SliverFixedExtentList], [SliverGrid], or a
38/// [SliverToBoxAdapter], for example.
39///
40/// See also:
41///
42/// * [ListView], [PageView], [GridView], and [CustomScrollView], which combine
43/// [Scrollable] and [Viewport] into widgets that are easier to use.
44/// * [SliverToBoxAdapter], which allows a box widget to be placed inside a
45/// sliver context (the opposite of this widget).
46/// * [ShrinkWrappingViewport], a variant of [Viewport] that shrink-wraps its
47/// contents along the main axis.
48/// * [ViewportElementMixin], which should be mixed in to the [Element] type used
49/// by viewport-like widgets to correctly handle scroll notifications.
50class Viewport extends MultiChildRenderObjectWidget {
51 /// Creates a widget that is bigger on the inside.
52 ///
53 /// The viewport listens to the [offset], which means you do not need to
54 /// rebuild this widget when the [offset] changes.
55 ///
56 /// The [cacheExtent] must be specified if the [cacheExtentStyle] is
57 /// not [CacheExtentStyle.pixel].
58 Viewport({
59 super.key,
60 this.axisDirection = AxisDirection.down,
61 this.crossAxisDirection,
62 this.anchor = 0.0,
63 required this.offset,
64 this.center,
65 this.cacheExtent,
66 this.cacheExtentStyle = CacheExtentStyle.pixel,
67 this.clipBehavior = Clip.hardEdge,
68 List<Widget> slivers = const <Widget>[],
69 }) : assert(center == null || slivers.where((Widget child) => child.key == center).length == 1),
70 assert(cacheExtentStyle != CacheExtentStyle.viewport || cacheExtent != null),
71 super(children: slivers);
72
73 /// The direction in which the [offset]'s [ViewportOffset.pixels] increases.
74 ///
75 /// For example, if the [axisDirection] is [AxisDirection.down], a scroll
76 /// offset of zero is at the top of the viewport and increases towards the
77 /// bottom of the viewport.
78 final AxisDirection axisDirection;
79
80 /// The direction in which child should be laid out in the cross axis.
81 ///
82 /// If the [axisDirection] is [AxisDirection.down] or [AxisDirection.up], this
83 /// property defaults to [AxisDirection.left] if the ambient [Directionality]
84 /// is [TextDirection.rtl] and [AxisDirection.right] if the ambient
85 /// [Directionality] is [TextDirection.ltr].
86 ///
87 /// If the [axisDirection] is [AxisDirection.left] or [AxisDirection.right],
88 /// this property defaults to [AxisDirection.down].
89 final AxisDirection? crossAxisDirection;
90
91 /// The relative position of the zero scroll offset.
92 ///
93 /// For example, if [anchor] is 0.5 and the [axisDirection] is
94 /// [AxisDirection.down] or [AxisDirection.up], then the zero scroll offset is
95 /// vertically centered within the viewport. If the [anchor] is 1.0, and the
96 /// [axisDirection] is [AxisDirection.right], then the zero scroll offset is
97 /// on the left edge of the viewport.
98 ///
99 /// {@macro flutter.rendering.GrowthDirection.sample}
100 final double anchor;
101
102 /// Which part of the content inside the viewport should be visible.
103 ///
104 /// The [ViewportOffset.pixels] value determines the scroll offset that the
105 /// viewport uses to select which part of its content to display. As the user
106 /// scrolls the viewport, this value changes, which changes the content that
107 /// is displayed.
108 ///
109 /// Typically a [ScrollPosition].
110 final ViewportOffset offset;
111
112 /// The first child in the [GrowthDirection.forward] growth direction.
113 ///
114 /// Children after [center] will be placed in the [axisDirection] relative to
115 /// the [center]. Children before [center] will be placed in the opposite of
116 /// the [axisDirection] relative to the [center].
117 ///
118 /// The [center] must be the key of a child of the viewport.
119 ///
120 /// {@macro flutter.rendering.GrowthDirection.sample}
121 final Key? center;
122
123 /// {@macro flutter.rendering.RenderViewportBase.cacheExtent}
124 ///
125 /// See also:
126 ///
127 /// * [cacheExtentStyle], which controls the units of the [cacheExtent].
128 final double? cacheExtent;
129
130 /// {@macro flutter.rendering.RenderViewportBase.cacheExtentStyle}
131 final CacheExtentStyle cacheExtentStyle;
132
133 /// {@macro flutter.material.Material.clipBehavior}
134 ///
135 /// Defaults to [Clip.hardEdge].
136 final Clip clipBehavior;
137
138 /// Given a [BuildContext] and an [AxisDirection], determine the correct cross
139 /// axis direction.
140 ///
141 /// This depends on the [Directionality] if the `axisDirection` is vertical;
142 /// otherwise, the default cross axis direction is downwards.
143 static AxisDirection getDefaultCrossAxisDirection(BuildContext context, AxisDirection axisDirection) {
144 switch (axisDirection) {
145 case AxisDirection.up:
146 assert(debugCheckHasDirectionality(
147 context,
148 why: "to determine the cross-axis direction when the viewport has an 'up' axisDirection",
149 alternative: "Alternatively, consider specifying the 'crossAxisDirection' argument on the Viewport.",
150 ));
151 return textDirectionToAxisDirection(Directionality.of(context));
152 case AxisDirection.right:
153 return AxisDirection.down;
154 case AxisDirection.down:
155 assert(debugCheckHasDirectionality(
156 context,
157 why: "to determine the cross-axis direction when the viewport has a 'down' axisDirection",
158 alternative: "Alternatively, consider specifying the 'crossAxisDirection' argument on the Viewport.",
159 ));
160 return textDirectionToAxisDirection(Directionality.of(context));
161 case AxisDirection.left:
162 return AxisDirection.down;
163 }
164 }
165
166 @override
167 RenderViewport createRenderObject(BuildContext context) {
168 return RenderViewport(
169 axisDirection: axisDirection,
170 crossAxisDirection: crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(context, axisDirection),
171 anchor: anchor,
172 offset: offset,
173 cacheExtent: cacheExtent,
174 cacheExtentStyle: cacheExtentStyle,
175 clipBehavior: clipBehavior,
176 );
177 }
178
179 @override
180 void updateRenderObject(BuildContext context, RenderViewport renderObject) {
181 renderObject
182 ..axisDirection = axisDirection
183 ..crossAxisDirection = crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(context, axisDirection)
184 ..anchor = anchor
185 ..offset = offset
186 ..cacheExtent = cacheExtent
187 ..cacheExtentStyle = cacheExtentStyle
188 ..clipBehavior = clipBehavior;
189 }
190
191 @override
192 MultiChildRenderObjectElement createElement() => _ViewportElement(this);
193
194 @override
195 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
196 super.debugFillProperties(properties);
197 properties.add(EnumProperty<AxisDirection>('axisDirection', axisDirection));
198 properties.add(EnumProperty<AxisDirection>('crossAxisDirection', crossAxisDirection, defaultValue: null));
199 properties.add(DoubleProperty('anchor', anchor));
200 properties.add(DiagnosticsProperty<ViewportOffset>('offset', offset));
201 if (center != null) {
202 properties.add(DiagnosticsProperty<Key>('center', center));
203 } else if (children.isNotEmpty && children.first.key != null) {
204 properties.add(DiagnosticsProperty<Key>('center', children.first.key, tooltip: 'implicit'));
205 }
206 properties.add(DiagnosticsProperty<double>('cacheExtent', cacheExtent));
207 properties.add(DiagnosticsProperty<CacheExtentStyle>('cacheExtentStyle', cacheExtentStyle));
208 }
209}
210
211class _ViewportElement extends MultiChildRenderObjectElement with NotifiableElementMixin, ViewportElementMixin {
212 /// Creates an element that uses the given widget as its configuration.
213 _ViewportElement(Viewport super.widget);
214
215 bool _doingMountOrUpdate = false;
216 int? _centerSlotIndex;
217
218 @override
219 RenderViewport get renderObject => super.renderObject as RenderViewport;
220
221 @override
222 void mount(Element? parent, Object? newSlot) {
223 assert(!_doingMountOrUpdate);
224 _doingMountOrUpdate = true;
225 super.mount(parent, newSlot);
226 _updateCenter();
227 assert(_doingMountOrUpdate);
228 _doingMountOrUpdate = false;
229 }
230
231 @override
232 void update(MultiChildRenderObjectWidget newWidget) {
233 assert(!_doingMountOrUpdate);
234 _doingMountOrUpdate = true;
235 super.update(newWidget);
236 _updateCenter();
237 assert(_doingMountOrUpdate);
238 _doingMountOrUpdate = false;
239 }
240
241 void _updateCenter() {
242 // TODO(ianh): cache the keys to make this faster
243 final Viewport viewport = widget as Viewport;
244 if (viewport.center != null) {
245 int elementIndex = 0;
246 for (final Element e in children) {
247 if (e.widget.key == viewport.center) {
248 renderObject.center = e.renderObject as RenderSliver?;
249 break;
250 }
251 elementIndex++;
252 }
253 assert(elementIndex < children.length);
254 _centerSlotIndex = elementIndex;
255 } else if (children.isNotEmpty) {
256 renderObject.center = children.first.renderObject as RenderSliver?;
257 _centerSlotIndex = 0;
258 } else {
259 renderObject.center = null;
260 _centerSlotIndex = null;
261 }
262 }
263
264 @override
265 void insertRenderObjectChild(RenderObject child, IndexedSlot<Element?> slot) {
266 super.insertRenderObjectChild(child, slot);
267 // Once [mount]/[update] are done, the `renderObject.center` will be updated
268 // in [_updateCenter].
269 if (!_doingMountOrUpdate && slot.index == _centerSlotIndex) {
270 renderObject.center = child as RenderSliver?;
271 }
272 }
273
274 @override
275 void moveRenderObjectChild(RenderObject child, IndexedSlot<Element?> oldSlot, IndexedSlot<Element?> newSlot) {
276 super.moveRenderObjectChild(child, oldSlot, newSlot);
277 assert(_doingMountOrUpdate);
278 }
279
280 @override
281 void removeRenderObjectChild(RenderObject child, Object? slot) {
282 super.removeRenderObjectChild(child, slot);
283 if (!_doingMountOrUpdate && renderObject.center == child) {
284 renderObject.center = null;
285 }
286 }
287
288 @override
289 void debugVisitOnstageChildren(ElementVisitor visitor) {
290 children.where((Element e) {
291 final RenderSliver renderSliver = e.renderObject! as RenderSliver;
292 return renderSliver.geometry!.visible;
293 }).forEach(visitor);
294 }
295}
296
297/// A widget that is bigger on the inside and shrink wraps its children in the
298/// main axis.
299///
300/// [ShrinkWrappingViewport] displays a subset of its children according to its
301/// own dimensions and the given [offset]. As the offset varies, different
302/// children are visible through the viewport.
303///
304/// [ShrinkWrappingViewport] differs from [Viewport] in that [Viewport] expands
305/// to fill the main axis whereas [ShrinkWrappingViewport] sizes itself to match
306/// its children in the main axis. This shrink wrapping behavior is expensive
307/// because the children, and hence the viewport, could potentially change size
308/// whenever the [offset] changes (e.g., because of a collapsing header).
309///
310/// [ShrinkWrappingViewport] cannot contain box children directly. Instead, use
311/// a [SliverList], [SliverFixedExtentList], [SliverGrid], or a
312/// [SliverToBoxAdapter], for example.
313///
314/// See also:
315///
316/// * [ListView], [PageView], [GridView], and [CustomScrollView], which combine
317/// [Scrollable] and [ShrinkWrappingViewport] into widgets that are easier to
318/// use.
319/// * [SliverToBoxAdapter], which allows a box widget to be placed inside a
320/// sliver context (the opposite of this widget).
321/// * [Viewport], a viewport that does not shrink-wrap its contents.
322class ShrinkWrappingViewport extends MultiChildRenderObjectWidget {
323 /// Creates a widget that is bigger on the inside and shrink wraps its
324 /// children in the main axis.
325 ///
326 /// The viewport listens to the [offset], which means you do not need to
327 /// rebuild this widget when the [offset] changes.
328 const ShrinkWrappingViewport({
329 super.key,
330 this.axisDirection = AxisDirection.down,
331 this.crossAxisDirection,
332 required this.offset,
333 this.clipBehavior = Clip.hardEdge,
334 List<Widget> slivers = const <Widget>[],
335 }) : super(children: slivers);
336
337 /// The direction in which the [offset]'s [ViewportOffset.pixels] increases.
338 ///
339 /// For example, if the [axisDirection] is [AxisDirection.down], a scroll
340 /// offset of zero is at the top of the viewport and increases towards the
341 /// bottom of the viewport.
342 final AxisDirection axisDirection;
343
344 /// The direction in which child should be laid out in the cross axis.
345 ///
346 /// If the [axisDirection] is [AxisDirection.down] or [AxisDirection.up], this
347 /// property defaults to [AxisDirection.left] if the ambient [Directionality]
348 /// is [TextDirection.rtl] and [AxisDirection.right] if the ambient
349 /// [Directionality] is [TextDirection.ltr].
350 ///
351 /// If the [axisDirection] is [AxisDirection.left] or [AxisDirection.right],
352 /// this property defaults to [AxisDirection.down].
353 final AxisDirection? crossAxisDirection;
354
355 /// Which part of the content inside the viewport should be visible.
356 ///
357 /// The [ViewportOffset.pixels] value determines the scroll offset that the
358 /// viewport uses to select which part of its content to display. As the user
359 /// scrolls the viewport, this value changes, which changes the content that
360 /// is displayed.
361 ///
362 /// Typically a [ScrollPosition].
363 final ViewportOffset offset;
364
365 /// {@macro flutter.material.Material.clipBehavior}
366 ///
367 /// Defaults to [Clip.hardEdge].
368 final Clip clipBehavior;
369
370 @override
371 RenderShrinkWrappingViewport createRenderObject(BuildContext context) {
372 return RenderShrinkWrappingViewport(
373 axisDirection: axisDirection,
374 crossAxisDirection: crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(context, axisDirection),
375 offset: offset,
376 clipBehavior: clipBehavior,
377 );
378 }
379
380 @override
381 void updateRenderObject(BuildContext context, RenderShrinkWrappingViewport renderObject) {
382 renderObject
383 ..axisDirection = axisDirection
384 ..crossAxisDirection = crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(context, axisDirection)
385 ..offset = offset
386 ..clipBehavior = clipBehavior;
387 }
388
389 @override
390 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
391 super.debugFillProperties(properties);
392 properties.add(EnumProperty<AxisDirection>('axisDirection', axisDirection));
393 properties.add(EnumProperty<AxisDirection>('crossAxisDirection', crossAxisDirection, defaultValue: null));
394 properties.add(DiagnosticsProperty<ViewportOffset>('offset', offset));
395 }
396}
397