sliver.dart 13.4 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1 2 3 4 5 6 7 8 9 10 11
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:collection' show SplayTreeMap, HashMap;

import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';

import 'framework.dart';
import 'basic.dart';
12

13 14 15 16 17
export 'package:flutter/rendering.dart' show
  SliverGridDelegate,
  SliverGridDelegateWithFixedCrossAxisCount,
  SliverGridDelegateWithMaxCrossAxisExtent;

Adam Barth's avatar
Adam Barth committed
18
abstract class SliverChildDelegate {
Ian Hickson's avatar
Ian Hickson committed
19 20
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
Adam Barth's avatar
Adam Barth committed
21
  const SliverChildDelegate();
Ian Hickson's avatar
Ian Hickson committed
22 23 24

  Widget build(BuildContext context, int index);

25 26 27 28 29 30 31 32
  /// Returns an estimate of the number of children this delegate will build.
  ///
  /// Used to estimate the maximum scroll offset if [estimateMaxScrollOffset]
  /// returns null.
  ///
  /// Return null if there are an unbounded number of children or if it would
  /// be too difficult to estimate the number of children.
  int get estimatedChildCount => null;
Ian Hickson's avatar
Ian Hickson committed
33

34
  double estimateMaxScrollOffset(
Ian Hickson's avatar
Ian Hickson committed
35 36 37 38
    int firstIndex,
    int lastIndex,
    double leadingScrollOffset,
    double trailingScrollOffset,
39 40 41
  ) => null;

  bool shouldRebuild(@checked SliverChildDelegate oldDelegate);
Ian Hickson's avatar
Ian Hickson committed
42 43
}

44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
class SliverChildBuilderDelegate extends SliverChildDelegate {
  const SliverChildBuilderDelegate(this.builder, { this.childCount });

  final IndexedWidgetBuilder builder;

  final int childCount;

  @override
  Widget build(BuildContext context, int index) {
    assert(builder != null);
    if (index < 0 || (childCount != null && index >= childCount))
      return null;
    final Widget child = builder(context, index);
    if (child == null)
      return null;
    return new RepaintBoundary.wrap(child, index);
  }

  @override
  int get estimatedChildCount => childCount;

  @override
66
  bool shouldRebuild(@checked SliverChildBuilderDelegate oldDelegate) => true;
67 68
}

Ian Hickson's avatar
Ian Hickson committed
69 70 71
// ///
// /// In general building all the widgets in advance is not efficient. It is
// /// better to create a delegate that builds them on demand by subclassing
72
// /// [SliverChildDelegate] directly.
Ian Hickson's avatar
Ian Hickson committed
73 74 75 76 77 78 79 80
// ///
// /// This class is provided for the cases where either the list of children is
// /// known well in advance (ideally the children are themselves compile-time
// /// constants, for example), and therefore will not be built each time the
// /// delegate itself is created, or the list is small, such that it's likely
// /// always visible (and thus there is nothing to be gained by building it on
// /// demand). For example, the body of a dialog box might fit both of these
// /// conditions.
Adam Barth's avatar
Adam Barth committed
81
class SliverChildListDelegate extends SliverChildDelegate {
82
  const SliverChildListDelegate(this.children, { this.addRepaintBoundaries: true });
Ian Hickson's avatar
Ian Hickson committed
83

84 85 86 87 88 89 90 91 92 93 94 95
  /// Whether to wrap each child in a [RepaintBoundary].
  ///
  /// Typically, children in a scrolling container are wrapped in repaint
  /// boundaries so that they do not need to be repainted as the list scrolls.
  /// If the children are easy to repaint (e.g., solid color blocks or a short
  /// snippet of text), it might be more efficient to not add a repaint boundary
  /// and simply repaint the children during scrolling.
  ///
  /// Defaults to true.
  final bool addRepaintBoundaries;

  /// The widgets to display.
Ian Hickson's avatar
Ian Hickson committed
96 97 98 99 100 101 102
  final List<Widget> children;

  @override
  Widget build(BuildContext context, int index) {
    assert(children != null);
    if (index < 0 || index >= children.length)
      return null;
103 104
    final Widget child = children[index];
    assert(child != null);
105
    return addRepaintBoundaries ? new RepaintBoundary.wrap(child, index) : child;
Ian Hickson's avatar
Ian Hickson committed
106 107
  }

108 109 110
  @override
  int get estimatedChildCount => children.length;

Ian Hickson's avatar
Ian Hickson committed
111
  @override
Adam Barth's avatar
Adam Barth committed
112
  bool shouldRebuild(@checked SliverChildListDelegate oldDelegate) {
Ian Hickson's avatar
Ian Hickson committed
113 114 115 116
    return children != oldDelegate.children;
  }
}

