Unverified Commit 73b63989 authored by xubaolin's avatar xubaolin Committed by GitHub

Improve the behavior of Scrollable.ensureVisible when Scrollable nested (#65226)

parent 062e469f
......@@ -647,6 +647,12 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
/// Animates the position such that the given object is as visible as possible
/// by just scrolling this position.
///
/// The optional `targetRenderObject` parameter is used to determine which area
/// of that object should be as visible as possible. If `targetRenderObject` is null,
/// the entire [RenderObject] (as defined by its [RenderObject.paintBounds])
/// will be as visible as possible. If `targetRenderObject` is provided it should be
/// a descendant of the object.
///
/// See also:
///
/// * [ScrollPositionAlignmentPolicy] for the way in which `alignment` is
......@@ -657,25 +663,34 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
Duration duration = Duration.zero,
Curve curve = Curves.ease,
ScrollPositionAlignmentPolicy alignmentPolicy = ScrollPositionAlignmentPolicy.explicit,
RenderObject? targetRenderObject,
}) {
assert(alignmentPolicy != null);
assert(object.attached);
final RenderAbstractViewport viewport = RenderAbstractViewport.of(object)!;
assert(viewport != null);
Rect? targetRect;
if (targetRenderObject != null) {
targetRect = MatrixUtils.transformRect(
targetRenderObject.getTransformTo(object),
object.paintBounds.intersect(targetRenderObject.paintBounds)
);
}
double target;
switch (alignmentPolicy) {
case ScrollPositionAlignmentPolicy.explicit:
target = viewport.getOffsetToReveal(object, alignment).offset.clamp(minScrollExtent, maxScrollExtent);
target = viewport.getOffsetToReveal(object, alignment, rect: targetRect).offset.clamp(minScrollExtent, maxScrollExtent);
break;
case ScrollPositionAlignmentPolicy.keepVisibleAtEnd:
target = viewport.getOffsetToReveal(object, 1.0).offset.clamp(minScrollExtent, maxScrollExtent);
target = viewport.getOffsetToReveal(object, 1.0, rect: targetRect).offset.clamp(minScrollExtent, maxScrollExtent);
if (target < pixels) {
target = pixels;
}
break;
case ScrollPositionAlignmentPolicy.keepVisibleAtStart:
target = viewport.getOffsetToReveal(object, 0.0).offset.clamp(minScrollExtent, maxScrollExtent);
target = viewport.getOffsetToReveal(object, 0.0, rect: targetRect).offset.clamp(minScrollExtent, maxScrollExtent);
if (target > pixels) {
target = pixels;
}
......
......@@ -307,6 +307,13 @@ class Scrollable extends StatefulWidget {
}) {
final List<Future<void>> futures = <Future<void>>[];
// The `targetRenderObject` is used to record the first target renderObject.
// If there are multiple scrollable widgets nested, we should let
// the `targetRenderObject` as visible as possible to improve the user experience.
// Otherwise, let the outer renderObject as visible as possible maybe cause
// the `targetRenderObject` invisible.
// Also see https://github.com/flutter/flutter/issues/65100
RenderObject? targetRenderObject;
ScrollableState? scrollable = Scrollable.of(context);
while (scrollable != null) {
futures.add(scrollable.position.ensureVisible(
......@@ -315,7 +322,10 @@ class Scrollable extends StatefulWidget {
duration: duration,
curve: curve,
alignmentPolicy: alignmentPolicy,
targetRenderObject: targetRenderObject,
));
targetRenderObject = targetRenderObject ?? context.findRenderObject();
context = scrollable.context;
scrollable = Scrollable.of(context);
}
......
......@@ -224,6 +224,83 @@ void main() {
await tester.pump();
expect(tester.getTopLeft(findKey(0)).dy, moreOrLessEquals(500.0, epsilon: 0.1));
});
testWidgets('Nested SingleChildScrollView ensureVisible behavior test', (WidgetTester tester) async {
// Regressing test for https://github.com/flutter/flutter/issues/65100
Finder findKey(String coordinate) => find.byKey(ValueKey<String>(coordinate));
BuildContext findContext(String coordinate) => tester.element(findKey(coordinate));
final List<Row> rows = List<Row>.generate(
7,
(int y) => Row(
children: List<Container>.generate(
7,
(int x) => Container(key: ValueKey<String>('$x, $y'), width: 200.0, height: 200.0,),
),
),
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
width: 600.0,
height: 400.0,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: Column(
children: rows,
),
),
),
),
),
),
);
// Items: 7 * 7 Container(width: 200.0, height: 200.0)
// viewport: Size(width: 600.0, height: 400.0)
//
// 0 600
// +----------------------+
// |0,0 |1,0 |2,0 |
// | | | |
// +----------------------+
// |0,1 |1,1 |2,1 |
// | | | |
// 400 +----------------------+
Scrollable.ensureVisible(findContext('0, 0'));
await tester.pump();
expect(tester.getTopLeft(findKey('0, 0')), const Offset(100.0, 100.0));
Scrollable.ensureVisible(findContext('3, 0'));
await tester.pump();
expect(tester.getTopLeft(findKey('3, 0')), const Offset(100.0, 100.0));
Scrollable.ensureVisible(findContext('3, 0'), alignment: 0.5);
await tester.pump();
expect(tester.getTopLeft(findKey('3, 0')), const Offset(300.0, 100.0));
Scrollable.ensureVisible(findContext('6, 0'));
await tester.pump();
expect(tester.getTopLeft(findKey('6, 0')), const Offset(500.0, 100.0));
Scrollable.ensureVisible(findContext('0, 2'));
await tester.pump();
expect(tester.getTopLeft(findKey('0, 2')), const Offset(100.0, 100.0));
Scrollable.ensureVisible(findContext('3, 2'));
await tester.pump();
expect(tester.getTopLeft(findKey('3, 2')), const Offset(100.0, 100.0));
// It should be at the center of the screen.
Scrollable.ensureVisible(findContext('3, 2'), alignment: 0.5);
await tester.pump();
expect(tester.getTopLeft(findKey('3, 2')), const Offset(300.0, 200.0));
});
});
group('ListView', () {
......
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