Unverified Commit fb5b1570 authored by Gary Qian's avatar Gary Qian Committed by GitHub

Clamp scrollOffset to prevent textfield bouncing (#38573)

parent 1c0c3896
......@@ -1017,6 +1017,14 @@ class RenderEditable extends RenderBox {
return enableInteractiveSelection ?? !obscureText;
}
/// The maximum amount the text is allowed to scroll.
///
/// This value is only valid after layout and can change as additional
/// text is entered or removed in order to accommodate expanding when
/// [expands] is set to true.
double get maxScrollExtent => _maxScrollExtent;
double _maxScrollExtent = 0;
double get _caretMargin => _kCaretGap + cursorWidth;
@override
......@@ -1229,8 +1237,6 @@ class RenderEditable extends RenderBox {
return null;
}
double _maxScrollExtent = 0;
// We need to check the paint offset here because during animation, the start of
// the text may position outside the visible region even when the text fits.
bool get _hasVisualOverflow => _maxScrollExtent > 0 || _paintOffset != Offset.zero;
......
......@@ -1286,6 +1286,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
caretStart = caretRect.top - caretOffset;
caretEnd = caretRect.bottom + caretOffset;
} else {
// Scrolls horizontally for single-line fields.
caretStart = caretRect.left;
caretEnd = caretRect.right;
}
......@@ -1297,6 +1298,13 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
} else if (caretEnd >= viewportExtent) { // cursor after end of bounds
scrollOffset += caretEnd - viewportExtent;
}
if (_isMultiline) {
// Clamp the final results to prevent programmatically scrolling to
// out-of-paragraph-bounds positions when encountering tall fonts/scripts that
// extend past the ascent.
scrollOffset = scrollOffset.clamp(0.0, renderEditable.maxScrollExtent);
}
return scrollOffset;
}
......
......@@ -544,4 +544,47 @@ void main() {
editable.hasFocus = false;
expect(editable.hasFocus, false);
});
test('has correct maxScrollExtent', () {
final TextSelectionDelegate delegate = FakeEditableTextState();
EditableText.debugDeterministicCursor = true;
final RenderEditable editable = RenderEditable(
maxLines: 2,
backgroundCursorColor: Colors.grey,
textDirection: TextDirection.ltr,
cursorColor: const Color.fromARGB(0xFF, 0xFF, 0x00, 0x00),
offset: ViewportOffset.zero(),
textSelectionDelegate: delegate,
text: const TextSpan(
text: '撒地方加咖啡哈金凤凰卡号方式剪坏算法发挥福建垃\nasfjafjajfjaslfjaskjflasjfksajf撒分开建安路口附近拉设\n计费可使肌肤撒附近埃里克圾房卡设计费"',
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Roboto',
),
),
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
selection: const TextSelection.collapsed(
offset: 4,
affinity: TextAffinity.upstream,
),
);
editable.layout(BoxConstraints.loose(const Size(100.0, 1000.0)));
expect(editable.size, equals(const Size(100, 20)));
expect(editable.maxLines, equals(2));
expect(editable.maxScrollExtent, equals(90));
editable.layout(BoxConstraints.loose(const Size(150.0, 1000.0)));
expect(editable.maxScrollExtent, equals(50));
editable.layout(BoxConstraints.loose(const Size(200.0, 1000.0)));
expect(editable.maxScrollExtent, equals(40));
editable.layout(BoxConstraints.loose(const Size(500.0, 1000.0)));
expect(editable.maxScrollExtent, equals(10));
editable.layout(BoxConstraints.loose(const Size(1000.0, 1000.0)));
expect(editable.maxScrollExtent, equals(10));
});
}
......@@ -2568,6 +2568,56 @@ void main() {
debugDefaultTargetPlatformOverride = null;
}, skip: isBrowser);
testWidgets('scrolling doesn\'t bounce', (WidgetTester tester) async {
// 3 lines of text, where the last line overflows and requires scrolling.
const String testText = 'XXXXX\nXXXXX\nXXXXX';
final TextEditingController controller = TextEditingController(text: testText);
await tester.pumpWidget(MaterialApp(
home: Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 100,
child: EditableText(
showSelectionHandles: true,
maxLines: 2,
controller: controller,
focusNode: FocusNode(),
style: Typography(platform: TargetPlatform.android).black.subhead.copyWith(fontFamily: 'Roboto'),
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
),
),
),
));
final EditableTextState state =
tester.state<EditableTextState>(find.byType(EditableText));
final RenderEditable renderEditable = state.renderEditable;
final Scrollable scrollable = tester.widget<Scrollable>(find.byType(Scrollable));
expect(scrollable.controller.position.viewportDimension, equals(28));
expect(scrollable.controller.position.pixels, equals(0));
expect(renderEditable.maxScrollExtent, equals(14));
scrollable.controller.jumpTo(20.0);
await tester.pump();
expect(scrollable.controller.position.pixels, equals(20));
state.bringIntoView(const TextPosition(offset: 0));
await tester.pump();
expect(scrollable.controller.position.pixels, equals(0));
state.bringIntoView(const TextPosition(offset: 13));
await tester.pump();
expect(scrollable.controller.position.pixels, equals(14));
expect(scrollable.controller.position.pixels, equals(renderEditable.maxScrollExtent));
}, skip: isBrowser);
}
class MockTextSelectionControls extends Mock implements TextSelectionControls {
......
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