homogeneous_viewport.dart 11.4 KB
Newer Older
1 2 3 4 5 6
// Copyright 2015 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:math' as math;

7
import 'package:flutter/rendering.dart';
8 9 10

import 'framework.dart';
import 'basic.dart';
11

12
typedef List<Widget> ListBuilder(BuildContext context, int startIndex, int count);
13

Hans Muller's avatar
Hans Muller committed
14 15
abstract class _ViewportBase extends RenderObjectWidget {
  _ViewportBase({
16 17
    Key key,
    this.builder,
18
    this.itemsWrap: false,
Hans Muller's avatar
Hans Muller committed
19
    this.itemCount,
20
    this.direction: ScrollDirection.vertical,
21 22
    this.startOffset: 0.0,
    this.overlayPainter
Hans Muller's avatar
Hans Muller committed
23
  }) : super(key: key);
24

25 26 27 28 29
  final ListBuilder builder;
  final bool itemsWrap;
  final int itemCount;
  final ScrollDirection direction;
  final double startOffset;
30
  final Painter overlayPainter;
31 32 33 34 35

  // we don't pass constructor arguments to the RenderBlockViewport() because until
  // we know our children, the constructor arguments we could give have no effect
  RenderBlockViewport createRenderObject() => new RenderBlockViewport();

Hans Muller's avatar
Hans Muller committed
36
  bool isLayoutDifferentThan(_ViewportBase oldWidget) {
37
    // changing the builder doesn't imply the layout changed
38 39 40 41 42 43 44
    return itemsWrap != oldWidget.itemsWrap ||
           itemCount != oldWidget.itemCount ||
           direction != oldWidget.direction ||
           startOffset != oldWidget.startOffset;
  }
}

Hans Muller's avatar
Hans Muller committed
45 46
abstract class _ViewportBaseElement<T extends _ViewportBase> extends RenderObjectElement<T> {
  _ViewportBaseElement(T widget) : super(widget);
47

48
  List<Element> _children = const <Element>[];
49 50
  int _layoutFirstIndex;
  int _layoutItemCount;
51 52 53

  RenderBlockViewport get renderObject => super.renderObject;

54
  void visitChildren(ElementVisitor visitor) {
55 56
    if (_children == null)
      return;
57 58
    for (Element child in _children)
      visitor(child);
59 60
  }

61 62 63 64 65 66
  void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    renderObject.callback = layout;
    renderObject.totalExtentCallback = getTotalExtent;
    renderObject.minCrossAxisExtentCallback = getMinCrossAxisExtent;
    renderObject.maxCrossAxisExtentCallback = getMaxCrossAxisExtent;
67
    renderObject.overlayPainter = widget.overlayPainter;
68 69
  }

70 71 72 73 74
  void unmount() {
    renderObject.callback = null;
    renderObject.totalExtentCallback = null;
    renderObject.minCrossAxisExtentCallback = null;
    renderObject.maxCrossAxisExtentCallback = null;
75
    renderObject.overlayPainter = null;
76
    super.unmount();
77 78
  }

Hans Muller's avatar
Hans Muller committed
79
  void update(T newWidget) {
80 81 82
    bool needLayout = newWidget.isLayoutDifferentThan(widget);
    super.update(newWidget);
    if (needLayout)
83
      renderObject.markNeedsLayout();
84
    else
85 86 87
      _updateChildren();
  }

88 89 90 91
  void reinvokeBuilders() {
    _updateChildren();
  }

Hans Muller's avatar
Hans Muller committed
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
  void layout(BoxConstraints constraints);

  void _updateChildren() {
    assert(_layoutFirstIndex != null);
    assert(_layoutItemCount != null);
    List<Widget> newWidgets;
    if (_layoutItemCount > 0)
      newWidgets = widget.builder(this, _layoutFirstIndex, _layoutItemCount).map((Widget widget) {
        return new RepaintBoundary(key: new ValueKey<Key>(widget.key), child: widget);
      }).toList();
    else
      newWidgets = <Widget>[];
    _children = updateChildren(_children, newWidgets);
  }

  double getTotalExtent(BoxConstraints constraints);

  double getMinCrossAxisExtent(BoxConstraints constraints) {
    return 0.0;
  }

  double getMaxCrossAxisExtent(BoxConstraints constraints) {
    if (widget.direction == ScrollDirection.vertical)
      return constraints.maxWidth;
    return constraints.maxHeight;
  }

  void insertChildRenderObject(RenderObject child, Element slot) {
    RenderObject nextSibling = slot?.renderObject;
    renderObject.add(child, before: nextSibling);
  }

  void moveChildRenderObject(RenderObject child, Element slot) {
    assert(child.parent == renderObject);
    RenderObject nextSibling = slot?.renderObject;
    renderObject.move(child, before: nextSibling);
  }

  void removeChildRenderObject(RenderObject child) {
    assert(child.parent == renderObject);
    renderObject.remove(child);
  }

}

