Commit f0276d09 authored by Adam Barth's avatar Adam Barth

Add scroll anchor for Block

This patch teaches block how to anchor its scrolling to the end rather than the
start.

Fixes #136
parent b6678c62
...@@ -14,6 +14,33 @@ enum ViewportAnchor { ...@@ -14,6 +14,33 @@ enum ViewportAnchor {
end, 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 { abstract class HasScrollDirection {
Axis get scrollDirection; Axis get scrollDirection;
} }
...@@ -27,9 +54,11 @@ class RenderViewportBase extends RenderBox implements HasScrollDirection { ...@@ -27,9 +54,11 @@ class RenderViewportBase extends RenderBox implements HasScrollDirection {
RenderViewportBase( RenderViewportBase(
Offset paintOffset, Offset paintOffset,
Axis scrollDirection, Axis scrollDirection,
ViewportAnchor scrollAnchor,
Painter overlayPainter Painter overlayPainter
) : _paintOffset = paintOffset, ) : _paintOffset = paintOffset,
_scrollDirection = scrollDirection, _scrollDirection = scrollDirection,
_scrollAnchor = scrollAnchor,
_overlayPainter = overlayPainter { _overlayPainter = overlayPainter {
assert(paintOffset != null); assert(paintOffset != null);
assert(scrollDirection != null); assert(scrollDirection != null);
...@@ -76,6 +105,17 @@ class RenderViewportBase extends RenderBox implements HasScrollDirection { ...@@ -76,6 +105,17 @@ class RenderViewportBase extends RenderBox implements HasScrollDirection {
markNeedsLayout(); 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 get overlayPainter => _overlayPainter;
Painter _overlayPainter; Painter _overlayPainter;
void set overlayPainter(Painter value) { void set overlayPainter(Painter value) {
...@@ -99,16 +139,25 @@ class RenderViewportBase extends RenderBox implements HasScrollDirection { ...@@ -99,16 +139,25 @@ class RenderViewportBase extends RenderBox implements HasScrollDirection {
_overlayPainter?.detach(); _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; final double devicePixelRatio = ui.window.devicePixelRatio;
int dxInDevicePixels = (_paintOffset.dx * devicePixelRatio).round(); int dxInDevicePixels = (_paintOffset.dx * devicePixelRatio).round();
int dyInDevicePixels = (_paintOffset.dy * devicePixelRatio).round(); int dyInDevicePixels = (_paintOffset.dy * devicePixelRatio).round();
return new Offset(dxInDevicePixels / devicePixelRatio, return _dimensions.getAbsolutePaintOffset(
dyInDevicePixels / devicePixelRatio); paintOffset: new Offset(dxInDevicePixels / devicePixelRatio, dyInDevicePixels / devicePixelRatio),
anchor: _scrollAnchor
);
} }
void applyPaintTransform(RenderBox child, Matrix4 transform) { void applyPaintTransform(RenderBox child, Matrix4 transform) {
final Offset effectivePaintOffset = _paintOffsetRoundedToIntegerDevicePixels; final Offset effectivePaintOffset = _effectivePaintOffset;
super.applyPaintTransform(child, transform.translate(effectivePaintOffset.dx, effectivePaintOffset.dy)); super.applyPaintTransform(child, transform.translate(effectivePaintOffset.dx, effectivePaintOffset.dy));
} }
...@@ -126,8 +175,9 @@ class RenderViewport extends RenderViewportBase with RenderObjectWithChildMixin< ...@@ -126,8 +175,9 @@ class RenderViewport extends RenderViewportBase with RenderObjectWithChildMixin<
RenderBox child, RenderBox child,
Offset paintOffset: Offset.zero, Offset paintOffset: Offset.zero,
Axis scrollDirection: Axis.vertical, Axis scrollDirection: Axis.vertical,
ViewportAnchor scrollAnchor: ViewportAnchor.start,
Painter overlayPainter Painter overlayPainter
}) : super(paintOffset, scrollDirection, overlayPainter) { }) : super(paintOffset, scrollDirection, scrollAnchor, overlayPainter) {
this.child = child; this.child = child;
} }
...@@ -183,8 +233,10 @@ class RenderViewport extends RenderViewportBase with RenderObjectWithChildMixin< ...@@ -183,8 +233,10 @@ class RenderViewport extends RenderViewportBase with RenderObjectWithChildMixin<
size = constraints.constrain(child.size); size = constraints.constrain(child.size);
final BoxParentData childParentData = child.parentData; final BoxParentData childParentData = child.parentData;
childParentData.offset = Offset.zero; childParentData.offset = Offset.zero;
dimensions = new ViewportDimensions(containerSize: size, contentSize: child.size);
} else { } else {
performResize(); performResize();
dimensions = new ViewportDimensions(containerSize: size);
} }
} }
...@@ -195,7 +247,7 @@ class RenderViewport extends RenderViewportBase with RenderObjectWithChildMixin< ...@@ -195,7 +247,7 @@ class RenderViewport extends RenderViewportBase with RenderObjectWithChildMixin<
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (child != null) { if (child != null) {
final Offset effectivePaintOffset = _paintOffsetRoundedToIntegerDevicePixels; final Offset effectivePaintOffset = _effectivePaintOffset;
void paintContents(PaintingContext context, Offset offset) { void paintContents(PaintingContext context, Offset offset) {
context.paintChild(child, offset + effectivePaintOffset); context.paintChild(child, offset + effectivePaintOffset);
...@@ -211,7 +263,7 @@ class RenderViewport extends RenderViewportBase with RenderObjectWithChildMixin< ...@@ -211,7 +263,7 @@ class RenderViewport extends RenderViewportBase with RenderObjectWithChildMixin<
} }
Rect describeApproximatePaintClip(RenderObject child) { Rect describeApproximatePaintClip(RenderObject child) {
if (child != null && _shouldClipAtPaintOffset(_paintOffsetRoundedToIntegerDevicePixels)) if (child != null && _shouldClipAtPaintOffset(_effectivePaintOffset))
return Point.origin & size; return Point.origin & size;
return null; return null;
} }
...@@ -219,7 +271,7 @@ class RenderViewport extends RenderViewportBase with RenderObjectWithChildMixin< ...@@ -219,7 +271,7 @@ class RenderViewport extends RenderViewportBase with RenderObjectWithChildMixin<
bool hitTestChildren(HitTestResult result, { Point position }) { bool hitTestChildren(HitTestResult result, { Point position }) {
if (child != null) { if (child != null) {
assert(child.parentData is BoxParentData); assert(child.parentData is BoxParentData);
Point transformed = position + -_paintOffsetRoundedToIntegerDevicePixels; Point transformed = position + -_effectivePaintOffset;
return child.hitTest(result, position: transformed); return child.hitTest(result, position: transformed);
} }
return false; return false;
...@@ -234,10 +286,11 @@ abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<Rende ...@@ -234,10 +286,11 @@ abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<Rende
LayoutCallback callback, LayoutCallback callback,
Offset paintOffset: Offset.zero, Offset paintOffset: Offset.zero,
Axis scrollDirection: Axis.vertical, Axis scrollDirection: Axis.vertical,
ViewportAnchor scrollAnchor: ViewportAnchor.start,
Painter overlayPainter Painter overlayPainter
}) : _virtualChildCount = virtualChildCount, }) : _virtualChildCount = virtualChildCount,
_callback = callback, _callback = callback,
super(paintOffset, scrollDirection, overlayPainter); super(paintOffset, scrollDirection, scrollAnchor, overlayPainter);
int get virtualChildCount => _virtualChildCount; int get virtualChildCount => _virtualChildCount;
int _virtualChildCount; int _virtualChildCount;
...@@ -262,11 +315,11 @@ abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<Rende ...@@ -262,11 +315,11 @@ abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<Rende
} }
bool hitTestChildren(HitTestResult result, { Point position }) { bool hitTestChildren(HitTestResult result, { Point position }) {
return defaultHitTestChildren(result, position: position + -_paintOffsetRoundedToIntegerDevicePixels); return defaultHitTestChildren(result, position: position + -_effectivePaintOffset);
} }
void _paintContents(PaintingContext context, Offset offset) { void _paintContents(PaintingContext context, Offset offset) {
defaultPaint(context, offset + _paintOffsetRoundedToIntegerDevicePixels); defaultPaint(context, offset + _effectivePaintOffset);
_overlayPainter?.paint(context, offset); _overlayPainter?.paint(context, offset);
} }
......
...@@ -65,6 +65,7 @@ export 'package:flutter/rendering.dart' show ...@@ -65,6 +65,7 @@ export 'package:flutter/rendering.dart' show
TextStyle, TextStyle,
TransferMode, TransferMode,
ValueChanged, ValueChanged,
ViewportAnchor,
VoidCallback; VoidCallback;
...@@ -796,8 +797,9 @@ class Baseline extends OneChildRenderObjectWidget { ...@@ -796,8 +797,9 @@ class Baseline extends OneChildRenderObjectWidget {
class Viewport extends OneChildRenderObjectWidget { class Viewport extends OneChildRenderObjectWidget {
Viewport({ Viewport({
Key key, Key key,
this.scrollDirection: Axis.vertical,
this.paintOffset: Offset.zero, this.paintOffset: Offset.zero,
this.scrollDirection: Axis.vertical,
this.scrollAnchor: ViewportAnchor.start,
this.overlayPainter, this.overlayPainter,
Widget child Widget child
}) : super(key: key, child: child) { }) : super(key: key, child: child) {
...@@ -805,6 +807,11 @@ class Viewport extends OneChildRenderObjectWidget { ...@@ -805,6 +807,11 @@ class Viewport extends OneChildRenderObjectWidget {
assert(paintOffset != null); assert(paintOffset != null);
} }
/// The offset at which to paint the child.
///
/// The offset can be non-zero only in the [scrollDirection].
final Offset paintOffset;
/// The direction in which the child is permitted to be larger than the viewport /// The direction in which the child is permitted to be larger than the viewport
/// ///
/// If the viewport is scrollable in a particular direction (e.g., vertically), /// If the viewport is scrollable in a particular direction (e.g., vertically),
...@@ -812,26 +819,27 @@ class Viewport extends OneChildRenderObjectWidget { ...@@ -812,26 +819,27 @@ class Viewport extends OneChildRenderObjectWidget {
/// that direction (e.g., the child can be as tall as it wants). /// that direction (e.g., the child can be as tall as it wants).
final Axis scrollDirection; final Axis scrollDirection;
/// The offset at which to paint the child. final ViewportAnchor scrollAnchor;
///
/// The offset can be non-zero only in the [scrollDirection].
final Offset paintOffset;
/// Paints an overlay over the viewport. /// Paints an overlay over the viewport.
/// ///
/// Often used to paint scroll bars. /// Often used to paint scroll bars.
final Painter overlayPainter; final Painter overlayPainter;
RenderViewport createRenderObject() => new RenderViewport( RenderViewport createRenderObject() {
scrollDirection: scrollDirection, return new RenderViewport(
paintOffset: paintOffset, paintOffset: paintOffset,
overlayPainter: overlayPainter scrollDirection: scrollDirection,
); scrollAnchor: scrollAnchor,
overlayPainter: overlayPainter
);
}
void updateRenderObject(RenderViewport renderObject, Viewport oldWidget) { void updateRenderObject(RenderViewport renderObject, Viewport oldWidget) {
// Order dependency: RenderViewport validates scrollOffset based on scrollDirection. // Order dependency: RenderViewport validates scrollOffset based on scrollDirection.
renderObject renderObject
..scrollDirection = scrollDirection ..scrollDirection = scrollDirection
..scrollAnchor = scrollAnchor
..paintOffset = paintOffset ..paintOffset = paintOffset
..overlayPainter = overlayPainter; ..overlayPainter = overlayPainter;
} }
......
...@@ -520,8 +520,9 @@ class _ScrollableViewportState extends ScrollableState<ScrollableViewport> { ...@@ -520,8 +520,9 @@ class _ScrollableViewportState extends ScrollableState<ScrollableViewport> {
return new SizeObserver( return new SizeObserver(
onSizeChanged: _handleViewportSizeChanged, onSizeChanged: _handleViewportSizeChanged,
child: new Viewport( child: new Viewport(
scrollDirection: config.scrollDirection,
paintOffset: scrollOffsetToPixelDelta(scrollOffset), paintOffset: scrollOffsetToPixelDelta(scrollOffset),
scrollDirection: config.scrollDirection,
scrollAnchor: config.scrollAnchor,
child: new SizeObserver( child: new SizeObserver(
onSizeChanged: _handleChildSizeChanged, onSizeChanged: _handleChildSizeChanged,
child: config.child child: config.child
...@@ -541,6 +542,7 @@ class Block extends StatelessComponent { ...@@ -541,6 +542,7 @@ class Block extends StatelessComponent {
this.padding, this.padding,
this.initialScrollOffset, this.initialScrollOffset,
this.scrollDirection: Axis.vertical, this.scrollDirection: Axis.vertical,
this.scrollAnchor: ViewportAnchor.start,
this.onScroll, this.onScroll,
this.scrollableKey this.scrollableKey
}) : super(key: key) { }) : super(key: key) {
...@@ -552,6 +554,7 @@ class Block extends StatelessComponent { ...@@ -552,6 +554,7 @@ class Block extends StatelessComponent {
final EdgeDims padding; final EdgeDims padding;
final double initialScrollOffset; final double initialScrollOffset;
final Axis scrollDirection; final Axis scrollDirection;
final ViewportAnchor scrollAnchor;
final ScrollListener onScroll; final ScrollListener onScroll;
final Key scrollableKey; final Key scrollableKey;
...@@ -563,6 +566,7 @@ class Block extends StatelessComponent { ...@@ -563,6 +566,7 @@ class Block extends StatelessComponent {
key: scrollableKey, key: scrollableKey,
initialScrollOffset: initialScrollOffset, initialScrollOffset: initialScrollOffset,
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
scrollAnchor: scrollAnchor,
onScroll: onScroll, onScroll: onScroll,
child: contents child: contents
); );
......
...@@ -66,4 +66,53 @@ void main() { ...@@ -66,4 +66,53 @@ void main() {
tester.dispatchEvent(pointer.up(), target); 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: <Widget>[
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));
});
});
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment