sliver.dart 14.3 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1 2 3 4 5 6 7 8 9 10
// 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 'basic.dart';
11
import 'framework.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
  ) => null;

41
  bool shouldRebuild(covariant SliverChildDelegate oldDelegate);
42 43 44

  @override
  String toString() {
45
    final List<String> description = <String>[];
46 47 48 49 50 51 52 53 54 55 56 57 58 59
    debugFillDescription(description);
    return '$runtimeType#$hashCode(${description.join(", ")})';
  }

  @protected
  void debugFillDescription(List<String> description) {
    try {
      final int children = estimatedChildCount;
      if (children != null)
        description.add('estimated child count: $children');
    } catch (e) {
      description.add('estimated child count: EXCEPTION (${e.runtimeType})');
    }
  }
Ian Hickson's avatar
Ian Hickson committed
60 61
}

62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
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
84
  bool shouldRebuild(covariant SliverChildBuilderDelegate oldDelegate) => true;
85 86
}

Ian Hickson's avatar
Ian Hickson committed
87 88 89
// ///
// /// 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
90
// /// [SliverChildDelegate] directly.
Ian Hickson's avatar
Ian Hickson committed
91 92 93 94 95 96 97 98
// ///
// /// 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
99
class SliverChildListDelegate extends SliverChildDelegate {
100
  const SliverChildListDelegate(this.children, { this.addRepaintBoundaries: true });
Ian Hickson's avatar
Ian Hickson committed
101

102 103 104 105 106 107 108 109 110 111 112 113
  /// 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
114 115 116 117 118 119 120
  final List<Widget> children;

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

126 127 128
  @override
  int get estimatedChildCount => children.length;

Ian Hickson's avatar
Ian Hickson committed
129
  @override
130
  bool shouldRebuild(covariant SliverChildListDelegate oldDelegate) {
Ian Hickson's avatar
Ian Hickson committed
131 132 133 134
    return children != oldDelegate.children;
  }
}

Adam Barth's avatar
Adam Barth committed
135 136
abstract class SliverMultiBoxAdaptorWidget extends RenderObjectWidget {
  SliverMultiBoxAdaptorWidget({
Ian Hickson's avatar
Ian Hickson committed
137 138 139 140 141 142
    Key key,
    @required this.delegate,
  }) : super(key: key) {
    assert(delegate != null);
  }

Adam Barth's avatar
Adam Barth committed
143
  final SliverChildDelegate delegate;
Ian Hickson's avatar
Ian Hickson committed
144 145

  @override
Adam Barth's avatar
Adam Barth committed
146
  SliverMultiBoxAdaptorElement createElement() => new SliverMultiBoxAdaptorElement(this);
Ian Hickson's avatar
Ian Hickson committed
147 148

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

151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
  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
167 168 169 170 171 172 173
  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('delegate: $delegate');
  }
}

174 175
class SliverList extends SliverMultiBoxAdaptorWidget {
  SliverList({
Adam Barth's avatar
Adam Barth committed
176 177 178
    Key key,
    @required SliverChildDelegate delegate,
  }) : super(key: key, delegate: delegate);
Ian Hickson's avatar
Ian Hickson committed
179 180

  @override
181
  RenderSliverList createRenderObject(BuildContext context) {
Adam Barth's avatar
Adam Barth committed
182
    final SliverMultiBoxAdaptorElement element = context;
183
    return new RenderSliverList(childManager: element);
Adam Barth's avatar
Adam Barth committed
184 185
  }
}
Ian Hickson's avatar
Ian Hickson committed
186

187 188
class SliverFixedExtentList extends SliverMultiBoxAdaptorWidget {
  SliverFixedExtentList({
Adam Barth's avatar
Adam Barth committed
189 190 191 192 193 194
    Key key,
    @required SliverChildDelegate delegate,
    @required this.itemExtent,
  }) : super(key: key, delegate: delegate);

  final double itemExtent;
Ian Hickson's avatar
Ian Hickson committed
195 196

  @override
197
  RenderSliverFixedExtentList createRenderObject(BuildContext context) {
Adam Barth's avatar
Adam Barth committed
198
    final SliverMultiBoxAdaptorElement element = context;
199
    return new RenderSliverFixedExtentList(childManager: element, itemExtent: itemExtent);
Ian Hickson's avatar
Ian Hickson committed
200 201 202
  }

