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