Adam Barth's avatar
Adam Barth committed
117 118
abstract class SliverMultiBoxAdaptorWidget extends RenderObjectWidget {
  SliverMultiBoxAdaptorWidget({
Ian Hickson's avatar
Ian Hickson committed
119 120 121 122 123 124
    Key key,
    @required this.delegate,
  }) : super(key: key) {
    assert(delegate != null);
  }

Adam Barth's avatar
Adam Barth committed
125
  final SliverChildDelegate delegate;
Ian Hickson's avatar
Ian Hickson committed
126 127

  @override
Adam Barth's avatar
Adam Barth committed
128
  SliverMultiBoxAdaptorElement createElement() => new SliverMultiBoxAdaptorElement(this);
Ian Hickson's avatar
Ian Hickson committed
129 130

  @override
Adam Barth's avatar
Adam Barth committed
131
  RenderSliverMultiBoxAdaptor createRenderObject(BuildContext context);
Ian Hickson's avatar
Ian Hickson committed
132

133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
  double estimateMaxScrollOffset(
    SliverConstraints constraints,
    int firstIndex,
    int lastIndex,
    double leadingScrollOffset,
    double trailingScrollOffset,
  ) {
    assert(lastIndex >= firstIndex);
    return delegate.estimateMaxScrollOffset(
      firstIndex,
      lastIndex,
      leadingScrollOffset,
      trailingScrollOffset,
    );
  }

Ian Hickson's avatar
Ian Hickson committed
149 150 151 152 153 154 155
  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('delegate: $delegate');
  }
}

156 157
class SliverList extends SliverMultiBoxAdaptorWidget {
  SliverList({
Adam Barth's avatar
Adam Barth committed
158 159 160
    Key key,
    @required SliverChildDelegate delegate,
  }) : super(key: key, delegate: delegate);
Ian Hickson's avatar
Ian Hickson committed
161 162

  @override
163
  RenderSliverList createRenderObject(BuildContext context) {
Adam Barth's avatar
Adam Barth committed
164
    final SliverMultiBoxAdaptorElement element = context;
165
    return new RenderSliverList(childManager: element);
Adam Barth's avatar
Adam Barth committed
166 167
  }
}
Ian Hickson's avatar
Ian Hickson committed
168

169 170
class SliverFixedExtentList extends SliverMultiBoxAdaptorWidget {
  SliverFixedExtentList({
Adam Barth's avatar
Adam Barth committed
171 172 173 174 175 176
    Key key,
    @required SliverChildDelegate delegate,
    @required this.itemExtent,
  }) : super(key: key, delegate: delegate);

  final double itemExtent;
Ian Hickson's avatar
Ian Hickson committed
177 178

  @override
179
  RenderSliverFixedExtentList createRenderObject(BuildContext context) {
Adam Barth's avatar
Adam Barth committed
180
    final SliverMultiBoxAdaptorElement element = context;
181
    return new RenderSliverFixedExtentList(childManager: element, itemExtent: itemExtent);
Ian Hickson's avatar
Ian Hickson committed
182 183 184
  }

  @override
185
  void updateRenderObject(BuildContext context, RenderSliverFixedExtentList renderObject) {
Adam Barth's avatar
Adam Barth committed
186
    renderObject.itemExtent = itemExtent;
Ian Hickson's avatar
Ian Hickson committed
187
  }
Adam Barth's avatar
Adam Barth committed
188 189
}

190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
class SliverGrid extends SliverMultiBoxAdaptorWidget {
  SliverGrid({
    Key key,
    @required SliverChildDelegate delegate,
    @required this.gridDelegate,
  }) : super(key: key, delegate: delegate);

  final SliverGridDelegate gridDelegate;

  @override
  RenderSliverGrid createRenderObject(BuildContext context) {
    final SliverMultiBoxAdaptorElement element = context;
    return new RenderSliverGrid(childManager: element, gridDelegate: gridDelegate);
  }

  @override
  void updateRenderObject(BuildContext context, RenderSliverGrid renderObject) {
    renderObject.gridDelegate = gridDelegate;
  }

  @override
  double estimateMaxScrollOffset(
    SliverConstraints constraints,
    int firstIndex,
    int lastIndex,
    double leadingScrollOffset,
    double trailingScrollOffset,
  ) {
    return super.estimateMaxScrollOffset(
      constraints,
      firstIndex,
      lastIndex,
      leadingScrollOffset,
      trailingScrollOffset,
224
    ) ?? gridDelegate.getLayout(constraints).estimateMaxScrollOffset(delegate.estimatedChildCount);
225 226 227
  }
}

