scrollable_grid.dart 7.04 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:collection/collection.dart' show lowerBound;
8
import 'package:flutter/foundation.dart';
9 10
import 'package:flutter/rendering.dart';

11
import 'framework.dart';
12
import 'scroll_configuration.dart';
13
import 'scrollable.dart';
Adam Barth's avatar
Adam Barth committed
14
import 'virtual_viewport.dart';
15 16 17

/// A vertically scrollable grid.
///
18 19 20 21
/// Requires that [delegate] places its children in row-major order.
///
/// See also:
///
22 23 24
///  * [CustomGrid].
///  * [ScrollableList].
///  * [ScrollableViewport].
25
class ScrollableGrid extends StatelessWidget {
26 27 28
  /// Creates a vertically scrollable grid.
  ///
  /// The [delegate] argument must not be null.
29 30
  ScrollableGrid({
    Key key,
31 32 33 34 35 36
    this.initialScrollOffset,
    this.onScrollStart,
    this.onScroll,
    this.onScrollEnd,
    this.snapOffsetCallback,
    this.scrollableKey,
37
    @required this.delegate,
38
    this.children: const <Widget>[],
39 40 41
  }) : super(key: key) {
    assert(delegate != null);
  }
42

Hans Muller's avatar
Hans Muller committed
43 44 45 46
  // Warning: keep the dartdoc comments that follow in sync with the copies in
  // Scrollable, LazyBlock, ScrollableViewport, ScrollableList, and
  // ScrollableLazyList. And see: https://github.com/dart-lang/dartdoc/issues/1161.

47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
  /// The scroll offset this widget should use when first created.
  final double initialScrollOffset;

  /// Called whenever this widget starts to scroll.
  final ScrollListener onScrollStart;

  /// Called whenever this widget's scroll offset changes.
  final ScrollListener onScroll;

  /// Called whenever this widget stops scrolling.
  final ScrollListener onScrollEnd;

  /// Called to determine the offset to which scrolling should snap,
  /// when handling a fling.
  ///
  /// This callback, if set, will be called with the offset that the
  /// Scrollable would have scrolled to in the absence of this
  /// callback, and a Size describing the size of the Scrollable
  /// itself.
  ///
  /// The callback's return value is used as the new scroll offset to
  /// aim for.
  ///
  /// If the callback simply returns its first argument (the offset),
  /// then it is as if the callback was null.
  final SnapOffsetCallback snapOffsetCallback;

  /// The key for the Scrollable created by this widget.
  final Key scrollableKey;
76

77
  /// The delegate that controls the layout of the children.
78 79 80 81
  ///
  /// For example, a [FixedColumnCountGridDelegate] for grids that have a fixed
  /// number of columns or a [MaxTileWidthGridDelegate] for grids that have a
  /// maximum tile width.
82
  final GridDelegate delegate;
83 84

  /// The children that will be placed in the grid.
85
  final Iterable<Widget> children;
86

87
  Widget _buildViewport(BuildContext context, ScrollableState state) {
88
    return new GridViewport(
89
      scrollOffset: state.scrollOffset,
90
      delegate: delegate,
91
      onExtentsChanged: state.handleExtentsChanged,
92 93 94 95 96 97
      children: children
    );
  }

  @override
  Widget build(BuildContext context) {
98
    final Widget result = new Scrollable(
99 100 101 102 103 104 105 106 107 108
      key: scrollableKey,
      initialScrollOffset: initialScrollOffset,
      // TODO(abarth): Support horizontal offsets. For horizontally scrolling
      // grids. For horizontally scrolling grids, we'll probably need to use a
      // delegate that places children in column-major order.
      scrollDirection: Axis.vertical,
      onScrollStart: onScrollStart,
      onScroll: onScroll,
      onScrollEnd: onScrollEnd,
      snapOffsetCallback: snapOffsetCallback,
109
      builder: _buildViewport,
110
    );
111
    return ScrollConfiguration.wrap(context, result);
112 113 114
  }
}