class HomogeneousViewport extends _ViewportBase {
  HomogeneousViewport({
    Key key,
    ListBuilder builder,
    bool itemsWrap: false,
    int itemCount, // optional, but you cannot shrink-wrap this class or otherwise use its intrinsic dimensions if you don't specify it
    ScrollDirection direction: ScrollDirection.vertical,
    double startOffset: 0.0,
    Painter overlayPainter,
    this.itemExtent // required, must be non-zero
  }) : super(
    key: key,
    builder: builder,
    itemsWrap: itemsWrap,
    itemCount: itemCount,
    direction: direction,
    startOffset: startOffset,
    overlayPainter: overlayPainter
  ) {
    assert(itemExtent != null);
    assert(itemExtent > 0);
  }

  final double itemExtent;

  _HomogeneousViewportElement createElement() => new _HomogeneousViewportElement(this);

  bool isLayoutDifferentThan(HomogeneousViewport oldWidget) {
    return itemExtent != oldWidget.itemExtent || super.isLayoutDifferentThan(oldWidget);
  }
}

class _HomogeneousViewportElement extends _ViewportBaseElement<HomogeneousViewport> {
  _HomogeneousViewportElement(HomogeneousViewport widget) : super(widget);

172
  void layout(BoxConstraints constraints) {
173 174 175 176 177 178 179 180 181 182
    // We enter a build scope (meaning that markNeedsBuild() is forbidden)
    // because we are in the middle of layout and if we allowed people to set
    // state, they'd expect to have that state reflected immediately, which, if
    // we were to try to honour it, would potentially result in assertions
    // because you can't normally mutate the render object tree during layout.
    // (If there were a way to limit these writes to descendants of this, it'd
    // be ok because we are exempt from that assert since we are still actively
    // doing our own layout.)
    BuildableElement.lockState(() {
      double mainAxisExtent = widget.direction == ScrollDirection.vertical ? constraints.maxHeight : constraints.maxWidth;
183
      double offset;
184
      if (widget.startOffset <= 0.0 && !widget.itemsWrap) {
185
        _layoutFirstIndex = 0;
186
        offset = -widget.startOffset;
187
      } else {
188 189
        _layoutFirstIndex = (widget.startOffset / widget.itemExtent).floor();
        offset = -(widget.startOffset % widget.itemExtent);
190 191
      }
      if (mainAxisExtent < double.INFINITY) {
192 193 194
        _layoutItemCount = ((mainAxisExtent - offset) / widget.itemExtent).ceil();
        if (widget.itemCount != null && !widget.itemsWrap)
          _layoutItemCount = math.min(_layoutItemCount, widget.itemCount - _layoutFirstIndex);
195 196 197 198 199 200 201
      } else {
        assert(() {
          'This HomogeneousViewport has no specified number of items (meaning it has infinite items), ' +
          'and has been placed in an unconstrained environment where all items can be rendered. ' +
          'It is most likely that you have placed your HomogeneousViewport (which is an internal ' +
          'component of several scrollable widgets) inside either another scrolling box, a flexible ' +
          'box (Row, Column), or a Stack, without giving it a specific size.';
202
          return widget.itemCount != null;
203
        });
204
        _layoutItemCount = widget.itemCount - _layoutFirstIndex;
205 206 207 208
      }
      _layoutItemCount = math.max(0, _layoutItemCount);
      _updateChildren();
      // Update the renderObject configuration
209 210
      renderObject.direction = widget.direction == ScrollDirection.vertical ? BlockDirection.vertical : BlockDirection.horizontal;
      renderObject.itemExtent = widget.itemExtent;
211 212
      renderObject.minExtent = getTotalExtent(null);
      renderObject.startOffset = offset;
213
      renderObject.overlayPainter = widget.overlayPainter;
214
    }, building: true);
215 216 217 218
  }

