Unverified Commit 4bb62b43 authored by Hans Muller's avatar Hans Muller Committed by GitHub

Stop TextFields from wobbling within scrollables (#24015)

parent 57c2fac1
......@@ -984,11 +984,7 @@ abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMix
assert(targetOffset != null);
if (duration == Duration.zero) {
offset.jumpTo(targetOffset.offset);
} else {
offset.animateTo(targetOffset.offset, duration: duration, curve: curve);
}
offset.moveTo(targetOffset.offset, duration: duration, curve: curve);
return targetOffset.rect;
}
}
......
......@@ -181,6 +181,27 @@ abstract class ViewportOffset extends ChangeNotifier {
@required Curve curve,
});
/// Calls [jumpTo] if duration is null or [Duration.zero], otherwise
/// [animateTo] is called.
///
/// If [animateTo] is called then [curve] defaults to [Curves.ease]. The
/// [clamp] parameter is ignored by this stub implementation but subclasses
/// like [ScrollPosition] handle it by adjusting [to] to prevent over or
/// underscroll.
Future<void> moveTo(double to, {
Duration duration,
Curve curve,
bool clamp,
}) {
assert(to != null);
if (duration == null || duration == Duration.zero) {
jumpTo(to);
return Future<void>.value();
} else {
return animateTo(to, duration: duration, curve: curve ?? Curves.ease);
}
}
/// The direction in which the user is trying to change [pixels], relative to
/// the viewport's [RenderViewport.axisDirection].
///
......
......@@ -562,6 +562,28 @@ abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
@override
void jumpTo(double value);
/// Calls [jumpTo] if duration is null or [Duration.zero], otherwise
/// [animateTo] is called.
///
/// If [clamp] is true (the default) then [to] is adjusted to prevent over or
/// underscroll.
///
/// If [animateTo] is called then [curve] defaults to [Curves.ease].
@override
Future<void> moveTo(double to, {
Duration duration,
Curve curve,
bool clamp = true,
}) {
assert(to != null);
assert(clamp != null);
if (clamp)
to = to.clamp(minScrollExtent, maxScrollExtent);
return super.moveTo(to, duration: duration, curve: curve);
}
@override
bool get allowImplicitScrolling => physics.allowImplicitScrolling;
......
......@@ -4,6 +4,7 @@
import 'dart:async';
import 'dart:io' show Platform;
import 'dart:math' as math;
import 'dart:ui' as ui show window;
import 'package:flutter/material.dart';
......@@ -3182,4 +3183,148 @@ void main() {
final Rect fieldRect = tester.getRect(find.text('Just some text'));
expect(labelRect.bottom, lessThanOrEqualTo(fieldRect.top));
});
testWidgets('TextField scrolls into view but does not bounce (SingleChildScrollView)', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/20485
final Key textField1 = UniqueKey();
final Key textField2 = UniqueKey();
final ScrollController scrollController = ScrollController();
double minOffset;
double maxOffset;
scrollController.addListener(() {
final double offset = scrollController.offset;
minOffset = math.min(minOffset ?? offset, offset);
maxOffset = math.max(maxOffset ?? offset, offset);
});
Widget buildFrame(Axis scrollDirection) {
return MaterialApp(
home: Scaffold(
body: SafeArea(
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
controller: scrollController,
child: Column(
children: <Widget>[
SizedBox( // visible when scrollOffset is 0.0
height: 100.0,
width: 100.0,
child: TextField(key: textField1, scrollPadding: const EdgeInsets.all(200.0)),
),
const SizedBox(
height: 600.0, // Same size as the frame. Initially
width: 800.0, // textField2 is not visible
),
SizedBox( // visible when scrollOffset is 200.0
height: 100.0,
width: 100.0,
child: TextField(key: textField2, scrollPadding: const EdgeInsets.all(200.0)),
),
],
),
),
),
),
);
}
await tester.pumpWidget(buildFrame(Axis.vertical));
await tester.enterText(find.byKey(textField1), '1');
await tester.pumpAndSettle();
await tester.enterText(find.byKey(textField2), '2'); //scroll textField2 into view
await tester.pumpAndSettle();
await tester.enterText(find.byKey(textField1), '3'); //scroll textField1 back into view
await tester.pumpAndSettle();
expect(minOffset, 0.0);
expect(maxOffset, 200.0);
minOffset = null;
maxOffset = null;
await tester.pumpWidget(buildFrame(Axis.horizontal));
await tester.enterText(find.byKey(textField1), '1');
await tester.pumpAndSettle();
await tester.enterText(find.byKey(textField2), '2'); //scroll textField2 into view
await tester.pumpAndSettle();
await tester.enterText(find.byKey(textField1), '3'); //scroll textField1 back into view
await tester.pumpAndSettle();
expect(minOffset, 0.0);
expect(maxOffset, 200.0);
});
testWidgets('TextField scrolls into view but does not bounce (ListView)', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/20485
final Key textField1 = UniqueKey();
final Key textField2 = UniqueKey();
final ScrollController scrollController = ScrollController();
double minOffset;
double maxOffset;
scrollController.addListener(() {
final double offset = scrollController.offset;
minOffset = math.min(minOffset ?? offset, offset);
maxOffset = math.max(maxOffset ?? offset, offset);
});
Widget buildFrame(Axis scrollDirection) {
return MaterialApp(
home: Scaffold(
body: SafeArea(
child: ListView(
physics: const BouncingScrollPhysics(),
controller: scrollController,
children: <Widget>[
SizedBox( // visible when scrollOffset is 0.0
height: 100.0,
width: 100.0,
child: TextField(key: textField1, scrollPadding: const EdgeInsets.all(200.0)),
),
const SizedBox(
height: 450.0, // 50.0 smaller than the overall frame so that both
width: 650.0, // textfields are always partially visible.
),
SizedBox( // visible when scrollOffset = 50.0
height: 100.0,
width: 100.0,
child: TextField(key: textField2, scrollPadding: const EdgeInsets.all(200.0)),
),
],
),
),
),
);
}
await tester.pumpWidget(buildFrame(Axis.vertical));
await tester.enterText(find.byKey(textField1), '1'); // textfield1 is visible
await tester.pumpAndSettle();
await tester.enterText(find.byKey(textField2), '2'); //scroll textField2 into view
await tester.pumpAndSettle();
await tester.enterText(find.byKey(textField1), '3'); //scroll textField1 back into view
await tester.pumpAndSettle();
expect(minOffset, 0.0);
expect(maxOffset, 50.0);
minOffset = null;
maxOffset = null;
await tester.pumpWidget(buildFrame(Axis.horizontal));
await tester.enterText(find.byKey(textField1), '1'); // textfield1 is visible
await tester.pumpAndSettle();
await tester.enterText(find.byKey(textField2), '2'); //scroll textField2 into view
await tester.pumpAndSettle();
await tester.enterText(find.byKey(textField1), '3'); //scroll textField1 back into view
await tester.pumpAndSettle();
expect(minOffset, 0.0);
expect(maxOffset, 50.0);
});
}
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