homogeneous_viewport.dart 6.26 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 8 9 10 11
import 'package:sky/src/rendering/block.dart';
import 'package:sky/src/rendering/box.dart';
import 'package:sky/src/rendering/object.dart';
import 'package:sky/src/widgets/framework.dart';
import 'package:sky/src/widgets/basic.dart';
12 13 14 15 16 17 18

typedef List<Widget> ListBuilder(int startIndex, int count);

class HomogeneousViewport extends RenderObjectWrapper {
  HomogeneousViewport({
    Key key,
    this.builder,
19
    this.itemsWrap: false,
20 21 22 23 24 25 26 27 28
    this.itemExtent, // required
    this.itemCount, // optional, but you cannot shrink-wrap this class or otherwise use its intrinsic dimensions if you don't specify it
    this.direction: ScrollDirection.vertical,
    this.startOffset: 0.0
  }) : super(key: key) {
    assert(itemExtent != null);
  }

  ListBuilder builder;
29
  bool itemsWrap;
30 31 32 33 34 35
  double itemExtent;
  int itemCount;
  ScrollDirection direction;
  double startOffset;

  List<Widget> _children;
36 37 38
  bool _layoutDirty = true;
  int _layoutFirstIndex;
  int _layoutItemCount;
39 40 41 42 43 44 45 46 47

  RenderBlockViewport get renderObject => super.renderObject;

  RenderBlockViewport createNode() {
    // 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 result = new RenderBlockViewport();
    result.callback = layout;
    result.totalExtentCallback = getTotalExtent;
48 49
    result.minCrossAxisExtentCallback = getMinCrossAxisExtent;
    result.maxCrossAxisExtentCallback = getMaxCrossAxisExtent;
50 51 52 53 54 55
    return result;
  }

  void remove() {
    renderObject.callback = null;
    renderObject.totalExtentCallback = null;
56 57
    renderObject.minCrossAxisExtentCallback = null;
    renderObject.maxCrossAxisExtentCallback = null;
58 59 60
    super.remove();
    _children.clear();
    _layoutDirty = true;
61 62 63 64 65
    assert(() {
      _layoutFirstIndex = null;
      _layoutItemCount = null;
      return true;
    });
66 67 68
  }

  void walkChildren(WidgetTreeWalker walker) {
69 70
    if (_children == null)
      return;
71 72 73 74 75
    for (Widget child in _children)
      walker(child);
  }

  void insertChildRenderObject(RenderObjectWrapper child, Widget slot) {
76
    RenderObject nextSibling = slot?.renderObject;
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
    renderObject.add(child.renderObject, before: nextSibling);
  }

  void detachChildRenderObject(RenderObjectWrapper child) {
    renderObject.remove(child.renderObject);
  }

  bool retainStatefulNodeIfPossible(HomogeneousViewport newNode) {
    retainStatefulRenderObjectWrapper(newNode);
    if (startOffset != newNode.startOffset) {
      _layoutDirty = true;
      startOffset = newNode.startOffset;
    }
    if (itemCount != newNode.itemCount) {
      _layoutDirty = true;
      itemCount = newNode.itemCount;
    }
94 95 96 97
    if (itemsWrap != newNode.itemsWrap) {
      _layoutDirty = true;
      itemsWrap = newNode.itemsWrap;
    }
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
    if (itemExtent != newNode.itemExtent) {
      _layoutDirty = true;
      itemExtent = newNode.itemExtent;
    }
    if (direction != newNode.direction) {
      _layoutDirty = true;
      direction = newNode.direction;
    }
    if (builder != newNode.builder) {
      _layoutDirty = true;
      builder = newNode.builder;
    }
    return true;
  }

  // This is called during the regular component build
  void syncRenderObject(HomogeneousViewport old) {
    super.syncRenderObject(old);
    if (_layoutDirty) {
      renderObject.markNeedsLayout();
    } else {
      assert(old != null); // if old was null, we'd be new, and therefore _layoutDirty would be true
      _updateChildren();
    }
  }

  void layout(BoxConstraints constraints) {
    LayoutCallbackBuilderHandle handle = enterLayoutCallbackBuilder();
    try {
      double mainAxisExtent = direction == ScrollDirection.vertical ? constraints.maxHeight : constraints.maxWidth;
      double offset;
129
      if (startOffset <= 0.0 && !itemsWrap) {
130 131 132
        _layoutFirstIndex = 0;
        offset = -startOffset;
      } else {
133
        _layoutFirstIndex = (startOffset / itemExtent).floor();
134 135 136 137
        offset = -(startOffset % itemExtent);
      }
      if (mainAxisExtent < double.INFINITY) {
        _layoutItemCount = ((mainAxisExtent - offset) / itemExtent).ceil();
138
        if (itemCount != null && !itemsWrap)
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 172 173 174 175 176 177 178 179
          _layoutItemCount = math.min(_layoutItemCount, itemCount - _layoutFirstIndex);
      } 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.';
          return itemCount != null;
        });
        _layoutItemCount = itemCount - _layoutFirstIndex;
      }
      _layoutItemCount = math.max(0, _layoutItemCount);
      _updateChildren();
      // Update the renderObject configuration
      renderObject.direction = direction == ScrollDirection.vertical ? BlockDirection.vertical : BlockDirection.horizontal;
      renderObject.itemExtent = itemExtent;
      renderObject.minExtent = getTotalExtent(null);
      renderObject.startOffset = offset;
    } finally {
      exitLayoutCallbackBuilder(handle);
    }
  }

  void _updateChildren() {
    assert(_layoutFirstIndex != null);
    assert(_layoutItemCount != null);
    List<Widget> newChildren;
    if (_layoutItemCount > 0)
      newChildren = builder(_layoutFirstIndex, _layoutItemCount);
    else
      newChildren = <Widget>[];
    syncChildren(newChildren, _children == null ? <Widget>[] : _children);
    _children = newChildren;
  }

  double getTotalExtent(BoxConstraints constraints) {
    // constraints is null when called by layout() above
    return itemCount != null ? itemCount * itemExtent : double.INFINITY;
  }

180
  double getMinCrossAxisExtent(BoxConstraints constraints) {
181 182 183
    return 0.0;
  }

184
  double getMaxCrossAxisExtent(BoxConstraints constraints) {
185 186 187 188 189 190
    if (direction == ScrollDirection.vertical)
      return constraints.maxWidth;
    return constraints.maxHeight;
  }

}