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 {
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