Skip to content

Commit 80fb7bd

Browse files
authored
Support ensureVisible/showOnScreen/showInViewport for 2D Scrolling (#135182)
1 parent 47f12ca commit 80fb7bd

9 files changed

+1026
-58
lines changed

packages/flutter/lib/src/rendering/list_wheel_viewport.dart

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1121,11 +1121,18 @@ class RenderListWheelViewport
11211121
}
11221122

11231123
@override
1124-
RevealedOffset getOffsetToReveal(RenderObject target, double alignment, { Rect? rect }) {
1124+
RevealedOffset getOffsetToReveal(
1125+
RenderObject target,
1126+
double alignment, {
1127+
Rect? rect,
1128+
Axis? axis,
1129+
}) {
1130+
// One dimensional viewport has only one axis, it should match if it has
1131+
// been provided.
1132+
assert(axis == null || axis == Axis.vertical);
11251133
// `target` is only fully revealed when in the selected/center position. Therefore,
11261134
// this method always returns the offset that shows `target` in the center position,
11271135
// which is the same offset for all `alignment` values.
1128-
11291136
rect ??= target.paintBounds;
11301137

11311138
// `child` will be the last RenderObject before the viewport when walking up from `target`.

packages/flutter/lib/src/rendering/viewport.dart

Lines changed: 80 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,22 @@ abstract interface class RenderAbstractViewport extends RenderObject {
108108
/// when the offset of the viewport is changed by x then `target` also moves
109109
/// by x within the viewport.
110110
///
111+
/// The optional [Axis] is used by
112+
/// [RenderTwoDimensionalViewport.getOffsetToReveal] to
113+
/// determine which of the two axes to compute an offset for. One dimensional
114+
/// subclasses like [RenderViewportBase] and [RenderListWheelViewport] will
115+
/// assert in debug builds if the `axis` value is provided and does not match
116+
/// the single [Axis] that viewport is configured for.
117+
///
111118
/// See also:
112119
///
113120
/// * [RevealedOffset], which describes the return value of this method.
114-
RevealedOffset getOffsetToReveal(RenderObject target, double alignment, { Rect? rect });
121+
RevealedOffset getOffsetToReveal(
122+
RenderObject target,
123+
double alignment, {
124+
Rect? rect,
125+
Axis? axis,
126+
});
115127

116128
/// The default value for the cache extent of the viewport.
117129
///
@@ -169,6 +181,56 @@ class RevealedOffset {
169181
/// value for a specific element.
170182
final Rect rect;
171183

184+
/// Determines which provided leading or trailing edge of the viewport, as
185+
/// [RevealedOffset]s, will be used for [RenderViewportBase.showInViewport]
186+
/// accounting for the size and already visible portion of the [RenderObject]
187+
/// that is being revealed.
188+
///
189+
/// Also used by [RenderTwoDimensionalViewport.showInViewport] for each
190+
/// horizontal and vertical [Axis].
191+
///
192+
/// If the target [RenderObject] is already fully visible, this will return
193+
/// null.
194+
static RevealedOffset? clampOffset({
195+
required RevealedOffset leadingEdgeOffset,
196+
required RevealedOffset trailingEdgeOffset,
197+
required double currentOffset,
198+
}) {
199+
// scrollOffset
200+
// 0 +---------+
201+
// | |
202+
// _ | |
203+
// viewport position | | |
204+
// with `descendant` at | | | _
205+
// trailing edge |_ | xxxxxxx | | viewport position
206+
// | | | with `descendant` at
207+
// | | _| leading edge
208+
// | |
209+
// 800 +---------+
210+
//
211+
// `trailingEdgeOffset`: Distance from scrollOffset 0 to the start of the
212+
// viewport on the left in image above.
213+
// `leadingEdgeOffset`: Distance from scrollOffset 0 to the start of the
214+
// viewport on the right in image above.
215+
//
216+
// The viewport position on the left is achieved by setting `offset.pixels`
217+
// to `trailingEdgeOffset`, the one on the right by setting it to
218+
// `leadingEdgeOffset`.
219+
final bool inverted = leadingEdgeOffset.offset < trailingEdgeOffset.offset;
220+
final RevealedOffset smaller;
221+
final RevealedOffset larger;
222+
(smaller, larger) = inverted
223+
? (leadingEdgeOffset, trailingEdgeOffset)
224+
: (trailingEdgeOffset, leadingEdgeOffset);
225+
if (currentOffset > larger.offset) {
226+
return larger;
227+
} else if (currentOffset < smaller.offset) {
228+
return smaller;
229+
} else {
230+
return null;
231+
}
232+
}
233+
172234
@override
173235
String toString() {
174236
return '${objectRuntimeType(this, 'RevealedOffset')}(offset: $offset, rect: $rect)';
@@ -753,7 +815,17 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
753815
}
754816

755817
@override
756-
RevealedOffset getOffsetToReveal(RenderObject target, double alignment, { Rect? rect }) {
818+
RevealedOffset getOffsetToReveal(
819+
RenderObject target,
820+
double alignment, {
821+
Rect? rect,
822+
Axis? axis,
823+
}) {
824+
// One dimensional viewport has only one axis, it should match if it has
825+
// been provided.
826+
axis ??= this.axis;
827+
assert(axis == this.axis);
828+
757829
// Steps to convert `rect` (from a RenderBox coordinate system) to its
758830
// scroll offset within this viewport (not in the exact order):
759831
//
@@ -1164,52 +1236,19 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
11641236
final RevealedOffset leadingEdgeOffset = viewport.getOffsetToReveal(descendant, 0.0, rect: rect);
11651237
final RevealedOffset trailingEdgeOffset = viewport.getOffsetToReveal(descendant, 1.0, rect: rect);
11661238
final double currentOffset = offset.pixels;
1167-
1168-
// scrollOffset
1169-
// 0 +---------+
1170-
// | |
1171-
// _ | |
1172-
// viewport position | | |
1173-
// with `descendant` at | | | _
1174-
// trailing edge |_ | xxxxxxx | | viewport position
1175-
// | | | with `descendant` at
1176-
// | | _| leading edge
1177-
// | |
1178-
// 800 +---------+
1179-
//
1180-
// `trailingEdgeOffset`: Distance from scrollOffset 0 to the start of the
1181-
// viewport on the left in image above.
1182-
// `leadingEdgeOffset`: Distance from scrollOffset 0 to the start of the
1183-
// viewport on the right in image above.
1184-
//
1185-
// The viewport position on the left is achieved by setting `offset.pixels`
1186-
// to `trailingEdgeOffset`, the one on the right by setting it to
1187-
// `leadingEdgeOffset`.
1188-
1189-
final RevealedOffset targetOffset;
1190-
if (leadingEdgeOffset.offset < trailingEdgeOffset.offset) {
1191-
// `descendant` is too big to be visible on screen in its entirety. Let's
1192-
// align it with the edge that requires the least amount of scrolling.
1193-
final double leadingEdgeDiff = (offset.pixels - leadingEdgeOffset.offset).abs();
1194-
final double trailingEdgeDiff = (offset.pixels - trailingEdgeOffset.offset).abs();
1195-
targetOffset = leadingEdgeDiff < trailingEdgeDiff ? leadingEdgeOffset : trailingEdgeOffset;
1196-
} else if (currentOffset > leadingEdgeOffset.offset) {
1197-
// `descendant` currently starts above the leading edge and can be shown
1198-
// fully on screen by scrolling down (which means: moving viewport up).
1199-
targetOffset = leadingEdgeOffset;
1200-
} else if (currentOffset < trailingEdgeOffset.offset) {
1201-
// `descendant currently ends below the trailing edge and can be shown
1202-
// fully on screen by scrolling up (which means: moving viewport down)
1203-
targetOffset = trailingEdgeOffset;
1204-
} else {
1239+
final RevealedOffset? targetOffset = RevealedOffset.clampOffset(
1240+
leadingEdgeOffset: leadingEdgeOffset,
1241+
trailingEdgeOffset: trailingEdgeOffset,
1242+
currentOffset: currentOffset,
1243+
);
1244+
if (targetOffset == null) {
12051245
// `descendant` is between leading and trailing edge and hence already
12061246
// fully shown on screen. No action necessary.
12071247
assert(viewport.parent != null);
12081248
final Matrix4 transform = descendant.getTransformTo(viewport.parent);
12091249
return MatrixUtils.transformRect(transform, rect ?? descendant.paintBounds);
12101250
}
12111251

1212-
12131252
offset.moveTo(targetOffset.offset, duration: duration, curve: curve);
12141253
return targetOffset.rect;
12151254
}

packages/flutter/lib/src/widgets/scroll_position.dart

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -810,14 +810,32 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
810810
double target;
811811
switch (_applyAxisDirectionToAlignmentPolicy(alignmentPolicy)) {
812812
case ScrollPositionAlignmentPolicy.explicit:
813-
target = clampDouble(viewport.getOffsetToReveal(object, alignment, rect: targetRect).offset, minScrollExtent, maxScrollExtent);
813+
target = viewport.getOffsetToReveal(
814+
object,
815+
alignment,
816+
rect: targetRect,
817+
axis: axis,
818+
).offset;
819+
target = clampDouble(target, minScrollExtent, maxScrollExtent);
814820
case ScrollPositionAlignmentPolicy.keepVisibleAtEnd:
815-
target = clampDouble(viewport.getOffsetToReveal(object, 1.0, rect: targetRect).offset, minScrollExtent, maxScrollExtent);
821+
target = viewport.getOffsetToReveal(
822+
object,
823+
1.0, // Aligns to end
824+
rect: targetRect,
825+
axis: axis,
826+
).offset;
827+
target = clampDouble(target, minScrollExtent, maxScrollExtent);
816828
if (target < pixels) {
817829
target = pixels;
818830
}
819831
case ScrollPositionAlignmentPolicy.keepVisibleAtStart:
820-
target = clampDouble(viewport.getOffsetToReveal(object, 0.0, rect: targetRect).offset, minScrollExtent, maxScrollExtent);
832+
target = viewport.getOffsetToReveal(
833+
object,
834+
0.0, // Aligns to start
835+
rect: targetRect,
836+
axis: axis,
837+
).offset;
838+
target = clampDouble(target, minScrollExtent, maxScrollExtent);
821839
if (target > pixels) {
822840
target = pixels;
823841
}

packages/flutter/lib/src/widgets/scrollable.dart

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ typedef ViewportBuilder = Widget Function(BuildContext context, ViewportOffset p
4444
/// which the scrollable content is displayed.
4545
typedef TwoDimensionalViewportBuilder = Widget Function(BuildContext context, ViewportOffset verticalPosition, ViewportOffset horizontalPosition);
4646

47+
// The return type of _performEnsureVisible.
48+
//
49+
// The list of futures represents each pending ScrollPosition call to
50+
// ensureVisible. The returned ScrollableState's context is used to find the
51+
// next potential ancestor Scrollable.
52+
typedef _EnsureVisibleResults = (List<Future<void>>, ScrollableState);
53+
4754
/// A widget that manages scrolling in one dimension and informs the [Viewport]
4855
/// through which the content is viewed.
4956
///
@@ -441,6 +448,10 @@ class Scrollable extends StatefulWidget {
441448

442449
/// Scrolls the scrollables that enclose the given context so as to make the
443450
/// given context visible.
451+
///
452+
/// If the [Scrollable] of the provided [BuildContext] is a
453+
/// [TwoDimensionalScrollable], both vertical and horizontal axes will ensure
454+
/// the target is made visible.
444455
static Future<void> ensureVisible(
445456
BuildContext context, {
446457
double alignment = 0.0,
@@ -459,14 +470,16 @@ class Scrollable extends StatefulWidget {
459470
RenderObject? targetRenderObject;
460471
ScrollableState? scrollable = Scrollable.maybeOf(context);
461472
while (scrollable != null) {
462-
futures.add(scrollable.position.ensureVisible(
473+
final List<Future<void>> newFutures;
474+
(newFutures, scrollable) = scrollable._performEnsureVisible(
463475
context.findRenderObject()!,
464476
alignment: alignment,
465477
duration: duration,
466478
curve: curve,
467479
alignmentPolicy: alignmentPolicy,
468480
targetRenderObject: targetRenderObject,
469-
));
481+
);
482+
futures.addAll(newFutures);
470483

471484
targetRenderObject = targetRenderObject ?? context.findRenderObject();
472485
context = scrollable.context;
@@ -1011,6 +1024,28 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
10111024
return result;
10121025
}
10131026

1027+
// Returns the Future from calling ensureVisible for the ScrollPosition, as
1028+
// as well as this ScrollableState instance so its context can be used to
1029+
// check for other ancestor Scrollables in executing ensureVisible.
1030+
_EnsureVisibleResults _performEnsureVisible(
1031+
RenderObject object, {
1032+
double alignment = 0.0,
1033+
Duration duration = Duration.zero,
1034+
Curve curve = Curves.ease,
1035+
ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit,
1036+
RenderObject? targetRenderObject,
1037+
}) {
1038+
final Future<void> ensureVisibleFuture = position.ensureVisible(
1039+
object,
1040+
alignment: alignment,
1041+
duration: duration,
1042+
curve: curve,
1043+
alignmentPolicy: alignmentPolicy,
1044+
targetRenderObject: targetRenderObject,
1045+
);
1046+
return (<Future<void>>[ ensureVisibleFuture ], this);
1047+
}
1048+
10141049
@override
10151050
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
10161051
super.debugFillProperties(properties);
@@ -2040,6 +2075,25 @@ class _VerticalOuterDimension extends Scrollable {
20402075
class _VerticalOuterDimensionState extends ScrollableState {
20412076
DiagonalDragBehavior get diagonalDragBehavior => (widget as _VerticalOuterDimension).diagonalDragBehavior;
20422077

2078+
// Implemented in the _HorizontalInnerDimension instead.
2079+
@override
2080+
_EnsureVisibleResults _performEnsureVisible(
2081+
RenderObject object, {
2082+
double alignment = 0.0,
2083+
Duration duration = Duration.zero,
2084+
Curve curve = Curves.ease,
2085+
ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit,
2086+
RenderObject? targetRenderObject,
2087+
}) {
2088+
assert(
2089+
false,
2090+
'The _performEnsureVisible method was called for the vertical scrollable '
2091+
'of a TwoDimensionalScrollable. This should not happen as the horizontal '
2092+
'scrollable handles both axes.'
2093+
);
2094+
return (<Future<void>>[], this);
2095+
}
2096+
20432097
@override
20442098
void setCanDrag(bool value) {
20452099
switch (diagonalDragBehavior) {
@@ -2119,6 +2173,39 @@ class _HorizontalInnerDimensionState extends ScrollableState {
21192173
super.didChangeDependencies();
21202174
}
21212175

2176+
// Returns the Future from calling ensureVisible for the ScrollPosition, as
2177+
// as well as the vertical ScrollableState instance so its context can be
2178+
// used to check for other ancestor Scrollables in executing ensureVisible.
2179+
@override
2180+
_EnsureVisibleResults _performEnsureVisible(
2181+
RenderObject object, {
2182+
double alignment = 0.0,
2183+
Duration duration = Duration.zero,
2184+
Curve curve = Curves.ease,
2185+
ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit,
2186+
RenderObject? targetRenderObject,
2187+
}) {
2188+
final List<Future<void>> newFutures = <Future<void>>[];
2189+
2190+
newFutures.add(position.ensureVisible(
2191+
object,
2192+
alignment: alignment,
2193+
duration: duration,
2194+
curve: curve,
2195+
alignmentPolicy: alignmentPolicy,
2196+
));
2197+
2198+
newFutures.add(verticalScrollable.position.ensureVisible(
2199+
object,
2200+
alignment: alignment,
2201+
duration: duration,
2202+
curve: curve,
2203+
alignmentPolicy: alignmentPolicy,
2204+
));
2205+
2206+
return (newFutures, verticalScrollable);
2207+
}
2208+
21222209
void _evaluateLockedAxis(Offset offset) {
21232210
assert(lastDragOffset != null);
21242211
final Offset offsetDelta = lastDragOffset! - offset;

packages/flutter/lib/src/widgets/single_child_scroll_view.dart

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,17 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
592592
}
593593

594594
@override
595-
RevealedOffset getOffsetToReveal(RenderObject target, double alignment, { Rect? rect }) {
595+
RevealedOffset getOffsetToReveal(
596+
RenderObject target,
597+
double alignment, {
598+
Rect? rect,
599+
Axis? axis,
600+
}) {
601+
// One dimensional viewport has only one axis, it should match if it has
602+
// been provided.
603+
axis ??= this.axis;
604+
assert(axis == this.axis);
605+
596606
rect ??= target.paintBounds;
597607
if (target is! RenderBox) {
598608
return RevealedOffset(offset: offset.pixels, rect: rect);

0 commit comments

Comments
 (0)