115 116 117 118 119 120
/// A virtual viewport onto a grid of widgets.
///
/// Used by [ScrollableGrid].
///
/// See also:
///
121 122
///  * [ListViewport].
///  * [LazyListViewport].
123
class GridViewport extends VirtualViewportFromIterable {
124 125 126
  /// Creates a virtual viewport onto a grid of widgets.
  ///
  /// The [delegate] argument must not be null.
127
  GridViewport({
128
    this.scrollOffset,
129 130
    this.delegate,
    this.onExtentsChanged,
131
    this.children: const <Widget>[],
132 133 134
  }) {
    assert(delegate != null);
  }
135

136 137 138
  /// The [startOffset] without taking the [delegate]'s padding into account.
  final double scrollOffset;

139
  @override
140 141 142 143 144
  double get startOffset {
    if (delegate == null)
      return scrollOffset;
    return scrollOffset - delegate.padding.top;
  }
145 146

  /// The delegate that controls the layout of the children.
147 148 149 150
  ///
  /// For example, a [FixedColumnCountGridDelegate] for grids that have a fixed
  /// number of columns or a [MaxTileWidthGridDelegate] for grids that have a
  /// maximum tile width.
151
  final GridDelegate delegate;
152 153

  /// Called when the interior or exterior dimensions of the viewport change.
154
  final ExtentsChangedCallback onExtentsChanged;
155 156

  @override
157
  final Iterable<Widget> children;
158

159
  @override
160
  RenderGrid createRenderObject(BuildContext context) => new RenderGrid(delegate: delegate);
161

162
  @override
163 164 165
  _GridViewportElement createElement() => new _GridViewportElement(this);
}

166
class _GridViewportElement extends VirtualViewportElement {
167 168
  _GridViewportElement(GridViewport widget) : super(widget);

169
  @override
170 171
  GridViewport get widget => super.widget;

172
  @override
Adam Barth's avatar
Adam Barth committed
173
  RenderGrid get renderObject => super.renderObject;
174

175
  @override
Adam Barth's avatar
Adam Barth committed
176
  int get materializedChildBase => _materializedChildBase;
177 178
  int _materializedChildBase;

179
  @override
Adam Barth's avatar
Adam Barth committed
180 181
  int get materializedChildCount => _materializedChildCount;
  int _materializedChildCount;
182

183
  @override
184 185
  double get startOffsetBase => _startOffsetBase;
  double _startOffsetBase;
186

187
  @override
188 189
  double get startOffsetLimit =>_startOffsetLimit;
  double _startOffsetLimit;
190

191
  @override
192
  void updateRenderObject(GridViewport oldWidget) {
193
    renderObject.delegate = widget.delegate;
194
    super.updateRenderObject(oldWidget);
195 196
  }

197 198
  double _lastReportedContentExtent;
  double _lastReportedContainerExtent;
Adam Barth's avatar
Adam Barth committed
199
  GridSpecification _specification;
200

201
  @override
202 203 204 205 206
  void layout(BoxConstraints constraints) {
    _specification = renderObject.specification;
    double contentExtent = _specification.gridSize.height;
    double containerExtent = renderObject.size.height;

207 208
    int materializedRowBase = math.max(0, lowerBound(_specification.rowOffsets, widget.startOffset) - 1);
    int materializedRowLimit = math.min(_specification.rowCount, lowerBound(_specification.rowOffsets, widget.startOffset + containerExtent));
209

210 211
    _materializedChildBase = (materializedRowBase * _specification.columnCount).clamp(0, renderObject.virtualChildCount);
    _materializedChildCount = (materializedRowLimit * _specification.columnCount).clamp(0, renderObject.virtualChildCount) - _materializedChildBase;
212 213
    _startOffsetBase = _specification.rowOffsets[materializedRowBase];
    _startOffsetLimit = _specification.rowOffsets[materializedRowLimit] - containerExtent;
214

Adam Barth's avatar
Adam Barth committed
215
    super.layout(constraints);
216

217 218 219 220
    if (contentExtent != _lastReportedContentExtent || containerExtent != _lastReportedContainerExtent) {
      _lastReportedContentExtent = contentExtent;
      _lastReportedContainerExtent = containerExtent;
      widget.onExtentsChanged(_lastReportedContentExtent, _lastReportedContainerExtent);
221 222 223
    }
  }
}