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 {
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<T extends ContainerBoxParentDataMixin<Rende
LayoutCallback callback,
Offset paintOffset: Offset.zero,
Axis scrollDirection: Axis.vertical,
ViewportAnchor scrollAnchor: ViewportAnchor.start,
Painter overlayPainter
}) : _virtualChildCount = virtualChildCount,
_callback = callback,
super(paintOffset, scrollDirection, overlayPainter);
super(paintOffset, scrollDirection, scrollAnchor, overlayPainter);
int get virtualChildCount => _virtualChildCount;
int _virtualChildCount;
......@@ -262,11 +315,11 @@ abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<Rende
}
bool hitTestChildren(HitTestResult result, { Point position }) {
return defaultHitTestChildren(result, position: position + -_paintOffsetRoundedToIntegerDevicePixels);
return defaultHitTestChildren(result, position: position + -_effectivePaintOffset);
}
void _paintContents(PaintingContext context, Offset offset) {
defaultPaint(context, offset + _paintOffsetRoundedToIntegerDevicePixels);
defaultPaint(context, offset + _effectivePaintOffset);
_overlayPainter?.paint(context, offset);
}
......
......@@ -65,6 +65,7 @@ export 'package:flutter/rendering.dart' show
TextStyle,
TransferMode,
ValueChanged,
ViewportAnchor,
VoidCallback;
......@@ -796,8 +797,9 @@ class Baseline extends OneChildRenderObjectWidget {
class Viewport extends OneChildRenderObjectWidget {
Viewport({
Key key,
this.scrollDirection: Axis.vertical,
this.paintOffset: Offset.zero,
this.scrollDirection: Axis.vertical,
this.scrollAnchor: ViewportAnchor.start,
this.overlayPainter,
Widget child
}) : super(key: key, child: child) {
......@@ -805,6 +807,11 @@ class Viewport extends OneChildRenderObjectWidget {
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
///
/// If the viewport is scrollable in a particular direction (e.g., vertically),
......@@ -812,26 +819,27 @@ class Viewport extends OneChildRenderObjectWidget {
/// that direction (e.g., the child can be as tall as it wants).
final Axis scrollDirection;
/// The offset at which to paint the child.
///
/// The offset can be non-zero only in the [scrollDirection].
final Offset paintOffset;
final ViewportAnchor scrollAnchor;
/// Paints an overlay over the viewport.
///
/// Often used to paint scroll bars.
final Painter overlayPainter;
RenderViewport createRenderObject() => 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;
}
......
......@@ -520,8 +520,9 @@ class _ScrollableViewportState extends ScrollableState<ScrollableViewport> {
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
);
......
......@@ -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: <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