Commit efd5aea0 authored by Adam Barth's avatar Adam Barth

Merge pull request #1736 from abarth/scroll_anchor3

Add scroll anchor for Block
parents a82b8963 f0276d09
...@@ -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