mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Add scroll anchor for Block
This patch teaches block how to anchor its scrolling to the end rather than the start. Fixes #136
This commit is contained in:
parent
b6678c62bd
commit
f0276d09e4
@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user