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,
|
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
_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<
|
|||||||
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<
|
|||||||
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<
|
|||||||
|
|
||||||
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<
|
|||||||
}
|
}
|
||||||
|
|
||||||
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<
|
|||||||
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
|
|||||||
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
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
|||||||
TextStyle,
|
TextStyle,
|
||||||
TransferMode,
|
TransferMode,
|
||||||
ValueChanged,
|
ValueChanged,
|
||||||
|
ViewportAnchor,
|
||||||
VoidCallback;
|
VoidCallback;
|
||||||
|
|
||||||
|
|
||||||
@ -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 {
|
|||||||
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 {
|
|||||||
/// 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> {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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() {
|
|||||||
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));
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user