diff --git a/packages/flutter/lib/src/rendering/viewport.dart b/packages/flutter/lib/src/rendering/viewport.dart index 0ef2c3048ec..3ed740d10f2 100644 --- a/packages/flutter/lib/src/rendering/viewport.dart +++ b/packages/flutter/lib/src/rendering/viewport.dart @@ -14,6 +14,33 @@ enum ViewportAnchor { end, } +class ViewportDimensions { + const ViewportDimensions({ + this.contentSize: Size.zero, + this.containerSize: Size.zero + }); + + static const ViewportDimensions zero = const ViewportDimensions(); + + final Size contentSize; + final Size containerSize; + + bool get _debugHasAtLeastOneCommonDimension { + return contentSize.width == containerSize.width + || contentSize.height == containerSize.height; + } + + Offset getAbsolutePaintOffset({ Offset paintOffset, ViewportAnchor anchor }) { + assert(_debugHasAtLeastOneCommonDimension); + switch (anchor) { + case ViewportAnchor.start: + return paintOffset; + case ViewportAnchor.end: + return paintOffset + (containerSize - contentSize); + } + } +} + abstract class HasScrollDirection { Axis get scrollDirection; } @@ -27,9 +54,11 @@ class RenderViewportBase extends RenderBox implements HasScrollDirection { RenderViewportBase( Offset paintOffset, Axis scrollDirection, + ViewportAnchor scrollAnchor, Painter overlayPainter ) : _paintOffset = paintOffset, _scrollDirection = scrollDirection, + _scrollAnchor = scrollAnchor, _overlayPainter = overlayPainter { assert(paintOffset != null); assert(scrollDirection != null); @@ -76,6 +105,17 @@ class RenderViewportBase extends RenderBox implements HasScrollDirection { markNeedsLayout(); } + ViewportAnchor get scrollAnchor => _scrollAnchor; + ViewportAnchor _scrollAnchor; + void set scrollAnchor(ViewportAnchor value) { + assert(value != null); + if (value == _scrollAnchor) + return; + _scrollAnchor = value; + markNeedsPaint(); + markNeedsSemanticsUpdate(); + } + Painter get overlayPainter => _overlayPainter; Painter _overlayPainter; void set overlayPainter(Painter value) { @@ -99,16 +139,25 @@ class RenderViewportBase extends RenderBox implements HasScrollDirection { _overlayPainter?.detach(); } - Offset get _paintOffsetRoundedToIntegerDevicePixels { + ViewportDimensions get dimensions => _dimensions; + ViewportDimensions _dimensions = ViewportDimensions.zero; + void set dimensions(ViewportDimensions value) { + assert(debugDoingThisLayout); + _dimensions = value; + } + + Offset get _effectivePaintOffset { final double devicePixelRatio = ui.window.devicePixelRatio; int dxInDevicePixels = (_paintOffset.dx * devicePixelRatio).round(); int dyInDevicePixels = (_paintOffset.dy * devicePixelRatio).round(); - return new Offset(dxInDevicePixels / devicePixelRatio, - dyInDevicePixels / devicePixelRatio); + return _dimensions.getAbsolutePaintOffset( + paintOffset: new Offset(dxInDevicePixels / devicePixelRatio, dyInDevicePixels / devicePixelRatio), + anchor: _scrollAnchor + ); } void applyPaintTransform(RenderBox child, Matrix4 transform) { - final Offset effectivePaintOffset = _paintOffsetRoundedToIntegerDevicePixels; + final Offset effectivePaintOffset = _effectivePaintOffset; super.applyPaintTransform(child, transform.translate(effectivePaintOffset.dx, effectivePaintOffset.dy)); } @@ -126,8 +175,9 @@ class RenderViewport extends RenderViewportBase with RenderObjectWithChildMixin< RenderBox child, Offset paintOffset: Offset.zero, Axis scrollDirection: Axis.vertical, + ViewportAnchor scrollAnchor: ViewportAnchor.start, Painter overlayPainter - }) : super(paintOffset, scrollDirection, overlayPainter) { + }) : super(paintOffset, scrollDirection, scrollAnchor, overlayPainter) { this.child = child; } @@ -183,8 +233,10 @@ class RenderViewport extends RenderViewportBase with RenderObjectWithChildMixin< size = constraints.constrain(child.size); final BoxParentData childParentData = child.parentData; childParentData.offset = Offset.zero; + dimensions = new ViewportDimensions(containerSize: size, contentSize: child.size); } else { performResize(); + dimensions = new ViewportDimensions(containerSize: size); } } @@ -195,7 +247,7 @@ class RenderViewport extends RenderViewportBase with RenderObjectWithChildMixin< void paint(PaintingContext context, Offset offset) { if (child != null) { - final Offset effectivePaintOffset = _paintOffsetRoundedToIntegerDevicePixels; + final Offset effectivePaintOffset = _effectivePaintOffset; void paintContents(PaintingContext context, Offset offset) { context.paintChild(child, offset + effectivePaintOffset); @@ -211,7 +263,7 @@ class RenderViewport extends RenderViewportBase with RenderObjectWithChildMixin< } Rect describeApproximatePaintClip(RenderObject child) { - if (child != null && _shouldClipAtPaintOffset(_paintOffsetRoundedToIntegerDevicePixels)) + if (child != null && _shouldClipAtPaintOffset(_effectivePaintOffset)) return Point.origin & size; return null; } @@ -219,7 +271,7 @@ class RenderViewport extends RenderViewportBase with RenderObjectWithChildMixin< bool hitTestChildren(HitTestResult result, { Point position }) { if (child != null) { assert(child.parentData is BoxParentData); - Point transformed = position + -_paintOffsetRoundedToIntegerDevicePixels; + Point transformed = position + -_effectivePaintOffset; return child.hitTest(result, position: transformed); } return false; @@ -234,10 +286,11 @@ abstract class RenderVirtualViewport _virtualChildCount; int _virtualChildCount; @@ -262,11 +315,11 @@ abstract class RenderVirtualViewport new RenderViewport( - scrollDirection: scrollDirection, - paintOffset: paintOffset, - overlayPainter: overlayPainter - ); + RenderViewport createRenderObject() { + return new RenderViewport( + paintOffset: paintOffset, + scrollDirection: scrollDirection, + scrollAnchor: scrollAnchor, + overlayPainter: overlayPainter + ); + } void updateRenderObject(RenderViewport renderObject, Viewport oldWidget) { // Order dependency: RenderViewport validates scrollOffset based on scrollDirection. renderObject ..scrollDirection = scrollDirection + ..scrollAnchor = scrollAnchor ..paintOffset = paintOffset ..overlayPainter = overlayPainter; } diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart index d2318ab3b63..8036dc5b801 100644 --- a/packages/flutter/lib/src/widgets/scrollable.dart +++ b/packages/flutter/lib/src/widgets/scrollable.dart @@ -520,8 +520,9 @@ class _ScrollableViewportState extends ScrollableState { return new SizeObserver( onSizeChanged: _handleViewportSizeChanged, child: new Viewport( - scrollDirection: config.scrollDirection, paintOffset: scrollOffsetToPixelDelta(scrollOffset), + scrollDirection: config.scrollDirection, + scrollAnchor: config.scrollAnchor, child: new SizeObserver( onSizeChanged: _handleChildSizeChanged, child: config.child @@ -541,6 +542,7 @@ class Block extends StatelessComponent { this.padding, this.initialScrollOffset, this.scrollDirection: Axis.vertical, + this.scrollAnchor: ViewportAnchor.start, this.onScroll, this.scrollableKey }) : super(key: key) { @@ -552,6 +554,7 @@ class Block extends StatelessComponent { final EdgeDims padding; final double initialScrollOffset; final Axis scrollDirection; + final ViewportAnchor scrollAnchor; final ScrollListener onScroll; final Key scrollableKey; @@ -563,6 +566,7 @@ class Block extends StatelessComponent { key: scrollableKey, initialScrollOffset: initialScrollOffset, scrollDirection: scrollDirection, + scrollAnchor: scrollAnchor, onScroll: onScroll, child: contents ); diff --git a/packages/flutter/test/widget/block_test.dart b/packages/flutter/test/widget/block_test.dart index 0d66305a3d2..ac2de5b2362 100644 --- a/packages/flutter/test/widget/block_test.dart +++ b/packages/flutter/test/widget/block_test.dart @@ -66,4 +66,53 @@ void main() { tester.dispatchEvent(pointer.up(), target); }); }); + + test('Scroll anchor', () { + testWidgets((WidgetTester tester) { + int first = 0; + int second = 0; + + Widget buildBlock(ViewportAnchor scrollAnchor) { + return new Block( + key: new UniqueKey(), + scrollAnchor: scrollAnchor, + children: [ + new GestureDetector( + onTap: () { ++first; }, + child: new Container( + height: 2000.0, // more than 600, the height of the test area + decoration: new BoxDecoration( + backgroundColor: new Color(0xFF00FF00) + ) + ) + ), + new GestureDetector( + onTap: () { ++second; }, + child: new Container( + height: 2000.0, // more than 600, the height of the test area + decoration: new BoxDecoration( + backgroundColor: new Color(0xFF0000FF) + ) + ) + ) + ] + ); + } + + tester.pumpWidget(buildBlock(ViewportAnchor.end)); + tester.pump(); // for SizeObservers + + Point target = const Point(200.0, 200.0); + tester.tapAt(target); + expect(first, equals(0)); + expect(second, equals(1)); + + tester.pumpWidget(buildBlock(ViewportAnchor.start)); + tester.pump(); // for SizeObservers + + tester.tapAt(target); + expect(first, equals(1)); + expect(second, equals(1)); + }); + }); }