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 'framework.dart';
8import 'scroll_delegate.dart';
9import 'sliver.dart';
10
11/// A sliver that places its box children in a linear array and constrains them
12/// to have the same extent as a prototype item along the main axis.
13///
14/// _To learn more about slivers, see [CustomScrollView.slivers]._
15///
16/// [SliverPrototypeExtentList] arranges its children in a line along
17/// the main axis starting at offset zero and without gaps. Each child is
18/// constrained to the same extent as the [prototypeItem] along the main axis
19/// and the [SliverConstraints.crossAxisExtent] along the cross axis.
20///
21/// [SliverPrototypeExtentList] is more efficient than [SliverList] because
22/// [SliverPrototypeExtentList] does not need to lay out its children to obtain
23/// their extent along the main axis. It's a little more flexible than
24/// [SliverFixedExtentList] because there's no need to determine the appropriate
25/// item extent in pixels.
26///
27/// See also:
28///
29/// * [SliverFixedExtentList], whose children are forced to a given pixel
30/// extent.
31/// * [SliverList], which does not require its children to have the same
32/// extent in the main axis.
33/// * [SliverFillViewport], which sizes its children based on the
34/// size of the viewport, regardless of what else is in the scroll view.
35class SliverPrototypeExtentList extends SliverMultiBoxAdaptorWidget {
36 /// Creates a sliver that places its box children in a linear array and
37 /// constrains them to have the same extent as a prototype item along
38 /// the main axis.
39 const SliverPrototypeExtentList({
40 super.key,
41 required super.delegate,
42 required this.prototypeItem,
43 });
44
45 /// A sliver that places its box children in a linear array and constrains them
46 /// to have the same extent as a prototype item along the main axis.
47 ///
48 /// This constructor is appropriate for sliver lists with a large (or
49 /// infinite) number of children whose extent is already determined.
50 ///
51 /// Providing a non-null `itemCount` improves the ability of the [SliverGrid]
52 /// to estimate the maximum scroll extent.
53 ///
54 /// `itemBuilder` will be called only with indices greater than or equal to
55 /// zero and less than `itemCount`.
56 ///
57 /// {@macro flutter.widgets.ListView.builder.itemBuilder}
58 ///
59 /// The `prototypeItem` argument is used to determine the extent of each item.
60 ///
61 /// {@macro flutter.widgets.PageView.findChildIndexCallback}
62 ///
63 /// The `addAutomaticKeepAlives` argument corresponds to the
64 /// [SliverChildBuilderDelegate.addAutomaticKeepAlives] property. The
65 /// `addRepaintBoundaries` argument corresponds to the
66 /// [SliverChildBuilderDelegate.addRepaintBoundaries] property. The
67 /// `addSemanticIndexes` argument corresponds to the
68 /// [SliverChildBuilderDelegate.addSemanticIndexes] property.
69 ///
70 /// {@tool snippet}
71 /// This example, which would be inserted into a [CustomScrollView.slivers]
72 /// list, shows an infinite number of items in varying shades of blue:
73 ///
74 /// ```dart
75 /// SliverPrototypeExtentList.builder(
76 /// prototypeItem: Container(
77 /// alignment: Alignment.center,
78 /// child: const Text('list item prototype'),
79 /// ),
80 /// itemBuilder: (BuildContext context, int index) {
81 /// return Container(
82 /// alignment: Alignment.center,
83 /// color: Colors.lightBlue[100 * (index % 9)],
84 /// child: Text('list item $index'),
85 /// );
86 /// },
87 /// )
88 /// ```
89 /// {@end-tool}
90 SliverPrototypeExtentList.builder({
91 super.key,
92 required NullableIndexedWidgetBuilder itemBuilder,
93 required this.prototypeItem,
94 ChildIndexGetter? findChildIndexCallback,
95 int? itemCount,
96 bool addAutomaticKeepAlives = true,
97 bool addRepaintBoundaries = true,
98 bool addSemanticIndexes = true,
99 }) : super(delegate: SliverChildBuilderDelegate(
100 itemBuilder,
101 findChildIndexCallback: findChildIndexCallback,
102 childCount: itemCount,
103 addAutomaticKeepAlives: addAutomaticKeepAlives,
104 addRepaintBoundaries: addRepaintBoundaries,
105 addSemanticIndexes: addSemanticIndexes,
106 ));
107
108 /// A sliver that places multiple box children in a linear array along the main
109 /// axis.
110 ///
111 /// This constructor uses a list of [Widget]s to build the sliver.
112 ///
113 /// The `addAutomaticKeepAlives` argument corresponds to the
114 /// [SliverChildBuilderDelegate.addAutomaticKeepAlives] property. The
115 /// `addRepaintBoundaries` argument corresponds to the
116 /// [SliverChildBuilderDelegate.addRepaintBoundaries] property. The
117 /// `addSemanticIndexes` argument corresponds to the
118 /// [SliverChildBuilderDelegate.addSemanticIndexes] property.
119 ///
120 /// {@tool snippet}
121 /// This example, which would be inserted into a [CustomScrollView.slivers]
122 /// list, shows an infinite number of items in varying shades of blue:
123 ///
124 /// ```dart
125 /// SliverPrototypeExtentList.list(
126 /// prototypeItem: const Text('Hello'),
127 /// children: const <Widget>[
128 /// Text('Hello'),
129 /// Text('World!'),
130 /// ],
131 /// );
132 /// ```
133 /// {@end-tool}
134 SliverPrototypeExtentList.list({
135 super.key,
136 required List<Widget> children,
137 required this.prototypeItem,
138 bool addAutomaticKeepAlives = true,
139 bool addRepaintBoundaries = true,
140 bool addSemanticIndexes = true,
141 }) : super(delegate: SliverChildListDelegate(
142 children,
143 addAutomaticKeepAlives: addAutomaticKeepAlives,
144 addRepaintBoundaries: addRepaintBoundaries,
145 addSemanticIndexes: addSemanticIndexes,
146 ));
147
148 /// Defines the main axis extent of all of this sliver's children.
149 ///
150 /// The [prototypeItem] is laid out before the rest of the sliver's children
151 /// and its size along the main axis fixes the size of each child. The
152 /// [prototypeItem] is essentially [Offstage]: it is not painted and it
153 /// cannot respond to input.
154 final Widget prototypeItem;
155
156 @override
157 RenderSliverMultiBoxAdaptor createRenderObject(BuildContext context) {
158 final _SliverPrototypeExtentListElement element = context as _SliverPrototypeExtentListElement;
159 return _RenderSliverPrototypeExtentList(childManager: element);
160 }
161
162 @override
163 SliverMultiBoxAdaptorElement createElement() => _SliverPrototypeExtentListElement(this);
164}
165
166class _SliverPrototypeExtentListElement extends SliverMultiBoxAdaptorElement {
167 _SliverPrototypeExtentListElement(SliverPrototypeExtentList super.widget);
168
169 @override
170 _RenderSliverPrototypeExtentList get renderObject => super.renderObject as _RenderSliverPrototypeExtentList;
171
172 Element? _prototype;
173 static final Object _prototypeSlot = Object();
174
175 @override
176 void insertRenderObjectChild(covariant RenderObject child, covariant Object slot) {
177 if (slot == _prototypeSlot) {
178 assert(child is RenderBox);
179 renderObject.child = child as RenderBox;
180 } else {
181 super.insertRenderObjectChild(child, slot as int);
182 }
183 }
184
185 @override
186 void didAdoptChild(RenderBox child) {
187 if (child != renderObject.child) {
188 super.didAdoptChild(child);
189 }
190 }
191
192 @override
193 void moveRenderObjectChild(RenderBox child, Object oldSlot, Object newSlot) {
194 if (newSlot == _prototypeSlot) {
195 // There's only one prototype child so it cannot be moved.
196 assert(false);
197 } else {
198 super.moveRenderObjectChild(child, oldSlot as int, newSlot as int);
199 }
200 }
201
202 @override
203 void removeRenderObjectChild(RenderBox child, Object slot) {
204 if (renderObject.child == child) {
205 renderObject.child = null;
206 } else {
207 super.removeRenderObjectChild(child, slot as int);
208 }
209 }
210
211 @override
212 void visitChildren(ElementVisitor visitor) {
213 if (_prototype != null) {
214 visitor(_prototype!);
215 }
216 super.visitChildren(visitor);
217 }
218
219 @override
220 void mount(Element? parent, Object? newSlot) {
221 super.mount(parent, newSlot);
222 _prototype = updateChild(_prototype, (widget as SliverPrototypeExtentList).prototypeItem, _prototypeSlot);
223 }
224
225 @override
226 void update(SliverPrototypeExtentList newWidget) {
227 super.update(newWidget);
228 assert(widget == newWidget);
229 _prototype = updateChild(_prototype, (widget as SliverPrototypeExtentList).prototypeItem, _prototypeSlot);
230 }
231}
232
233class _RenderSliverPrototypeExtentList extends RenderSliverFixedExtentBoxAdaptor {
234 _RenderSliverPrototypeExtentList({
235 required _SliverPrototypeExtentListElement childManager,
236 }) : super(childManager: childManager);
237
238 RenderBox? _child;
239 RenderBox? get child => _child;
240 set child(RenderBox? value) {
241 if (_child != null) {
242 dropChild(_child!);
243 }
244 _child = value;
245 if (_child != null) {
246 adoptChild(_child!);
247 }
248 markNeedsLayout();
249 }
250
251 @override
252 void performLayout() {
253 child!.layout(constraints.asBoxConstraints(), parentUsesSize: true);
254 super.performLayout();
255 }
256
257 @override
258 void attach(PipelineOwner owner) {
259 super.attach(owner);
260 if (_child != null) {
261 _child!.attach(owner);
262 }
263 }
264
265 @override
266 void detach() {
267 super.detach();
268 if (_child != null) {
269 _child!.detach();
270 }
271 }
272
273 @override
274 void redepthChildren() {
275 if (_child != null) {
276 redepthChild(_child!);
277 }
278 super.redepthChildren();
279 }
280
281 @override
282 void visitChildren(RenderObjectVisitor visitor) {
283 if (_child != null) {
284 visitor(_child!);
285 }
286 super.visitChildren(visitor);
287 }
288
289 @override
290 double get itemExtent {
291 assert(child != null && child!.hasSize);
292 return constraints.axis == Axis.vertical ? child!.size.height : child!.size.width;
293 }
294}
295