  double getTotalExtent(BoxConstraints constraints) {
    // constraints is null when called by layout() above
219
    return widget.itemCount != null ? widget.itemCount * widget.itemExtent : double.INFINITY;
220
  }
Hans Muller's avatar
Hans Muller committed
221
}
222

Hans Muller's avatar
Hans Muller committed
223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240
class HomogeneousPageViewport extends _ViewportBase {
  HomogeneousPageViewport({
    Key key,
    ListBuilder builder,
    bool itemsWrap: false,
    int itemCount, // optional, but you cannot shrink-wrap this class or otherwise use its intrinsic dimensions if you don't specify it
    ScrollDirection direction: ScrollDirection.vertical,
    double startOffset: 0.0,
    Painter overlayPainter
  }) : super(
    key: key,
    builder: builder,
    itemsWrap: itemsWrap,
    itemCount: itemCount,
    direction: direction,
    startOffset: startOffset,
    overlayPainter: overlayPainter
   );
241

Hans Muller's avatar
Hans Muller committed
242 243
  _HomogeneousPageViewportElement createElement() => new _HomogeneousPageViewportElement(this);
}
244

Hans Muller's avatar
Hans Muller committed
245 246
class _HomogeneousPageViewportElement extends _ViewportBaseElement<HomogeneousPageViewport> {
  _HomogeneousPageViewportElement(HomogeneousPageViewport widget) : super(widget);
247

Hans Muller's avatar
Hans Muller committed
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266
  void layout(BoxConstraints constraints) {
    // We enter a build scope (meaning that markNeedsBuild() is forbidden)
    // because we are in the middle of layout and if we allowed people to set
    // state, they'd expect to have that state reflected immediately, which, if
    // we were to try to honour it, would potentially result in assertions
    // because you can't normally mutate the render object tree during layout.
    // (If there were a way to limit these writes to descendants of this, it'd
    // be ok because we are exempt from that assert since we are still actively
    // doing our own layout.)
    BuildableElement.lockState(() {
      double itemExtent = widget.direction == ScrollDirection.vertical ? constraints.maxHeight : constraints.maxWidth;
      double offset;
      if (widget.startOffset <= 0.0 && !widget.itemsWrap) {
        _layoutFirstIndex = 0;
        offset = -widget.startOffset * itemExtent;
      } else {
        _layoutFirstIndex = widget.startOffset.floor();
        offset = -((widget.startOffset * itemExtent) % itemExtent);
      }
267 268 269 270
      if (itemExtent < double.INFINITY && widget.itemCount != null) {
        final double contentExtent = itemExtent * widget.itemCount;
        _layoutItemCount = contentExtent == 0.0 ? 0 : ((contentExtent - offset) / contentExtent).ceil();
        if (!widget.itemsWrap)
Hans Muller's avatar
Hans Muller committed
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291
          _layoutItemCount = math.min(_layoutItemCount, widget.itemCount - _layoutFirstIndex);
      } else {
        assert(() {
          'This HomogeneousPageViewport has no specified number of items (meaning it has infinite items), ' +
          'and has been placed in an unconstrained environment where all items can be rendered. ' +
          'It is most likely that you have placed your HomogeneousPageViewport (which is an internal ' +
          'component of several scrollable widgets) inside either another scrolling box, a flexible ' +
          'box (Row, Column), or a Stack, without giving it a specific size.';
          return widget.itemCount != null;
        });
        _layoutItemCount = widget.itemCount - _layoutFirstIndex;
      }
      _layoutItemCount = math.max(0, _layoutItemCount);
      _updateChildren();
      // Update the renderObject configuration
      renderObject.direction = widget.direction == ScrollDirection.vertical ? BlockDirection.vertical : BlockDirection.horizontal;
      renderObject.itemExtent = itemExtent;
      renderObject.minExtent = itemExtent;
      renderObject.startOffset = offset;
      renderObject.overlayPainter = widget.overlayPainter;
    }, building: true);
292 293
  }

Hans Muller's avatar
Hans Muller committed
294 295 296
  double getTotalExtent(BoxConstraints constraints) {
    double itemExtent = widget.direction == ScrollDirection.vertical ? constraints.maxHeight : constraints.maxWidth;
    return widget.itemCount != null ? widget.itemCount * itemExtent : double.INFINITY;
297
  }
298
}