  @override
203
  void updateRenderObject(BuildContext context, RenderSliverFixedExtentList renderObject) {
Adam Barth's avatar
Adam Barth committed
204
    renderObject.itemExtent = itemExtent;
Ian Hickson's avatar
Ian Hickson committed
205
  }
Adam Barth's avatar
Adam Barth committed
206 207
}

208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241
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,
242
    ) ?? gridDelegate.getLayout(constraints).estimateMaxScrollOffset(delegate.estimatedChildCount);
243 244 245
  }
}

Adam Barth's avatar
Adam Barth committed
246 247 248 249
class SliverFill extends SliverMultiBoxAdaptorWidget {
  SliverFill({
    Key key,
    @required SliverChildDelegate delegate,
250 251 252 253 254 255 256
    this.viewportFraction: 1.0,
  }) : super(key: key, delegate: delegate) {
    assert(viewportFraction != null);
    assert(viewportFraction > 0.0);
  }

  final double viewportFraction;
Adam Barth's avatar
Adam Barth committed
257 258 259 260

  @override
  RenderSliverFill createRenderObject(BuildContext context) {
    final SliverMultiBoxAdaptorElement element = context;
261 262 263 264 265 266
    return new RenderSliverFill(childManager: element, viewportFraction: viewportFraction);
  }

  @override
  void updateRenderObject(BuildContext context, RenderSliverFill renderObject) {
    renderObject.viewportFraction = viewportFraction;
Adam Barth's avatar
Adam Barth committed
267 268 269
  }
}

Adam Barth's avatar
Adam Barth committed
270 271 272 273 274
class SliverMultiBoxAdaptorElement extends RenderObjectElement implements RenderSliverBoxChildManager {
  SliverMultiBoxAdaptorElement(SliverMultiBoxAdaptorWidget widget) : super(widget);

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

  @override
Adam Barth's avatar
Adam Barth committed
277 278 279 280 281
  RenderSliverMultiBoxAdaptor get renderObject => super.renderObject;

  @override
  void update(SliverMultiBoxAdaptorWidget newWidget) {
    final SliverMultiBoxAdaptorWidget oldWidget = widget;
Ian Hickson's avatar
Ian Hickson committed
282
    super.update(newWidget);
Adam Barth's avatar
Adam Barth committed
283 284
    final SliverChildDelegate newDelegate = newWidget.delegate;
    final SliverChildDelegate oldDelegate = oldWidget.delegate;
Ian Hickson's avatar
Ian Hickson committed
285 286 287 288 289
    if (newDelegate != oldDelegate &&
        (newDelegate.runtimeType != oldDelegate.runtimeType || newDelegate.shouldRebuild(oldDelegate)))
      performRebuild();
  }

290
  SplayTreeMap<int, Element> _childElements = new SplayTreeMap<int, Element>();
Ian Hickson's avatar
Ian Hickson committed
291 292 293 294 295 296 297 298
  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
299
    assert(_currentlyUpdatingChildIndex == null);
Ian Hickson's avatar
Ian Hickson committed
300
    try {
301 302 303 304 305 306 307 308 309 310 311 312
      int firstIndex = _childElements.firstKey();
      int lastIndex = _childElements.lastKey();
      if (_childElements.isEmpty) {
        firstIndex = 0;
        lastIndex = 0;
      } else if (_didUnderflow) {
        lastIndex += 1;
      }
      // We won't call the delegate's build function multiple times, because we
      // cache the delegate's results until the next time we need to rebuild the
      // whole widget.
      for (int index = firstIndex; index <= lastIndex; ++index) {
Adam Barth's avatar
Adam Barth committed
313
        _currentlyUpdatingChildIndex = index;
314
        final Element newChild = updateChild(_childElements[index], _build(index), index);
Ian Hickson's avatar
Ian Hickson committed
315 316 317 318 319 320 321 322
        if (newChild != null) {
          _childElements[index] = newChild;
          _currentBeforeChild = newChild.renderObject;
        } else {
          _childElements.remove(index);
        }
      }
    } finally {
Adam Barth's avatar
Adam Barth committed
323
      _currentlyUpdatingChildIndex = null;
Ian Hickson's avatar
Ian Hickson committed
324 325 326 327
    }
  }

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