Adam Barth's avatar
Adam Barth committed
228 229 230 231 232 233 234 235 236 237 238 239 240
class SliverFill extends SliverMultiBoxAdaptorWidget {
  SliverFill({
    Key key,
    @required SliverChildDelegate delegate,
  }) : super(key: key, delegate: delegate);

  @override
  RenderSliverFill createRenderObject(BuildContext context) {
    final SliverMultiBoxAdaptorElement element = context;
    return new RenderSliverFill(childManager: element);
  }
}

Adam Barth's avatar
Adam Barth committed
241 242 243 244 245
class SliverMultiBoxAdaptorElement extends RenderObjectElement implements RenderSliverBoxChildManager {
  SliverMultiBoxAdaptorElement(SliverMultiBoxAdaptorWidget widget) : super(widget);

  @override
  SliverMultiBoxAdaptorWidget get widget => super.widget;
Ian Hickson's avatar
Ian Hickson committed
246 247

  @override
Adam Barth's avatar
Adam Barth committed
248 249 250 251 252
  RenderSliverMultiBoxAdaptor get renderObject => super.renderObject;

  @override
  void update(SliverMultiBoxAdaptorWidget newWidget) {
    final SliverMultiBoxAdaptorWidget oldWidget = widget;
Ian Hickson's avatar
Ian Hickson committed
253
    super.update(newWidget);
Adam Barth's avatar
Adam Barth committed
254 255
    final SliverChildDelegate newDelegate = newWidget.delegate;
    final SliverChildDelegate oldDelegate = oldWidget.delegate;
Ian Hickson's avatar
Ian Hickson committed
256 257 258 259 260 261 262 263 264 265 266 267 268 269
    if (newDelegate != oldDelegate &&
        (newDelegate.runtimeType != oldDelegate.runtimeType || newDelegate.shouldRebuild(oldDelegate)))
      performRebuild();
  }

  Map<int, Element> _childElements = new SplayTreeMap<int, Element>();
  Map<int, Widget> _childWidgets = new HashMap<int, Widget>();
  RenderBox _currentBeforeChild;

  @override
  void performRebuild() {
    _childWidgets.clear();
    super.performRebuild();
    _currentBeforeChild = null;
Adam Barth's avatar
Adam Barth committed
270
    assert(_currentlyUpdatingChildIndex == null);
Ian Hickson's avatar
Ian Hickson committed
271 272 273 274 275 276 277 278 279
    try {
      // The "toList()" below is to get a copy of the array so that we can
      // mutate _childElements within the loop. Basically we just update all the
      // same indexes as we had before. If any of them mutate the tree, then
      // this will also trigger a layout and so forth. (We won't call the
      // delegate's build function multiple times, though, because we cache the
      // delegate's results until the next time we need to rebuild the whole
      // block widget.)
      for (int index in _childElements.keys.toList()) {
Adam Barth's avatar
Adam Barth committed
280 281
        _currentlyUpdatingChildIndex = index;
        Element newChild = updateChild(_childElements[index], _build(index), index);
Ian Hickson's avatar
Ian Hickson committed
282 283 284 285 286 287 288 289
        if (newChild != null) {
          _childElements[index] = newChild;
          _currentBeforeChild = newChild.renderObject;
        } else {
          _childElements.remove(index);
        }
      }
    } finally {
Adam Barth's avatar
Adam Barth committed
290
      _currentlyUpdatingChildIndex = null;
Ian Hickson's avatar
Ian Hickson committed
291 292 293 294
    }
  }

  Widget _build(int index) {
295
    return _childWidgets.putIfAbsent(index, () => widget.delegate.build(this, index));
Ian Hickson's avatar
Ian Hickson committed
296 297
  }

Adam Barth's avatar
Adam Barth committed
298 299 300
  @override
  void createChild(int index, { @required RenderBox after }) {
    assert(_currentlyUpdatingChildIndex == null);
Ian Hickson's avatar
Ian Hickson committed
301
    owner.buildScope(this, () {
Adam Barth's avatar
Adam Barth committed
302
      final bool insertFirst = after == null;
Ian Hickson's avatar
Ian Hickson committed
303 304 305 306
      assert(insertFirst || _childElements[index-1] != null);
      _currentBeforeChild = insertFirst ? null : _childElements[index-1].renderObject;
      Element newChild;
      try {
Adam Barth's avatar
Adam Barth committed
307
        _currentlyUpdatingChildIndex = index;
Ian Hickson's avatar
Ian Hickson committed
308 309
        newChild = updateChild(_childElements[index], _build(index), index);
      } finally {
Adam Barth's avatar
Adam Barth committed
310
        _currentlyUpdatingChildIndex = null;
Ian Hickson's avatar
Ian Hickson committed
311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327
      }
      if (newChild != null) {
        _childElements[index] = newChild;
      } else {
        _childElements.remove(index);
      }
    });
  }

