Merge pull request #1074 from abarth/scrollable_list_features
Complete features of ScrollableList2
This commit is contained in:
commit
c4faad115d
@ -14,11 +14,10 @@ class FitnessItemList extends StatelessComponent {
|
||||
final FitnessItemHandler onDismissed;
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
return new ScrollableList<FitnessItem>(
|
||||
return new ScrollableList2(
|
||||
padding: const EdgeDims.all(4.0),
|
||||
items: items,
|
||||
itemExtent: kFitnessItemHeight,
|
||||
itemBuilder: (BuildContext context, FitnessItem item, int index) => item.toRow(onDismissed: onDismissed)
|
||||
children: items.map((FitnessItem item) => item.toRow(onDismissed: onDismissed))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -14,10 +14,9 @@ class StockList extends StatelessComponent {
|
||||
final StockRowActionCallback onAction;
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
return new ScrollableList<Stock>(
|
||||
items: stocks,
|
||||
return new ScrollableList2(
|
||||
itemExtent: StockRow.kHeight,
|
||||
itemBuilder: (BuildContext context, Stock stock, int index) {
|
||||
children: stocks.map((Stock stock) {
|
||||
return new StockRow(
|
||||
keySalt: keySalt,
|
||||
stock: stock,
|
||||
@ -25,7 +24,7 @@ class StockList extends StatelessComponent {
|
||||
onDoubleTap: onShow,
|
||||
onLongPressed: onAction
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -393,12 +393,11 @@ class CardCollectionState extends State<CardCollection> {
|
||||
Widget build(BuildContext context) {
|
||||
Widget cardCollection;
|
||||
if (_fixedSizeCards) {
|
||||
cardCollection = new ScrollableList<CardModel> (
|
||||
cardCollection = new ScrollableList2 (
|
||||
snapOffsetCallback: _snapToCenter ? _toSnapOffset : null,
|
||||
snapAlignmentOffset: _cardCollectionSize.height / 2.0,
|
||||
items: _cardModels,
|
||||
itemBuilder: (BuildContext context, CardModel card, int index) => _buildCard(context, card.value),
|
||||
itemExtent: _cardModels[0].height
|
||||
itemExtent: _cardModels[0].height,
|
||||
children: _cardModels.map((CardModel card) => _buildCard(context, card.value))
|
||||
);
|
||||
} else {
|
||||
cardCollection = new ScrollableMixedWidgetList(
|
||||
|
@ -74,12 +74,12 @@ class MediaQueryExample extends StatelessComponent {
|
||||
if (MediaQuery.of(context).size.width < _gridViewBreakpoint) {
|
||||
return new ScrollableList2(
|
||||
itemExtent: 50.0,
|
||||
children: items.map((AdaptiveItem item) => item.toListItem()).toList()
|
||||
children: items.map((AdaptiveItem item) => item.toListItem())
|
||||
);
|
||||
} else {
|
||||
return new ScrollableGrid(
|
||||
delegate: new MaxTileWidthGridDelegate(maxTileWidth: _maxTileWidth),
|
||||
children: items.map((AdaptiveItem item) => item.toCard()).toList()
|
||||
children: items.map((AdaptiveItem item) => item.toCard())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,21 @@
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
final NumberFormat _dd = new NumberFormat("00", "en_US");
|
||||
|
||||
class _Item extends StatelessComponent {
|
||||
_Item(this.index);
|
||||
|
||||
int index;
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
return new Text('Item ${_dd.format(index)}',
|
||||
key: new ValueKey<int>(index),
|
||||
style: Theme.of(context).text.title
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ScrollbarApp extends StatefulComponent {
|
||||
ScrollbarAppState createState() => new ScrollbarAppState();
|
||||
}
|
||||
@ -15,17 +30,10 @@ class ScrollbarAppState extends State<ScrollbarApp> {
|
||||
final ScrollbarPainter _scrollbarPainter = new ScrollbarPainter();
|
||||
|
||||
Widget _buildMenu(BuildContext context) {
|
||||
NumberFormat dd = new NumberFormat("00", "en_US");
|
||||
return new ScrollableList<int>(
|
||||
items: new List<int>.generate(_itemCount, (int i) => i),
|
||||
return new ScrollableList2(
|
||||
itemExtent: _itemExtent,
|
||||
itemBuilder: (_, __, int index) {
|
||||
return new Text('Item ${dd.format(index)}',
|
||||
key: new ValueKey<int>(index),
|
||||
style: Theme.of(context).text.title
|
||||
);
|
||||
},
|
||||
scrollableListPainter: _scrollbarPainter
|
||||
scrollableListPainter: _scrollbarPainter,
|
||||
children: new List<Widget>.generate(_itemCount, (int i) => new _Item(i))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -21,44 +21,35 @@ Map<MaterialListType, double> _kItemExtent = const <MaterialListType, double>{
|
||||
MaterialListType.threeLine: kThreeLineListItemHeight,
|
||||
};
|
||||
|
||||
class MaterialList<T> extends StatefulComponent {
|
||||
class MaterialList extends StatefulComponent {
|
||||
MaterialList({
|
||||
Key key,
|
||||
this.initialScrollOffset,
|
||||
this.onScroll,
|
||||
this.items,
|
||||
this.itemBuilder,
|
||||
this.type: MaterialListType.twoLine
|
||||
this.type: MaterialListType.twoLine,
|
||||
this.children
|
||||
}) : super(key: key);
|
||||
|
||||
final double initialScrollOffset;
|
||||
final ScrollListener onScroll;
|
||||
final List<T> items;
|
||||
final ItemBuilder<T> itemBuilder;
|
||||
final MaterialListType type;
|
||||
final Iterable<Widget> children;
|
||||
|
||||
_MaterialListState<T> createState() => new _MaterialListState<T>();
|
||||
_MaterialListState createState() => new _MaterialListState();
|
||||
}
|
||||
|
||||
class _MaterialListState<T> extends State<MaterialList<T>> {
|
||||
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollbarPainter = new ScrollbarPainter();
|
||||
}
|
||||
|
||||
ScrollbarPainter _scrollbarPainter;
|
||||
class _MaterialListState extends State<MaterialList> {
|
||||
ScrollbarPainter _scrollbarPainter = new ScrollbarPainter();
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
return new ScrollableList<T>(
|
||||
return new ScrollableList2(
|
||||
initialScrollOffset: config.initialScrollOffset,
|
||||
scrollDirection: ScrollDirection.vertical,
|
||||
onScroll: config.onScroll,
|
||||
items: config.items,
|
||||
itemBuilder: config.itemBuilder,
|
||||
itemExtent: _kItemExtent[config.type],
|
||||
padding: const EdgeDims.symmetric(vertical: 8.0),
|
||||
scrollableListPainter: _scrollbarPainter
|
||||
scrollableListPainter: _scrollbarPainter,
|
||||
children: config.children
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -30,22 +30,25 @@ class ScrollbarPainter extends ScrollableListPainter {
|
||||
Point thumbOrigin;
|
||||
Size thumbSize;
|
||||
|
||||
if (isVertical) {
|
||||
double thumbHeight = viewportBounds.height * viewportBounds.height / contentExtent;
|
||||
thumbHeight = thumbHeight.clamp(_kMinScrollbarThumbLength, viewportBounds.height);
|
||||
final double maxThumbTop = viewportBounds.height - thumbHeight;
|
||||
double thumbTop = (scrollOffset / (contentExtent - viewportBounds.height)) * maxThumbTop;
|
||||
thumbTop = viewportBounds.top + thumbTop.clamp(0.0, maxThumbTop);
|
||||
thumbOrigin = new Point(viewportBounds.right - _kScrollbarThumbGirth, thumbTop);
|
||||
thumbSize = new Size(_kScrollbarThumbGirth, thumbHeight);
|
||||
} else {
|
||||
double thumbWidth = viewportBounds.width * viewportBounds.width / contentExtent;
|
||||
thumbWidth = thumbWidth.clamp(_kMinScrollbarThumbLength, viewportBounds.width);
|
||||
final double maxThumbLeft = viewportBounds.width - thumbWidth;
|
||||
double thumbLeft = (scrollOffset / (contentExtent - viewportBounds.width)) * maxThumbLeft;
|
||||
thumbLeft = viewportBounds.left + thumbLeft.clamp(0.0, maxThumbLeft);
|
||||
thumbOrigin = new Point(thumbLeft, viewportBounds.height - _kScrollbarThumbGirth);
|
||||
thumbSize = new Size(thumbWidth, _kScrollbarThumbGirth);
|
||||
switch (scrollDirection) {
|
||||
case ScrollDirection.vertical:
|
||||
double thumbHeight = viewportBounds.height * viewportBounds.height / contentExtent;
|
||||
thumbHeight = thumbHeight.clamp(_kMinScrollbarThumbLength, viewportBounds.height);
|
||||
final double maxThumbTop = viewportBounds.height - thumbHeight;
|
||||
double thumbTop = (scrollOffset / (contentExtent - viewportBounds.height)) * maxThumbTop;
|
||||
thumbTop = viewportBounds.top + thumbTop.clamp(0.0, maxThumbTop);
|
||||
thumbOrigin = new Point(viewportBounds.right - _kScrollbarThumbGirth, thumbTop);
|
||||
thumbSize = new Size(_kScrollbarThumbGirth, thumbHeight);
|
||||
break;
|
||||
case ScrollDirection.horizontal:
|
||||
double thumbWidth = viewportBounds.width * viewportBounds.width / contentExtent;
|
||||
thumbWidth = thumbWidth.clamp(_kMinScrollbarThumbLength, viewportBounds.width);
|
||||
final double maxThumbLeft = viewportBounds.width - thumbWidth;
|
||||
double thumbLeft = (scrollOffset / (contentExtent - viewportBounds.width)) * maxThumbLeft;
|
||||
thumbLeft = viewportBounds.left + thumbLeft.clamp(0.0, maxThumbLeft);
|
||||
thumbOrigin = new Point(thumbLeft, viewportBounds.height - _kScrollbarThumbGirth);
|
||||
thumbSize = new Size(thumbWidth, _kScrollbarThumbGirth);
|
||||
break;
|
||||
}
|
||||
|
||||
paintThumb(context, thumbOrigin & thumbSize);
|
||||
@ -65,7 +68,7 @@ class ScrollbarPainter extends ScrollableListPainter {
|
||||
..variable = new AnimatedValue<double>(0.0, end: 1.0, curve: Curves.ease)
|
||||
..addListener(() {
|
||||
_opacity = _fade.value;
|
||||
renderer?.markNeedsPaint();
|
||||
renderObject?.markNeedsPaint();
|
||||
});
|
||||
return _fade.forward();
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import 'package:vector_math/vector_math_64.dart';
|
||||
|
||||
import 'box.dart';
|
||||
import 'object.dart';
|
||||
import 'viewport.dart';
|
||||
|
||||
/// Parent data for use with [RenderBlockBase].
|
||||
class BlockParentData extends ContainerBoxParentDataMixin<RenderBox> { }
|
||||
@ -32,8 +33,10 @@ typedef double _Constrainer(double value);
|
||||
/// children. Because blocks expand in the main axis, blocks must be given
|
||||
/// unlimited space in the main axis, typically by being contained in a
|
||||
/// viewport with a scrolling direction that matches the block's main axis.
|
||||
abstract class RenderBlockBase extends RenderBox with ContainerRenderObjectMixin<RenderBox, BlockParentData>,
|
||||
RenderBoxContainerDefaultsMixin<RenderBox, BlockParentData> {
|
||||
abstract class RenderBlockBase extends RenderBox
|
||||
with ContainerRenderObjectMixin<RenderBox, BlockParentData>,
|
||||
RenderBoxContainerDefaultsMixin<RenderBox, BlockParentData>
|
||||
implements HasScrollDirection {
|
||||
|
||||
RenderBlockBase({
|
||||
List<RenderBox> children,
|
||||
@ -82,6 +85,9 @@ abstract class RenderBlockBase extends RenderBox with ContainerRenderObjectMixin
|
||||
/// Whether the main axis is vertical.
|
||||
bool get isVertical => _direction == BlockDirection.vertical;
|
||||
|
||||
// TODO(abarth): Remove BlockDirection in favor of ScrollDirection.
|
||||
ScrollDirection get scrollDirection => isVertical ? ScrollDirection.vertical : ScrollDirection.horizontal;
|
||||
|
||||
BoxConstraints _getInnerConstraints(BoxConstraints constraints) {
|
||||
if (isVertical)
|
||||
return new BoxConstraints.tightFor(width: constraints.constrainWidth(constraints.maxWidth),
|
||||
|
@ -2,6 +2,8 @@
|
||||
// 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;
|
||||
|
||||
import 'box.dart';
|
||||
import 'object.dart';
|
||||
import 'viewport.dart';
|
||||
@ -9,18 +11,23 @@ import 'viewport.dart';
|
||||
/// Parent data for use with [RenderList].
|
||||
class ListParentData extends ContainerBoxParentDataMixin<RenderBox> { }
|
||||
|
||||
class RenderList extends RenderVirtualViewport<ListParentData> {
|
||||
class RenderList extends RenderVirtualViewport<ListParentData> implements HasScrollDirection {
|
||||
RenderList({
|
||||
List<RenderBox> children,
|
||||
double itemExtent,
|
||||
EdgeDims padding,
|
||||
int virtualChildCount,
|
||||
Offset paintOffset: Offset.zero,
|
||||
ScrollDirection scrollDirection: ScrollDirection.vertical,
|
||||
LayoutCallback callback
|
||||
}) : _itemExtent = itemExtent, super(
|
||||
virtualChildCount: virtualChildCount,
|
||||
paintOffset: paintOffset,
|
||||
callback: callback
|
||||
) {
|
||||
}) : _itemExtent = itemExtent,
|
||||
_padding = padding,
|
||||
_scrollDirection = scrollDirection,
|
||||
super(
|
||||
virtualChildCount: virtualChildCount,
|
||||
paintOffset: paintOffset,
|
||||
callback: callback
|
||||
) {
|
||||
assert(itemExtent != null);
|
||||
addAll(children);
|
||||
}
|
||||
@ -35,50 +42,124 @@ class RenderList extends RenderVirtualViewport<ListParentData> {
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
EdgeDims get padding => _padding;
|
||||
EdgeDims _padding;
|
||||
void set padding (EdgeDims newValue) {
|
||||
if (_padding == newValue)
|
||||
return;
|
||||
_padding = newValue;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
ScrollDirection get scrollDirection => _scrollDirection;
|
||||
ScrollDirection _scrollDirection;
|
||||
void set scrollDirection (ScrollDirection newValue) {
|
||||
if (_scrollDirection == newValue)
|
||||
return;
|
||||
_scrollDirection = newValue;
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
void setupParentData(RenderBox child) {
|
||||
if (child.parentData is! ListParentData)
|
||||
child.parentData = new ListParentData();
|
||||
}
|
||||
|
||||
double get _preferredMainAxisExtent => itemExtent * virtualChildCount;
|
||||
double get _scrollAxisPadding {
|
||||
switch (scrollDirection) {
|
||||
case ScrollDirection.vertical:
|
||||
return padding.vertical;
|
||||
case ScrollDirection.horizontal:
|
||||
return padding.horizontal;
|
||||
}
|
||||
}
|
||||
|
||||
double get _preferredExtent {
|
||||
double extent = itemExtent * virtualChildCount;
|
||||
if (padding != null)
|
||||
extent += _scrollAxisPadding;
|
||||
return extent;
|
||||
}
|
||||
|
||||
double _getIntrinsicWidth(BoxConstraints constraints) {
|
||||
assert(constraints.isNormalized);
|
||||
switch (scrollDirection) {
|
||||
case ScrollDirection.vertical:
|
||||
return constraints.constrainWidth(0.0);
|
||||
case ScrollDirection.horizontal:
|
||||
return constraints.constrainWidth(_preferredExtent);
|
||||
}
|
||||
}
|
||||
|
||||
double getMinIntrinsicWidth(BoxConstraints constraints) {
|
||||
assert(constraints.isNormalized);
|
||||
return constraints.constrainWidth(0.0);
|
||||
return _getIntrinsicWidth(constraints);
|
||||
}
|
||||
|
||||
double getMaxIntrinsicWidth(BoxConstraints constraints) {
|
||||
return _getIntrinsicWidth(constraints);
|
||||
}
|
||||
|
||||
double _getIntrinsicHeight(BoxConstraints constraints) {
|
||||
assert(constraints.isNormalized);
|
||||
return constraints.constrainWidth(0.0);
|
||||
switch (scrollDirection) {
|
||||
case ScrollDirection.vertical:
|
||||
return constraints.constrainHeight(_preferredExtent);
|
||||
case ScrollDirection.horizontal:
|
||||
return constraints.constrainHeight(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
double getMinIntrinsicHeight(BoxConstraints constraints) {
|
||||
assert(constraints.isNormalized);
|
||||
return constraints.constrainHeight(_preferredMainAxisExtent);
|
||||
return _getIntrinsicHeight(constraints);
|
||||
}
|
||||
|
||||
double getMaxIntrinsicHeight(BoxConstraints constraints) {
|
||||
assert(constraints.isNormalized);
|
||||
return constraints.constrainHeight(_preferredMainAxisExtent);
|
||||
return _getIntrinsicHeight(constraints);
|
||||
}
|
||||
|
||||
void performLayout() {
|
||||
double height = _preferredMainAxisExtent;
|
||||
size = new Size(constraints.maxWidth, constraints.constrainHeight(height));
|
||||
size = new Size(constraints.maxWidth,
|
||||
constraints.constrainHeight(_preferredExtent));
|
||||
|
||||
if (callback != null)
|
||||
invokeLayoutCallback(callback);
|
||||
|
||||
BoxConstraints innerConstraints =
|
||||
new BoxConstraints.tightFor(width: size.width, height: itemExtent);
|
||||
double itemWidth;
|
||||
double itemHeight;
|
||||
|
||||
double x = 0.0;
|
||||
double dx = 0.0;
|
||||
|
||||
double y = 0.0;
|
||||
double dy = 0.0;
|
||||
|
||||
switch (scrollDirection) {
|
||||
case ScrollDirection.vertical:
|
||||
itemWidth = math.max(0, size.width - (padding == null ? 0.0 : padding.horizontal));
|
||||
itemHeight = itemExtent;
|
||||
y = padding != null ? padding.top : 0.0;
|
||||
dy = itemExtent;
|
||||
break;
|
||||
case ScrollDirection.horizontal:
|
||||
itemWidth = itemExtent;
|
||||
itemHeight = math.max(0, size.height - (padding == null ? 0.0 : padding.vertical));
|
||||
x = padding != null ? padding.left : 0.0;
|
||||
dx = itemExtent;
|
||||
break;
|
||||
}
|
||||
|
||||
BoxConstraints innerConstraints =
|
||||
new BoxConstraints.tightFor(width: itemWidth, height: itemHeight);
|
||||
|
||||
int childIndex = 0;
|
||||
RenderBox child = firstChild;
|
||||
while (child != null) {
|
||||
child.layout(innerConstraints);
|
||||
|
||||
final ListParentData childParentData = child.parentData;
|
||||
childParentData.offset = new Offset(0.0, childIndex * itemExtent);
|
||||
childIndex += 1;
|
||||
childParentData.offset = new Offset(x, y);
|
||||
x += dx;
|
||||
y += dy;
|
||||
|
||||
assert(child.parentData == childParentData);
|
||||
child = childParentData.nextSibling;
|
||||
}
|
||||
|
@ -18,6 +18,10 @@ enum ScrollDirection {
|
||||
vertical,
|
||||
}
|
||||
|
||||
abstract class HasScrollDirection {
|
||||
ScrollDirection get scrollDirection;
|
||||
}
|
||||
|
||||
/// A render object that's bigger on the inside.
|
||||
///
|
||||
/// The child of a viewport can layout to a larger size than the viewport
|
||||
@ -27,7 +31,8 @@ enum ScrollDirection {
|
||||
///
|
||||
/// Viewport is the core scrolling primitive in the system, but it can be used
|
||||
/// in other situations.
|
||||
class RenderViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox> {
|
||||
class RenderViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox>
|
||||
implements HasScrollDirection {
|
||||
|
||||
RenderViewport({
|
||||
RenderBox child,
|
||||
@ -177,10 +182,12 @@ abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<Rende
|
||||
RenderVirtualViewport({
|
||||
int virtualChildCount,
|
||||
Offset paintOffset,
|
||||
LayoutCallback callback
|
||||
LayoutCallback callback,
|
||||
Painter overlayPainter
|
||||
}) : _virtualChildCount = virtualChildCount,
|
||||
_paintOffset = paintOffset,
|
||||
_callback = callback;
|
||||
_callback = callback,
|
||||
_overlayPainter = overlayPainter;
|
||||
|
||||
int get virtualChildCount => _virtualChildCount ?? childCount;
|
||||
int _virtualChildCount;
|
||||
@ -217,8 +224,31 @@ abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<Rende
|
||||
markNeedsLayout();
|
||||
}
|
||||
|
||||
Painter get overlayPainter => _overlayPainter;
|
||||
Painter _overlayPainter;
|
||||
void set overlayPainter(Painter value) {
|
||||
if (_overlayPainter == value)
|
||||
return;
|
||||
if (attached)
|
||||
_overlayPainter?.detach();
|
||||
_overlayPainter = value;
|
||||
if (attached)
|
||||
_overlayPainter?.attach(this);
|
||||
markNeedsPaint();
|
||||
}
|
||||
|
||||
void attach() {
|
||||
super.attach();
|
||||
_overlayPainter?.attach(this);
|
||||
}
|
||||
|
||||
void detach() {
|
||||
super.detach();
|
||||
_overlayPainter?.detach();
|
||||
}
|
||||
|
||||
void applyPaintTransform(RenderBox child, Matrix4 transform) {
|
||||
super.applyPaintTransform(child, transform.translate(paintOffset));
|
||||
super.applyPaintTransform(child, transform.translate(paintOffset.dx, paintOffset.dy));
|
||||
}
|
||||
|
||||
bool hitTestChildren(HitTestResult result, { Point position }) {
|
||||
@ -227,6 +257,7 @@ abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<Rende
|
||||
|
||||
void _paintContents(PaintingContext context, Offset offset) {
|
||||
defaultPaint(context, offset + paintOffset);
|
||||
_overlayPainter?.paint(context, offset);
|
||||
}
|
||||
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
|
@ -445,15 +445,19 @@ class Block extends StatelessComponent {
|
||||
|
||||
abstract class ScrollableListPainter extends Painter {
|
||||
void attach(RenderObject renderObject) {
|
||||
assert(renderObject is RenderBlockViewport);
|
||||
assert(renderObject is RenderBox);
|
||||
assert(renderObject is HasScrollDirection);
|
||||
super.attach(renderObject);
|
||||
}
|
||||
|
||||
RenderBlockViewport get renderer => renderObject;
|
||||
RenderBox get renderObject => super.renderObject;
|
||||
|
||||
bool get isVertical => renderer.isVertical;
|
||||
ScrollDirection get scrollDirection {
|
||||
HasScrollDirection scrollable = renderObject as dynamic;
|
||||
return scrollable?.scrollDirection;
|
||||
}
|
||||
|
||||
Size get viewportSize => renderer.size;
|
||||
Size get viewportSize => renderObject.size;
|
||||
|
||||
double get contentExtent => _contentExtent;
|
||||
double _contentExtent = 0.0;
|
||||
@ -463,7 +467,7 @@ abstract class ScrollableListPainter extends Painter {
|
||||
if (_contentExtent == value)
|
||||
return;
|
||||
_contentExtent = value;
|
||||
renderer?.markNeedsPaint();
|
||||
renderObject?.markNeedsPaint();
|
||||
}
|
||||
|
||||
double get scrollOffset => _scrollOffset;
|
||||
@ -473,7 +477,7 @@ abstract class ScrollableListPainter extends Painter {
|
||||
if (_scrollOffset == value)
|
||||
return;
|
||||
_scrollOffset = value;
|
||||
renderer?.markNeedsPaint();
|
||||
renderObject?.markNeedsPaint();
|
||||
}
|
||||
|
||||
/// Called when a scroll starts. Subclasses may override this method to
|
||||
@ -675,7 +679,7 @@ class ScrollableList<T> extends ScrollableWidgetList {
|
||||
double snapAlignmentOffset: 0.0,
|
||||
this.items,
|
||||
this.itemBuilder,
|
||||
itemsWrap: false,
|
||||
bool itemsWrap: false,
|
||||
double itemExtent,
|
||||
EdgeDims padding,
|
||||
ScrollableListPainter scrollableListPainter
|
||||
|
@ -36,7 +36,7 @@ class ScrollableGrid extends Scrollable {
|
||||
);
|
||||
|
||||
final GridDelegate delegate;
|
||||
final List<Widget> children;
|
||||
final Iterable<Widget> children;
|
||||
|
||||
ScrollableState createState() => new _ScrollableGridState();
|
||||
}
|
||||
@ -77,7 +77,10 @@ class GridViewport extends VirtualViewport {
|
||||
final double startOffset;
|
||||
final GridDelegate delegate;
|
||||
final ExtentsChangedCallback onExtentsChanged;
|
||||
final List<Widget> children;
|
||||
final Iterable<Widget> children;
|
||||
|
||||
// TODO(abarth): Support horizontal scrolling;
|
||||
ScrollDirection get scrollDirection => ScrollDirection.vertical;
|
||||
|
||||
RenderGrid createRenderObject() => new RenderGrid(delegate: delegate);
|
||||
|
||||
@ -136,8 +139,8 @@ class _GridViewportElement extends VirtualViewportElement<GridViewport> {
|
||||
int materializedRowBase = math.max(0, _lowerBound(_specification.rowOffsets, widget.startOffset) - 1);
|
||||
int materializedRowLimit = math.min(_specification.rowCount, _lowerBound(_specification.rowOffsets, widget.startOffset + containerExtent));
|
||||
|
||||
_materializedChildBase = (materializedRowBase * _specification.columnCount).clamp(0, widget.children.length);
|
||||
_materializedChildCount = (materializedRowLimit * _specification.columnCount).clamp(0, widget.children.length) - _materializedChildBase;
|
||||
_materializedChildBase = (materializedRowBase * _specification.columnCount).clamp(0, renderObject.virtualChildCount);
|
||||
_materializedChildCount = (materializedRowLimit * _specification.columnCount).clamp(0, renderObject.virtualChildCount) - _materializedChildBase;
|
||||
_repaintOffsetBase = _specification.rowOffsets[materializedRowBase];
|
||||
_repaintOffsetLimit = _specification.rowOffsets[materializedRowLimit];
|
||||
|
||||
|
@ -2,6 +2,8 @@
|
||||
// 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;
|
||||
|
||||
import 'framework.dart';
|
||||
import 'scrollable.dart';
|
||||
import 'virtual_viewport.dart';
|
||||
@ -13,23 +15,31 @@ class ScrollableList2 extends Scrollable {
|
||||
ScrollableList2({
|
||||
Key key,
|
||||
double initialScrollOffset,
|
||||
ScrollDirection scrollDirection: ScrollDirection.vertical,
|
||||
ScrollListener onScroll,
|
||||
SnapOffsetCallback snapOffsetCallback,
|
||||
double snapAlignmentOffset: 0.0,
|
||||
this.itemExtent,
|
||||
this.itemsWrap: false,
|
||||
this.padding,
|
||||
this.scrollableListPainter,
|
||||
this.children
|
||||
}) : super(
|
||||
key: key,
|
||||
initialScrollOffset: initialScrollOffset,
|
||||
// TODO(abarth): Support horizontal offsets.
|
||||
scrollDirection: ScrollDirection.vertical,
|
||||
scrollDirection: scrollDirection,
|
||||
onScroll: onScroll,
|
||||
snapOffsetCallback: snapOffsetCallback,
|
||||
snapAlignmentOffset: snapAlignmentOffset
|
||||
);
|
||||
) {
|
||||
assert(itemExtent != null);
|
||||
}
|
||||
|
||||
final double itemExtent;
|
||||
final List<Widget> children;
|
||||
final bool itemsWrap;
|
||||
final EdgeDims padding;
|
||||
final ScrollableListPainter scrollableListPainter;
|
||||
final Iterable<Widget> children;
|
||||
|
||||
ScrollableState createState() => new _ScrollableList2State();
|
||||
}
|
||||
@ -39,20 +49,40 @@ class _ScrollableList2State extends ScrollableState<ScrollableList2> {
|
||||
ExtentScrollBehavior get scrollBehavior => super.scrollBehavior;
|
||||
|
||||
void _handleExtentsChanged(double contentExtent, double containerExtent) {
|
||||
config.scrollableListPainter?.contentExtent = contentExtent;
|
||||
setState(() {
|
||||
scrollTo(scrollBehavior.updateExtents(
|
||||
contentExtent: contentExtent,
|
||||
contentExtent: config.itemsWrap ? double.INFINITY : contentExtent,
|
||||
containerExtent: containerExtent,
|
||||
scrollOffset: scrollOffset
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
void dispatchOnScrollStart() {
|
||||
super.dispatchOnScrollStart();
|
||||
config.scrollableListPainter?.scrollStarted();
|
||||
}
|
||||
|
||||
void dispatchOnScroll() {
|
||||
super.dispatchOnScroll();
|
||||
config.scrollableListPainter?.scrollOffset = scrollOffset;
|
||||
}
|
||||
|
||||
void dispatchOnScrollEnd() {
|
||||
super.dispatchOnScrollEnd();
|
||||
config.scrollableListPainter?.scrollEnded();
|
||||
}
|
||||
|
||||
Widget buildContent(BuildContext context) {
|
||||
return new ListViewport(
|
||||
startOffset: scrollOffset,
|
||||
itemExtent: config.itemExtent,
|
||||
onExtentsChanged: _handleExtentsChanged,
|
||||
startOffset: scrollOffset,
|
||||
scrollDirection: config.scrollDirection,
|
||||
itemExtent: config.itemExtent,
|
||||
itemsWrap: config.itemsWrap,
|
||||
padding: config.padding,
|
||||
overlayPainter: config.scrollableListPainter,
|
||||
children: config.children
|
||||
);
|
||||
}
|
||||
@ -61,16 +91,27 @@ class _ScrollableList2State extends ScrollableState<ScrollableList2> {
|
||||
class ListViewport extends VirtualViewport {
|
||||
ListViewport({
|
||||
Key key,
|
||||
this.startOffset,
|
||||
this.itemExtent,
|
||||
this.onExtentsChanged,
|
||||
this.startOffset: 0.0,
|
||||
this.scrollDirection: ScrollDirection.vertical,
|
||||
this.itemExtent,
|
||||
this.itemsWrap: false,
|
||||
this.padding,
|
||||
this.overlayPainter,
|
||||
this.children
|
||||
});
|
||||
}) {
|
||||
assert(scrollDirection != null);
|
||||
assert(itemExtent != null);
|
||||
}
|
||||
|
||||
final double startOffset;
|
||||
final double itemExtent;
|
||||
final ExtentsChangedCallback onExtentsChanged;
|
||||
final List<Widget> children;
|
||||
final double startOffset;
|
||||
final ScrollDirection scrollDirection;
|
||||
final double itemExtent;
|
||||
final bool itemsWrap;
|
||||
final EdgeDims padding;
|
||||
final Painter overlayPainter;
|
||||
final Iterable<Widget> children;
|
||||
|
||||
RenderList createRenderObject() => new RenderList(itemExtent: itemExtent);
|
||||
|
||||
@ -95,21 +136,39 @@ class _ListViewportElement extends VirtualViewportElement<ListViewport> {
|
||||
double _repaintOffsetLimit;
|
||||
|
||||
void updateRenderObject() {
|
||||
renderObject.scrollDirection = widget.scrollDirection;
|
||||
renderObject.itemExtent = widget.itemExtent;
|
||||
renderObject.padding = widget.padding;
|
||||
renderObject.overlayPainter = widget.overlayPainter;
|
||||
super.updateRenderObject();
|
||||
}
|
||||
|
||||
double _contentExtent;
|
||||
double _containerExtent;
|
||||
|
||||
double _getContainerExtentFromRenderObject() {
|
||||
switch (widget.scrollDirection) {
|
||||
case ScrollDirection.vertical:
|
||||
return renderObject.size.height;
|
||||
case ScrollDirection.horizontal:
|
||||
return renderObject.size.width;
|
||||
}
|
||||
}
|
||||
|
||||
void layout(BoxConstraints constraints) {
|
||||
double contentExtent = widget.itemExtent * widget.children.length;
|
||||
double containerExtent = renderObject.size.height;
|
||||
int length = renderObject.virtualChildCount;
|
||||
double contentExtent = widget.itemExtent * length;
|
||||
double containerExtent = _getContainerExtentFromRenderObject();
|
||||
|
||||
_materializedChildBase = math.max(0, widget.startOffset ~/ widget.itemExtent);
|
||||
int materializedChildLimit = math.max(0, ((widget.startOffset + containerExtent) / widget.itemExtent).ceil());
|
||||
|
||||
if (!widget.itemsWrap) {
|
||||
_materializedChildBase = math.min(length, _materializedChildBase);
|
||||
materializedChildLimit = math.min(length, materializedChildLimit);
|
||||
}
|
||||
|
||||
_materializedChildBase = (widget.startOffset ~/ widget.itemExtent).clamp(0, widget.children.length);
|
||||
int materializedChildLimit = ((widget.startOffset + containerExtent) / widget.itemExtent).ceil().clamp(0, widget.children.length);
|
||||
_materializedChildCount = materializedChildLimit - _materializedChildBase;
|
||||
|
||||
_repaintOffsetBase = _materializedChildBase * widget.itemExtent;
|
||||
_repaintOffsetLimit = materializedChildLimit * widget.itemExtent;
|
||||
|
||||
|
@ -11,7 +11,8 @@ typedef void ExtentsChangedCallback(double contentExtent, double containerExtent
|
||||
|
||||
abstract class VirtualViewport extends RenderObjectWidget {
|
||||
double get startOffset;
|
||||
List<Widget> get children;
|
||||
ScrollDirection get scrollDirection;
|
||||
Iterable<Widget> get children;
|
||||
}
|
||||
|
||||
abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderObjectElement<T> {
|
||||
@ -35,6 +36,8 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO
|
||||
|
||||
void mount(Element parent, dynamic newSlot) {
|
||||
super.mount(parent, newSlot);
|
||||
_iterator = null;
|
||||
_widgets = <Widget>[];
|
||||
renderObject.callback = layout;
|
||||
updateRenderObject();
|
||||
}
|
||||
@ -45,6 +48,10 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO
|
||||
}
|
||||
|
||||
void update(T newWidget) {
|
||||
if (widget.children != newWidget.children) {
|
||||
_iterator = null;
|
||||
_widgets = <Widget>[];
|
||||
}
|
||||
super.update(newWidget);
|
||||
updateRenderObject();
|
||||
if (!renderObject.needsLayout)
|
||||
@ -52,8 +59,23 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO
|
||||
}
|
||||
|
||||
void _updatePaintOffset() {
|
||||
renderObject.paintOffset =
|
||||
renderObject.paintOffset = new Offset(0.0, -(widget.startOffset - repaintOffsetBase));
|
||||
switch (widget.scrollDirection) {
|
||||
case ScrollDirection.vertical:
|
||||
renderObject.paintOffset = new Offset(0.0, -(widget.startOffset - repaintOffsetBase));
|
||||
break;
|
||||
case ScrollDirection.horizontal:
|
||||
renderObject.paintOffset = new Offset(-(widget.startOffset - repaintOffsetBase), 0.0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
double get _containerExtent {
|
||||
switch (widget.scrollDirection) {
|
||||
case ScrollDirection.vertical:
|
||||
return renderObject.size.height;
|
||||
case ScrollDirection.horizontal:
|
||||
return renderObject.size.width;
|
||||
}
|
||||
}
|
||||
|
||||
void updateRenderObject() {
|
||||
@ -67,7 +89,7 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO
|
||||
if (!renderObject.needsLayout) {
|
||||
if (repaintOffsetBase != null && widget.startOffset < repaintOffsetBase)
|
||||
renderObject.markNeedsLayout();
|
||||
else if (repaintOffsetLimit != null && widget.startOffset + renderObject.size.height > repaintOffsetLimit)
|
||||
else if (repaintOffsetLimit != null && widget.startOffset + _containerExtent > repaintOffsetLimit)
|
||||
renderObject.markNeedsLayout();
|
||||
}
|
||||
}
|
||||
@ -80,15 +102,37 @@ abstract class VirtualViewportElement<T extends VirtualViewport> extends RenderO
|
||||
BuildableElement.lockState(_materializeChildren);
|
||||
}
|
||||
|
||||
Iterator<Widget> _iterator;
|
||||
List<Widget> _widgets;
|
||||
|
||||
void _populateWidgets(int limit) {
|
||||
if (limit <= _widgets.length)
|
||||
return;
|
||||
if (widget.children is List<Widget>) {
|
||||
_widgets = widget.children;
|
||||
return;
|
||||
}
|
||||
_iterator ??= widget.children.iterator;
|
||||
while (_widgets.length < limit) {
|
||||
bool moved = _iterator.moveNext();
|
||||
assert(moved);
|
||||
Widget current = _iterator.current;
|
||||
assert(current != null);
|
||||
_widgets.add(current);
|
||||
}
|
||||
}
|
||||
|
||||
void _materializeChildren() {
|
||||
int base = materializedChildBase;
|
||||
int count = materializedChildCount;
|
||||
int length = renderObject.virtualChildCount;
|
||||
assert(base != null);
|
||||
assert(count != null);
|
||||
_populateWidgets(base + count);
|
||||
List<Widget> newWidgets = new List<Widget>(count);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
int childIndex = base + i;
|
||||
Widget child = widget.children[childIndex];
|
||||
Widget child = _widgets[childIndex % length];
|
||||
Key key = new ValueKey(child.key ?? childIndex);
|
||||
newWidgets[i] = new RepaintBoundary(key: key, child: child);
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ void handleOnDismissed(int item) {
|
||||
dismissedItems.add(item);
|
||||
}
|
||||
|
||||
Widget buildDismissableItem(BuildContext context, int item, int index) {
|
||||
Widget buildDismissableItem(int item) {
|
||||
return new Dismissable(
|
||||
key: new ValueKey<int>(item),
|
||||
direction: dismissDirection,
|
||||
@ -38,11 +38,12 @@ Widget buildDismissableItem(BuildContext context, int item, int index) {
|
||||
Widget widgetBuilder() {
|
||||
return new Container(
|
||||
padding: const EdgeDims.all(10.0),
|
||||
child: new ScrollableList<int>(
|
||||
items: <int>[0, 1, 2, 3, 4].where((int i) => !dismissedItems.contains(i)).toList(),
|
||||
itemBuilder: buildDismissableItem,
|
||||
child: new ScrollableList2(
|
||||
scrollDirection: scrollDirection,
|
||||
itemExtent: itemExtent
|
||||
itemExtent: itemExtent,
|
||||
children: <int>[0, 1, 2, 3, 4].where(
|
||||
(int i) => !dismissedItems.contains(i)
|
||||
).map(buildDismissableItem)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -99,16 +99,15 @@ void main() {
|
||||
|
||||
(key.currentState as StateMarkerState).marker = "marked";
|
||||
|
||||
tester.pumpWidget(new ScrollableList<int>(
|
||||
items: <int>[0],
|
||||
tester.pumpWidget(new ScrollableList2(
|
||||
itemExtent: 100.0,
|
||||
itemBuilder: (BuildContext context, int item, int index) {
|
||||
return new Container(
|
||||
children: <Widget>[
|
||||
new Container(
|
||||
key: new Key('container'),
|
||||
height: 100.0,
|
||||
child: new StateMarker(key: key)
|
||||
);
|
||||
}
|
||||
)
|
||||
]
|
||||
));
|
||||
|
||||
expect((key.currentState as StateMarkerState).marker, equals("marked"));
|
||||
|
@ -17,20 +17,18 @@ void main() {
|
||||
tester.pumpWidget(new Center(
|
||||
child: new Container(
|
||||
height: 50.0,
|
||||
child: new ScrollableList<int>(
|
||||
child: new ScrollableList2(
|
||||
key: new GlobalKey(),
|
||||
items: items,
|
||||
itemBuilder: (BuildContext context, int item, int index) {
|
||||
itemExtent: 290.0,
|
||||
scrollDirection: ScrollDirection.horizontal,
|
||||
children: items.map((int item) {
|
||||
return new Container(
|
||||
key: new ValueKey<int>(item),
|
||||
child: new GestureDetector(
|
||||
onTap: () { tapped.add(item); },
|
||||
child: new Text('$item')
|
||||
)
|
||||
);
|
||||
},
|
||||
itemExtent: 290.0,
|
||||
scrollDirection: ScrollDirection.horizontal
|
||||
})
|
||||
)
|
||||
)
|
||||
));
|
||||
@ -59,20 +57,18 @@ void main() {
|
||||
tester.pumpWidget(new Center(
|
||||
child: new Container(
|
||||
width: 50.0,
|
||||
child: new ScrollableList<int>(
|
||||
child: new ScrollableList2(
|
||||
key: new GlobalKey(),
|
||||
items: items,
|
||||
itemBuilder: (BuildContext context, int item, int index) {
|
||||
itemExtent: 290.0,
|
||||
scrollDirection: ScrollDirection.vertical,
|
||||
children: items.map((int item) {
|
||||
return new Container(
|
||||
key: new ValueKey<int>(item),
|
||||
child: new GestureDetector(
|
||||
onTap: () { tapped.add(item); },
|
||||
child: new Text('$item')
|
||||
)
|
||||
);
|
||||
},
|
||||
itemExtent: 290.0,
|
||||
scrollDirection: ScrollDirection.vertical
|
||||
})
|
||||
)
|
||||
)
|
||||
));
|
||||
|
@ -13,16 +13,14 @@ Widget buildFrame() {
|
||||
return new Center(
|
||||
child: new Container(
|
||||
height: 50.0,
|
||||
child: new ScrollableList<int>(
|
||||
items: items,
|
||||
itemBuilder: (BuildContext context, int item, int index) {
|
||||
child: new ScrollableList2(
|
||||
itemExtent: 290.0,
|
||||
scrollDirection: ScrollDirection.horizontal,
|
||||
children: items.map((int item) {
|
||||
return new Container(
|
||||
key: new ValueKey<int>(item),
|
||||
child: new Text('$item')
|
||||
);
|
||||
},
|
||||
itemExtent: 290.0,
|
||||
scrollDirection: ScrollDirection.horizontal
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
|
@ -9,16 +9,14 @@ import 'package:test/test.dart';
|
||||
const List<int> items = const <int>[0, 1, 2, 3, 4, 5];
|
||||
|
||||
Widget buildFrame() {
|
||||
return new ScrollableList<int>(
|
||||
items: items,
|
||||
itemBuilder: (BuildContext context, int item, int index) {
|
||||
return new ScrollableList2(
|
||||
itemExtent: 290.0,
|
||||
scrollDirection: ScrollDirection.vertical,
|
||||
children: items.map((int item) {
|
||||
return new Container(
|
||||
key: new ValueKey<int>(item),
|
||||
child: new Text('$item')
|
||||
);
|
||||
},
|
||||
itemExtent: 290.0,
|
||||
scrollDirection: ScrollDirection.vertical
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -12,9 +12,8 @@ const double itemExtent = 200.0;
|
||||
ScrollDirection scrollDirection = ScrollDirection.vertical;
|
||||
GlobalKey scrollableListKey;
|
||||
|
||||
Widget buildItem(BuildContext context, int item, int index) {
|
||||
Widget buildItem(int item) {
|
||||
return new Container(
|
||||
key: new ValueKey<int>(item),
|
||||
width: itemExtent,
|
||||
height: itemExtent,
|
||||
child: new Text(item.toString())
|
||||
@ -30,13 +29,12 @@ Widget buildFrame() {
|
||||
return new Center(
|
||||
child: new Container(
|
||||
height: itemExtent * 2.0,
|
||||
child: new ScrollableList<int>(
|
||||
child: new ScrollableList2(
|
||||
key: scrollableListKey,
|
||||
snapOffsetCallback: snapOffsetCallback,
|
||||
scrollDirection: scrollDirection,
|
||||
items: <int>[0, 1, 2, 3, 4, 5, 7, 8, 9],
|
||||
itemBuilder: buildItem,
|
||||
itemExtent: itemExtent
|
||||
itemExtent: itemExtent,
|
||||
children: <int>[0, 1, 2, 3, 4, 5, 7, 8, 9].map(buildItem)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user