Adam Barth's avatar
Adam Barth committed
331 332 333
  @override
  void createChild(int index, { @required RenderBox after }) {
    assert(_currentlyUpdatingChildIndex == null);
Ian Hickson's avatar
Ian Hickson committed
334
    owner.buildScope(this, () {
Adam Barth's avatar
Adam Barth committed
335
      final bool insertFirst = after == null;
Ian Hickson's avatar
Ian Hickson committed
336 337 338 339
      assert(insertFirst || _childElements[index-1] != null);
      _currentBeforeChild = insertFirst ? null : _childElements[index-1].renderObject;
      Element newChild;
      try {
Adam Barth's avatar
Adam Barth committed
340
        _currentlyUpdatingChildIndex = index;
Ian Hickson's avatar
Ian Hickson committed
341 342
        newChild = updateChild(_childElements[index], _build(index), index);
      } finally {
Adam Barth's avatar
Adam Barth committed
343
        _currentlyUpdatingChildIndex = null;
Ian Hickson's avatar
Ian Hickson committed
344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360
      }
      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
361 362 363 364
  @override
  void removeChild(RenderBox child) {
    final int index = renderObject.indexOf(child);
    assert(_currentlyUpdatingChildIndex == null);
Ian Hickson's avatar
Ian Hickson committed
365 366 367 368
    assert(index >= 0);
    owner.buildScope(this, () {
      assert(_childElements.containsKey(index));
      try {
Adam Barth's avatar
Adam Barth committed
369 370
        _currentlyUpdatingChildIndex = index;
        final Element result = updateChild(_childElements[index], null, index);
Ian Hickson's avatar
Ian Hickson committed
371 372
        assert(result == null);
      } finally {
Adam Barth's avatar
Adam Barth committed
373
        _currentlyUpdatingChildIndex = null;
Ian Hickson's avatar
Ian Hickson committed
374 375 376 377 378 379
      }
      _childElements.remove(index);
      assert(!_childElements.containsKey(index));
    });
  }

380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396
  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
397
  @override
398
  double estimateMaxScrollOffset(SliverConstraints constraints, {
Adam Barth's avatar
Adam Barth committed
399 400 401 402 403
    int firstIndex,
    int lastIndex,
    double leadingScrollOffset,
    double trailingScrollOffset,
  }) {
404 405 406 407 408 409 410
    return widget.estimateMaxScrollOffset(
      constraints,
      firstIndex,
      lastIndex,
      leadingScrollOffset,
      trailingScrollOffset,
    ) ?? _extrapolateMaxScrollOffset(
Adam Barth's avatar
Adam Barth committed
411 412 413
      firstIndex,
      lastIndex,
      leadingScrollOffset,
414
      trailingScrollOffset,
Adam Barth's avatar
Adam Barth committed
415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432
    );
  }

  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;
  }

433 434 435 436 437 438 439
  bool _didUnderflow = false;

  @override
  void setDidUnderflow(bool value) {
    _didUnderflow = value;
  }

Ian Hickson's avatar
Ian Hickson committed
440
  @override
441
  void insertChildRenderObject(covariant RenderObject child, int slot) {
Adam Barth's avatar
Adam Barth committed
442 443
    assert(slot != null);
    assert(_currentlyUpdatingChildIndex == slot);
Ian Hickson's avatar
Ian Hickson committed
444 445
    renderObject.insert(child, after: _currentBeforeChild);
    assert(() {
446
      final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
Ian Hickson's avatar
Ian Hickson committed
447 448 449 450 451 452
      assert(slot == childParentData.index);
      return true;
    });
  }

  @override
453
  void moveChildRenderObject(covariant RenderObject child, int slot) {
Ian Hickson's avatar
Ian Hickson committed
454 455 456 457 458 459
    // 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
460
  void removeChildRenderObject(covariant RenderObject child) {
Adam Barth's avatar
Adam Barth committed
461
    assert(_currentlyUpdatingChildIndex != null);
Ian Hickson's avatar
Ian Hickson committed
462 463 464 465 466 467 468 469 470 471 472
    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);
  }
}