  @override
  void forgetChild(Element child) {
    assert(child != null);
    assert(child.slot != null);
    assert(_childElements.containsKey(child.slot));
    _childElements.remove(child.slot);
  }

Adam Barth's avatar
Adam Barth committed
328 329 330 331
  @override
  void removeChild(RenderBox child) {
    final int index = renderObject.indexOf(child);
    assert(_currentlyUpdatingChildIndex == null);
Ian Hickson's avatar
Ian Hickson committed
332 333 334 335
    assert(index >= 0);
    owner.buildScope(this, () {
      assert(_childElements.containsKey(index));
      try {
Adam Barth's avatar
Adam Barth committed
336 337
        _currentlyUpdatingChildIndex = index;
        final Element result = updateChild(_childElements[index], null, index);
Ian Hickson's avatar
Ian Hickson committed
338 339
        assert(result == null);
      } finally {
Adam Barth's avatar
Adam Barth committed
340
        _currentlyUpdatingChildIndex = null;
Ian Hickson's avatar
Ian Hickson committed
341 342 343 344 345 346
      }
      _childElements.remove(index);
      assert(!_childElements.containsKey(index));
    });
  }

347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363
  double _extrapolateMaxScrollOffset(
    int firstIndex,
    int lastIndex,
    double leadingScrollOffset,
    double trailingScrollOffset,
  ) {
    final int childCount = widget.delegate.estimatedChildCount;
    if (childCount == null)
      return double.INFINITY;
    if (lastIndex == childCount - 1)
      return trailingScrollOffset;
    final int reifiedCount = lastIndex - firstIndex + 1;
    final double averageExtent = (trailingScrollOffset - leadingScrollOffset) / reifiedCount;
    final int remainingCount = childCount - lastIndex - 1;
    return trailingScrollOffset + averageExtent * remainingCount;
  }

Adam Barth's avatar
Adam Barth committed
364
  @override
365
  double estimateMaxScrollOffset(SliverConstraints constraints, {
Adam Barth's avatar
Adam Barth committed
366 367 368 369 370
    int firstIndex,
    int lastIndex,
    double leadingScrollOffset,
    double trailingScrollOffset,
  }) {
371 372 373 374 375 376 377
    return widget.estimateMaxScrollOffset(
      constraints,
      firstIndex,
      lastIndex,
      leadingScrollOffset,
      trailingScrollOffset,
    ) ?? _extrapolateMaxScrollOffset(
Adam Barth's avatar
Adam Barth committed
378 379 380
      firstIndex,
      lastIndex,
      leadingScrollOffset,
381
      trailingScrollOffset,
Adam Barth's avatar
Adam Barth committed
382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399
    );
  }

  int _currentlyUpdatingChildIndex;

  @override
  bool debugAssertChildListLocked() {
    assert(_currentlyUpdatingChildIndex == null);
    return true;
  }

  @override
  void didAdoptChild(RenderBox child) {
    assert(_currentlyUpdatingChildIndex != null);
    final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
    childParentData.index = _currentlyUpdatingChildIndex;
  }

Ian Hickson's avatar
Ian Hickson committed
400 401
  @override
  void insertChildRenderObject(@checked RenderObject child, int slot) {
Adam Barth's avatar
Adam Barth committed
402 403
    assert(slot != null);
    assert(_currentlyUpdatingChildIndex == slot);
Ian Hickson's avatar
Ian Hickson committed
404 405
    renderObject.insert(child, after: _currentBeforeChild);
    assert(() {
Adam Barth's avatar
Adam Barth committed
406
      SliverMultiBoxAdaptorParentData childParentData = child.parentData;
Ian Hickson's avatar
Ian Hickson committed
407 408 409 410 411 412 413 414 415 416 417 418 419 420
      assert(slot == childParentData.index);
      return true;
    });
  }

  @override
  void moveChildRenderObject(@checked RenderObject child, int slot) {
    // TODO(ianh): At some point we should be better about noticing when a
    // particular LocalKey changes slot, and handle moving the nodes around.
    assert(false);
  }

  @override
  void removeChildRenderObject(@checked RenderObject child) {
Adam Barth's avatar
Adam Barth committed
421
    assert(_currentlyUpdatingChildIndex != null);
Ian Hickson's avatar
Ian Hickson committed
422 423 424 425 426 427 428 429 430 431 432
    renderObject.remove(child);
  }

  @override
  void visitChildren(ElementVisitor visitor) {
   // The toList() is to make a copy so that the underlying list can be modified by
   // the visitor:
   assert(!_childElements.values.any((Element child) => child == null));
    _childElements.values.toList().forEach(visitor);
  }
}