Unverified Commit 80fb7bd2 authored by Kate Lovett's avatar Kate Lovett Committed by GitHub

Support ensureVisible/showOnScreen/showInViewport for 2D Scrolling (#135182)

parent 47f12cae
...@@ -1121,11 +1121,18 @@ class RenderListWheelViewport ...@@ -1121,11 +1121,18 @@ class RenderListWheelViewport
} }
@override @override
RevealedOffset getOffsetToReveal(RenderObject target, double alignment, { Rect? rect }) { RevealedOffset getOffsetToReveal(
RenderObject target,
double alignment, {
Rect? rect,
Axis? axis,
}) {
// One dimensional viewport has only one axis, it should match if it has
// been provided.
assert(axis == null || axis == Axis.vertical);
// `target` is only fully revealed when in the selected/center position. Therefore, // `target` is only fully revealed when in the selected/center position. Therefore,
// this method always returns the offset that shows `target` in the center position, // this method always returns the offset that shows `target` in the center position,
// which is the same offset for all `alignment` values. // which is the same offset for all `alignment` values.
rect ??= target.paintBounds; rect ??= target.paintBounds;
// `child` will be the last RenderObject before the viewport when walking up from `target`. // `child` will be the last RenderObject before the viewport when walking up from `target`.
......
...@@ -108,10 +108,22 @@ abstract interface class RenderAbstractViewport extends RenderObject { ...@@ -108,10 +108,22 @@ abstract interface class RenderAbstractViewport extends RenderObject {
/// when the offset of the viewport is changed by x then `target` also moves /// when the offset of the viewport is changed by x then `target` also moves
/// by x within the viewport. /// by x within the viewport.
/// ///
/// The optional [Axis] is used by
/// [RenderTwoDimensionalViewport.getOffsetToReveal] to
/// determine which of the two axes to compute an offset for. One dimensional
/// subclasses like [RenderViewportBase] and [RenderListWheelViewport] will
/// assert in debug builds if the `axis` value is provided and does not match
/// the single [Axis] that viewport is configured for.
///
/// See also: /// See also:
/// ///
/// * [RevealedOffset], which describes the return value of this method. /// * [RevealedOffset], which describes the return value of this method.
RevealedOffset getOffsetToReveal(RenderObject target, double alignment, { Rect? rect }); RevealedOffset getOffsetToReveal(
RenderObject target,
double alignment, {
Rect? rect,
Axis? axis,
});
/// The default value for the cache extent of the viewport. /// The default value for the cache extent of the viewport.
/// ///
...@@ -169,6 +181,56 @@ class RevealedOffset { ...@@ -169,6 +181,56 @@ class RevealedOffset {
/// value for a specific element. /// value for a specific element.
final Rect rect; final Rect rect;
/// Determines which provided leading or trailing edge of the viewport, as
/// [RevealedOffset]s, will be used for [RenderViewportBase.showInViewport]
/// accounting for the size and already visible portion of the [RenderObject]
/// that is being revealed.
///
/// Also used by [RenderTwoDimensionalViewport.showInViewport] for each
/// horizontal and vertical [Axis].
///
/// If the target [RenderObject] is already fully visible, this will return
/// null.
static RevealedOffset? clampOffset({
required RevealedOffset leadingEdgeOffset,
required RevealedOffset trailingEdgeOffset,
required double currentOffset,
}) {
// scrollOffset
// 0 +---------+
// | |
// _ | |
// viewport position | | |
// with `descendant` at | | | _
// trailing edge |_ | xxxxxxx | | viewport position
// | | | with `descendant` at
// | | _| leading edge
// | |
// 800 +---------+
//
// `trailingEdgeOffset`: Distance from scrollOffset 0 to the start of the
// viewport on the left in image above.
// `leadingEdgeOffset`: Distance from scrollOffset 0 to the start of the
// viewport on the right in image above.
//
// The viewport position on the left is achieved by setting `offset.pixels`
// to `trailingEdgeOffset`, the one on the right by setting it to
// `leadingEdgeOffset`.
final bool inverted = leadingEdgeOffset.offset < trailingEdgeOffset.offset;
final RevealedOffset smaller;
final RevealedOffset larger;
(smaller, larger) = inverted
? (leadingEdgeOffset, trailingEdgeOffset)
: (trailingEdgeOffset, leadingEdgeOffset);
if (currentOffset > larger.offset) {
return larger;
} else if (currentOffset < smaller.offset) {
return smaller;
} else {
return null;
}
}
@override @override
String toString() { String toString() {
return '${objectRuntimeType(this, 'RevealedOffset')}(offset: $offset, rect: $rect)'; return '${objectRuntimeType(this, 'RevealedOffset')}(offset: $offset, rect: $rect)';
...@@ -753,7 +815,17 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix ...@@ -753,7 +815,17 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
} }
@override @override
RevealedOffset getOffsetToReveal(RenderObject target, double alignment, { Rect? rect }) { RevealedOffset getOffsetToReveal(
RenderObject target,
double alignment, {
Rect? rect,
Axis? axis,
}) {
// One dimensional viewport has only one axis, it should match if it has
// been provided.
axis ??= this.axis;
assert(axis == this.axis);
// Steps to convert `rect` (from a RenderBox coordinate system) to its // Steps to convert `rect` (from a RenderBox coordinate system) to its
// scroll offset within this viewport (not in the exact order): // scroll offset within this viewport (not in the exact order):
// //
...@@ -1164,44 +1236,12 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix ...@@ -1164,44 +1236,12 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
final RevealedOffset leadingEdgeOffset = viewport.getOffsetToReveal(descendant, 0.0, rect: rect); final RevealedOffset leadingEdgeOffset = viewport.getOffsetToReveal(descendant, 0.0, rect: rect);
final RevealedOffset trailingEdgeOffset = viewport.getOffsetToReveal(descendant, 1.0, rect: rect); final RevealedOffset trailingEdgeOffset = viewport.getOffsetToReveal(descendant, 1.0, rect: rect);
final double currentOffset = offset.pixels; final double currentOffset = offset.pixels;
final RevealedOffset? targetOffset = RevealedOffset.clampOffset(
// scrollOffset leadingEdgeOffset: leadingEdgeOffset,
// 0 +---------+ trailingEdgeOffset: trailingEdgeOffset,
// | | currentOffset: currentOffset,
// _ | | );
// viewport position | | | if (targetOffset == null) {
// with `descendant` at | | | _
// trailing edge |_ | xxxxxxx | | viewport position
// | | | with `descendant` at
// | | _| leading edge
// | |
// 800 +---------+
//
// `trailingEdgeOffset`: Distance from scrollOffset 0 to the start of the
// viewport on the left in image above.
// `leadingEdgeOffset`: Distance from scrollOffset 0 to the start of the
// viewport on the right in image above.
//
// The viewport position on the left is achieved by setting `offset.pixels`
// to `trailingEdgeOffset`, the one on the right by setting it to
// `leadingEdgeOffset`.
final RevealedOffset targetOffset;
if (leadingEdgeOffset.offset < trailingEdgeOffset.offset) {
// `descendant` is too big to be visible on screen in its entirety. Let's
// align it with the edge that requires the least amount of scrolling.
final double leadingEdgeDiff = (offset.pixels - leadingEdgeOffset.offset).abs();
final double trailingEdgeDiff = (offset.pixels - trailingEdgeOffset.offset).abs();
targetOffset = leadingEdgeDiff < trailingEdgeDiff ? leadingEdgeOffset : trailingEdgeOffset;
} else if (currentOffset > leadingEdgeOffset.offset) {
// `descendant` currently starts above the leading edge and can be shown
// fully on screen by scrolling down (which means: moving viewport up).
targetOffset = leadingEdgeOffset;
} else if (currentOffset < trailingEdgeOffset.offset) {
// `descendant currently ends below the trailing edge and can be shown
// fully on screen by scrolling up (which means: moving viewport down)
targetOffset = trailingEdgeOffset;
} else {
// `descendant` is between leading and trailing edge and hence already // `descendant` is between leading and trailing edge and hence already
// fully shown on screen. No action necessary. // fully shown on screen. No action necessary.
assert(viewport.parent != null); assert(viewport.parent != null);
...@@ -1209,7 +1249,6 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix ...@@ -1209,7 +1249,6 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
return MatrixUtils.transformRect(transform, rect ?? descendant.paintBounds); return MatrixUtils.transformRect(transform, rect ?? descendant.paintBounds);
} }
offset.moveTo(targetOffset.offset, duration: duration, curve: curve); offset.moveTo(targetOffset.offset, duration: duration, curve: curve);
return targetOffset.rect; return targetOffset.rect;
} }
......
...@@ -810,14 +810,32 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics { ...@@ -810,14 +810,32 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
double target; double target;
switch (_applyAxisDirectionToAlignmentPolicy(alignmentPolicy)) { switch (_applyAxisDirectionToAlignmentPolicy(alignmentPolicy)) {
case ScrollPositionAlignmentPolicy.explicit: case ScrollPositionAlignmentPolicy.explicit:
target = clampDouble(viewport.getOffsetToReveal(object, alignment, rect: targetRect).offset, minScrollExtent, maxScrollExtent); target = viewport.getOffsetToReveal(
object,
alignment,
rect: targetRect,
axis: axis,
).offset;
target = clampDouble(target, minScrollExtent, maxScrollExtent);
case ScrollPositionAlignmentPolicy.keepVisibleAtEnd: case ScrollPositionAlignmentPolicy.keepVisibleAtEnd:
target = clampDouble(viewport.getOffsetToReveal(object, 1.0, rect: targetRect).offset, minScrollExtent, maxScrollExtent); target = viewport.getOffsetToReveal(
object,
1.0, // Aligns to end
rect: targetRect,
axis: axis,
).offset;
target = clampDouble(target, minScrollExtent, maxScrollExtent);
if (target < pixels) { if (target < pixels) {
target = pixels; target = pixels;
} }
case ScrollPositionAlignmentPolicy.keepVisibleAtStart: case ScrollPositionAlignmentPolicy.keepVisibleAtStart:
target = clampDouble(viewport.getOffsetToReveal(object, 0.0, rect: targetRect).offset, minScrollExtent, maxScrollExtent); target = viewport.getOffsetToReveal(
object,
0.0, // Aligns to start
rect: targetRect,
axis: axis,
).offset;
target = clampDouble(target, minScrollExtent, maxScrollExtent);
if (target > pixels) { if (target > pixels) {
target = pixels; target = pixels;
} }
......
...@@ -44,6 +44,13 @@ typedef ViewportBuilder = Widget Function(BuildContext context, ViewportOffset p ...@@ -44,6 +44,13 @@ typedef ViewportBuilder = Widget Function(BuildContext context, ViewportOffset p
/// which the scrollable content is displayed. /// which the scrollable content is displayed.
typedef TwoDimensionalViewportBuilder = Widget Function(BuildContext context, ViewportOffset verticalPosition, ViewportOffset horizontalPosition); typedef TwoDimensionalViewportBuilder = Widget Function(BuildContext context, ViewportOffset verticalPosition, ViewportOffset horizontalPosition);
// The return type of _performEnsureVisible.
//
// The list of futures represents each pending ScrollPosition call to
// ensureVisible. The returned ScrollableState's context is used to find the
// next potential ancestor Scrollable.
typedef _EnsureVisibleResults = (List<Future<void>>, ScrollableState);
/// A widget that manages scrolling in one dimension and informs the [Viewport] /// A widget that manages scrolling in one dimension and informs the [Viewport]
/// through which the content is viewed. /// through which the content is viewed.
/// ///
...@@ -441,6 +448,10 @@ class Scrollable extends StatefulWidget { ...@@ -441,6 +448,10 @@ class Scrollable extends StatefulWidget {
/// Scrolls the scrollables that enclose the given context so as to make the /// Scrolls the scrollables that enclose the given context so as to make the
/// given context visible. /// given context visible.
///
/// If the [Scrollable] of the provided [BuildContext] is a
/// [TwoDimensionalScrollable], both vertical and horizontal axes will ensure
/// the target is made visible.
static Future<void> ensureVisible( static Future<void> ensureVisible(
BuildContext context, { BuildContext context, {
double alignment = 0.0, double alignment = 0.0,
...@@ -459,14 +470,16 @@ class Scrollable extends StatefulWidget { ...@@ -459,14 +470,16 @@ class Scrollable extends StatefulWidget {
RenderObject? targetRenderObject; RenderObject? targetRenderObject;
ScrollableState? scrollable = Scrollable.maybeOf(context); ScrollableState? scrollable = Scrollable.maybeOf(context);
while (scrollable != null) { while (scrollable != null) {
futures.add(scrollable.position.ensureVisible( final List<Future<void>> newFutures;
(newFutures, scrollable) = scrollable._performEnsureVisible(
context.findRenderObject()!, context.findRenderObject()!,
alignment: alignment, alignment: alignment,
duration: duration, duration: duration,
curve: curve, curve: curve,
alignmentPolicy: alignmentPolicy, alignmentPolicy: alignmentPolicy,
targetRenderObject: targetRenderObject, targetRenderObject: targetRenderObject,
)); );
futures.addAll(newFutures);
targetRenderObject = targetRenderObject ?? context.findRenderObject(); targetRenderObject = targetRenderObject ?? context.findRenderObject();
context = scrollable.context; context = scrollable.context;
...@@ -1011,6 +1024,28 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R ...@@ -1011,6 +1024,28 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
return result; return result;
} }
// Returns the Future from calling ensureVisible for the ScrollPosition, as
// as well as this ScrollableState instance so its context can be used to
// check for other ancestor Scrollables in executing ensureVisible.
_EnsureVisibleResults _performEnsureVisible(
RenderObject object, {
double alignment = 0.0,
Duration duration = Duration.zero,
Curve curve = Curves.ease,
ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit,
RenderObject? targetRenderObject,
}) {
final Future<void> ensureVisibleFuture = position.ensureVisible(
object,
alignment: alignment,
duration: duration,
curve: curve,
alignmentPolicy: alignmentPolicy,
targetRenderObject: targetRenderObject,
);
return (<Future<void>>[ ensureVisibleFuture ], this);
}
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
...@@ -2040,6 +2075,25 @@ class _VerticalOuterDimension extends Scrollable { ...@@ -2040,6 +2075,25 @@ class _VerticalOuterDimension extends Scrollable {
class _VerticalOuterDimensionState extends ScrollableState { class _VerticalOuterDimensionState extends ScrollableState {
DiagonalDragBehavior get diagonalDragBehavior => (widget as _VerticalOuterDimension).diagonalDragBehavior; DiagonalDragBehavior get diagonalDragBehavior => (widget as _VerticalOuterDimension).diagonalDragBehavior;
// Implemented in the _HorizontalInnerDimension instead.
@override
_EnsureVisibleResults _performEnsureVisible(
RenderObject object, {
double alignment = 0.0,
Duration duration = Duration.zero,
Curve curve = Curves.ease,
ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit,
RenderObject? targetRenderObject,
}) {
assert(
false,
'The _performEnsureVisible method was called for the vertical scrollable '
'of a TwoDimensionalScrollable. This should not happen as the horizontal '
'scrollable handles both axes.'
);
return (<Future<void>>[], this);
}
@override @override
void setCanDrag(bool value) { void setCanDrag(bool value) {
switch (diagonalDragBehavior) { switch (diagonalDragBehavior) {
...@@ -2119,6 +2173,39 @@ class _HorizontalInnerDimensionState extends ScrollableState { ...@@ -2119,6 +2173,39 @@ class _HorizontalInnerDimensionState extends ScrollableState {
super.didChangeDependencies(); super.didChangeDependencies();
} }
// Returns the Future from calling ensureVisible for the ScrollPosition, as
// as well as the vertical ScrollableState instance so its context can be
// used to check for other ancestor Scrollables in executing ensureVisible.
@override
_EnsureVisibleResults _performEnsureVisible(
RenderObject object, {
double alignment = 0.0,
Duration duration = Duration.zero,
Curve curve = Curves.ease,
ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit,
RenderObject? targetRenderObject,
}) {
final List<Future<void>> newFutures = <Future<void>>[];
newFutures.add(position.ensureVisible(
object,
alignment: alignment,
duration: duration,
curve: curve,
alignmentPolicy: alignmentPolicy,
));
newFutures.add(verticalScrollable.position.ensureVisible(
object,
alignment: alignment,
duration: duration,
curve: curve,
alignmentPolicy: alignmentPolicy,
));
return (newFutures, verticalScrollable);
}
void _evaluateLockedAxis(Offset offset) { void _evaluateLockedAxis(Offset offset) {
assert(lastDragOffset != null); assert(lastDragOffset != null);
final Offset offsetDelta = lastDragOffset! - offset; final Offset offsetDelta = lastDragOffset! - offset;
......
...@@ -592,7 +592,17 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix ...@@ -592,7 +592,17 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
} }
@override @override
RevealedOffset getOffsetToReveal(RenderObject target, double alignment, { Rect? rect }) { RevealedOffset getOffsetToReveal(
RenderObject target,
double alignment, {
Rect? rect,
Axis? axis,
}) {
// One dimensional viewport has only one axis, it should match if it has
// been provided.
axis ??= this.axis;
assert(axis == this.axis);
rect ??= target.paintBounds; rect ??= target.paintBounds;
if (target is! RenderBox) { if (target is! RenderBox) {
return RevealedOffset(offset: offset.pixels, rect: rect); return RevealedOffset(offset: offset.pixels, rect: rect);
......
...@@ -9,6 +9,8 @@ import 'package:flutter/widgets.dart'; ...@@ -9,6 +9,8 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
import 'two_dimensional_utils.dart';
Finder findKey(int i) => find.byKey(ValueKey<int>(i), skipOffstage: false); Finder findKey(int i) => find.byKey(ValueKey<int>(i), skipOffstage: false);
Widget buildSingleChildScrollView(Axis scrollDirection, { bool reverse = false }) { Widget buildSingleChildScrollView(Axis scrollDirection, { bool reverse = false }) {
...@@ -1051,4 +1053,279 @@ void main() { ...@@ -1051,4 +1053,279 @@ void main() {
expect(tester.getTopLeft(findKey(-3)).dy, equals(100.0)); expect(tester.getTopLeft(findKey(-3)).dy, equals(100.0));
}); });
}); });
group('TwoDimensionalViewport ensureVisible', () {
Finder findKey(ChildVicinity vicinity) {
return find.byKey(ValueKey<ChildVicinity>(vicinity));
}
BuildContext findContext(WidgetTester tester, ChildVicinity vicinity) {
return tester.element(findKey(vicinity));
}
testWidgets('Axis.vertical', (WidgetTester tester) async {
await tester.pumpWidget(simpleBuilderTest(useCacheExtent: true));
Scrollable.ensureVisible(findContext(
tester,
const ChildVicinity(xIndex: 0, yIndex: 0)),
);
await tester.pump();
expect(
tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 0))).dy,
equals(0.0),
);
// (0, 3) is in the cache extent, and will be brought into view next
expect(
tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 3))).dy,
equals(600.0),
);
Scrollable.ensureVisible(findContext(
tester,
const ChildVicinity(xIndex: 0, yIndex: 3)),
);
await tester.pump();
// Now in view at top edge of viewport
expect(
tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 3))).dy,
equals(0.0),
);
// If already visible, no change
Scrollable.ensureVisible(findContext(
tester,
const ChildVicinity(xIndex: 0, yIndex: 3)),
);
await tester.pump();
expect(
tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 3))).dy,
equals(0.0),
);
});
testWidgets('Axis.horizontal', (WidgetTester tester) async {
await tester.pumpWidget(simpleBuilderTest(useCacheExtent: true));
Scrollable.ensureVisible(findContext(
tester,
const ChildVicinity(xIndex: 1, yIndex: 0)),
);
await tester.pump();
expect(
tester.getTopLeft(findKey(const ChildVicinity(xIndex: 1, yIndex: 0))).dx,
equals(0.0),
);
// (5, 0) is now in the cache extent, and will be brought into view next
expect(
tester.getTopLeft(findKey(const ChildVicinity(xIndex: 5, yIndex: 0))).dx,
equals(800.0),
);
Scrollable.ensureVisible(findContext(
tester,
const ChildVicinity(xIndex: 5, yIndex: 0)),
alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd,
);
await tester.pump();
// Now in view at trailing edge of viewport
expect(
tester.getTopLeft(findKey(const ChildVicinity(xIndex: 5, yIndex: 0))).dx,
equals(600.0),
);
// If already in position, no change
Scrollable.ensureVisible(findContext(
tester,
const ChildVicinity(xIndex: 5, yIndex: 0)),
alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd,
);
await tester.pump();
expect(
tester.getTopLeft(findKey(const ChildVicinity(xIndex: 5, yIndex: 0))).dx,
equals(600.0),
);
});
testWidgets('both axes', (WidgetTester tester) async {
await tester.pumpWidget(simpleBuilderTest(useCacheExtent: true));
Scrollable.ensureVisible(findContext(
tester,
const ChildVicinity(xIndex: 1, yIndex: 1)),
);
await tester.pump();
expect(
tester.getRect(findKey(const ChildVicinity(xIndex: 1, yIndex: 1))),
const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0),
);
// (5, 4) is in the cache extent, and will be brought into view next
expect(
tester.getRect(findKey(const ChildVicinity(xIndex: 5, yIndex: 4))),
const Rect.fromLTRB(800.0, 600.0, 1000.0, 800.0),
);
Scrollable.ensureVisible(findContext(
tester,
const ChildVicinity(xIndex: 5, yIndex: 4)),
alignment: 1.0, // Same as ScrollAlignmentPolicy.keepVisibleAtEnd
);
await tester.pump();
// Now in view at bottom trailing corner of viewport
expect(
tester.getRect(findKey(const ChildVicinity(xIndex: 5, yIndex: 4))),
const Rect.fromLTRB(600.0, 400.0, 800.0, 600.0),
);
// If already visible, no change
Scrollable.ensureVisible(findContext(
tester,
const ChildVicinity(xIndex: 5, yIndex: 4)),
alignment: 1.0,
);
await tester.pump();
expect(
tester.getRect(findKey(const ChildVicinity(xIndex: 5, yIndex: 4))),
const Rect.fromLTRB(600.0, 400.0, 800.0, 600.0),
);
});
testWidgets('Axis.vertical reverse', (WidgetTester tester) async {
await tester.pumpWidget(simpleBuilderTest(
verticalDetails: const ScrollableDetails.vertical(reverse: true),
useCacheExtent: true,
));
expect(
tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 0))).dy,
equals(400.0),
);
Scrollable.ensureVisible(findContext(
tester,
const ChildVicinity(xIndex: 0, yIndex: 0)),
);
await tester.pump();
// Already visible so no change.
expect(
tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 0))).dy,
equals(400.0),
);
// (0, 3) is in the cache extent, and will be brought into view next
expect(
tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 3))).dy,
equals(-200.0),
);
Scrollable.ensureVisible(findContext(
tester,
const ChildVicinity(xIndex: 0, yIndex: 3)),
);
await tester.pump();
// Now in view at bottom edge of viewport since we are reversed
expect(
tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 3))).dy,
equals(400.0),
);
// If already visible, no change
Scrollable.ensureVisible(findContext(
tester,
const ChildVicinity(xIndex: 0, yIndex: 3)),
);
await tester.pump();
expect(
tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 3))).dy,
equals(400.0),
);
});
testWidgets('Axis.horizontal reverse', (WidgetTester tester) async {
await tester.pumpWidget(simpleBuilderTest(
horizontalDetails: const ScrollableDetails.horizontal(reverse: true),
useCacheExtent: true,
));
expect(
tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 0))).dx,
equals(600.0),
);
Scrollable.ensureVisible(findContext(
tester,
const ChildVicinity(xIndex: 0, yIndex: 0)),
);
await tester.pump();
// Already visible so no change.
expect(
tester.getTopLeft(findKey(const ChildVicinity(xIndex: 0, yIndex: 0))).dx,
equals(600.0),
);
// (4, 0) is in the cache extent, and will be brought into view next
expect(
tester.getTopLeft(findKey(const ChildVicinity(xIndex: 4, yIndex: 0))).dx,
equals(-200.0),
);
Scrollable.ensureVisible(findContext(
tester,
const ChildVicinity(xIndex: 4, yIndex: 0)),
);
await tester.pump();
expect(
tester.getTopLeft(findKey(const ChildVicinity(xIndex: 4, yIndex: 0))).dx,
equals(200.0),
);
// If already visible, no change
Scrollable.ensureVisible(findContext(
tester,
const ChildVicinity(xIndex: 4, yIndex: 0)),
);
await tester.pump();
expect(
tester.getTopLeft(findKey(const ChildVicinity(xIndex: 4, yIndex: 0))).dx,
equals(200.0),
);
});
testWidgets('both axes reverse', (WidgetTester tester) async {
await tester.pumpWidget(simpleBuilderTest(
verticalDetails: const ScrollableDetails.vertical(reverse: true),
horizontalDetails: const ScrollableDetails.horizontal(reverse: true),
useCacheExtent: true,
));
Scrollable.ensureVisible(findContext(
tester,
const ChildVicinity(xIndex: 1, yIndex: 1)),
);
await tester.pump();
expect(
tester.getRect(findKey(const ChildVicinity(xIndex: 1, yIndex: 1))),
const Rect.fromLTRB(600.0, 400.0, 800.0, 600.0),
);
// (5, 4) is in the cache extent, and will be brought into view next
expect(
tester.getRect(findKey(const ChildVicinity(xIndex: 5, yIndex: 4))),
const Rect.fromLTRB(-200.0, -200.0, 0.0, 0.0),
);
Scrollable.ensureVisible(findContext(
tester,
const ChildVicinity(xIndex: 5, yIndex: 4)),
alignment: 1.0, // Same as ScrollAlignmentPolicy.keepVisibleAtEnd
);
await tester.pump();
// Now in view at trailing corner of viewport
expect(
tester.getRect(findKey(const ChildVicinity(xIndex: 5, yIndex: 4))),
const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0),
);
// If already visible, no change
Scrollable.ensureVisible(findContext(
tester,
const ChildVicinity(xIndex: 5, yIndex: 4)),
alignment: 1.0,
);
await tester.pump();
expect(
tester.getRect(findKey(const ChildVicinity(xIndex: 5, yIndex: 4))),
const Rect.fromLTRB(0.0, 0.0, 200.0, 200.0),
);
});
});
} }
...@@ -17,6 +17,7 @@ final TwoDimensionalChildBuilderDelegate builderDelegate = TwoDimensionalChildBu ...@@ -17,6 +17,7 @@ final TwoDimensionalChildBuilderDelegate builderDelegate = TwoDimensionalChildBu
maxYIndex: 5, maxYIndex: 5,
builder: (BuildContext context, ChildVicinity vicinity) { builder: (BuildContext context, ChildVicinity vicinity) {
return Container( return Container(
key: ValueKey<ChildVicinity>(vicinity),
color: vicinity.xIndex.isEven && vicinity.yIndex.isEven color: vicinity.xIndex.isEven && vicinity.yIndex.isEven
? Colors.amber[100] ? Colors.amber[100]
: (vicinity.xIndex.isOdd && vicinity.yIndex.isOdd : (vicinity.xIndex.isOdd && vicinity.yIndex.isOdd
......
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