Scroll text fields when the caret moves outside the viewport (#10323)
Fixes https://github.com/flutter/flutter/issues/9923
This commit is contained in:
parent
0809a4fcdc
commit
2ab60e93ef
@ -25,6 +25,11 @@ final String _kZeroWidthSpace = new String.fromCharCode(0x200B);
|
||||
/// Used by [RenderEditable.onSelectionChanged].
|
||||
typedef void SelectionChangedHandler(TextSelection selection, RenderEditable renderObject, bool longPress);
|
||||
|
||||
/// Signature for the callback that reports when the caret location changes.
|
||||
///
|
||||
/// Used by [RenderEditable.onCaretChanged].
|
||||
typedef void CaretChangedHandler(Rect caretRect);
|
||||
|
||||
/// Represents a global screen coordinate of the point in a selection, and the
|
||||
/// text direction at that point.
|
||||
@immutable
|
||||
@ -65,6 +70,7 @@ class RenderEditable extends RenderBox {
|
||||
TextSelection selection,
|
||||
@required ViewportOffset offset,
|
||||
this.onSelectionChanged,
|
||||
this.onCaretChanged,
|
||||
}) : _textPainter = new TextPainter(text: text, textAlign: textAlign, textScaleFactor: textScaleFactor),
|
||||
_cursorColor = cursorColor,
|
||||
_showCursor = showCursor ?? new ValueNotifier<bool>(false),
|
||||
@ -89,6 +95,11 @@ class RenderEditable extends RenderBox {
|
||||
|
||||
double _textLayoutLastWidth;
|
||||
|
||||
/// Called during the paint phase when the caret location changes.
|
||||
CaretChangedHandler onCaretChanged;
|
||||
|
||||
Rect _lastCaretRect;
|
||||
|
||||
/// Marks the render object as needing to be laid out again and have its text
|
||||
/// metrics recomputed.
|
||||
///
|
||||
@ -422,7 +433,13 @@ class RenderEditable extends RenderBox {
|
||||
assert(_textLayoutLastWidth == constraints.maxWidth);
|
||||
final Offset caretOffset = _textPainter.getOffsetForCaret(_selection.extent, _caretPrototype);
|
||||
final Paint paint = new Paint()..color = _cursorColor;
|
||||
canvas.drawRect(_caretPrototype.shift(caretOffset + effectiveOffset), paint);
|
||||
final Rect caretRect = _caretPrototype.shift(caretOffset + effectiveOffset);
|
||||
canvas.drawRect(caretRect, paint);
|
||||
if (caretRect != _lastCaretRect) {
|
||||
_lastCaretRect = caretRect;
|
||||
if (onCaretChanged != null)
|
||||
onCaretChanged(caretRect);
|
||||
}
|
||||
}
|
||||
|
||||
void _paintSelection(Canvas canvas, Offset effectiveOffset) {
|
||||
|
@ -445,6 +445,21 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
|
||||
_scrollController.jumpTo(_getScrollOffsetForCaret(caretRect));
|
||||
}
|
||||
|
||||
bool _textChangedSinceLastCaretUpdate = false;
|
||||
|
||||
void _handleCaretChanged(Rect caretRect) {
|
||||
// If the caret location has changed due to an update to the text or
|
||||
// selection, then scroll the caret into view.
|
||||
if (_textChangedSinceLastCaretUpdate) {
|
||||
_textChangedSinceLastCaretUpdate = false;
|
||||
_scrollController.animateTo(
|
||||
_getScrollOffsetForCaret(caretRect),
|
||||
curve: Curves.fastOutSlowIn,
|
||||
duration: const Duration(milliseconds: 50),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _formatAndSetValue(TextEditingValue value) {
|
||||
if (widget.inputFormatters != null && widget.inputFormatters.isNotEmpty) {
|
||||
for (TextInputFormatter formatter in widget.inputFormatters)
|
||||
@ -493,6 +508,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
|
||||
_updateRemoteEditingValueIfNeeded();
|
||||
_startOrStopCursorTimerIfNeeded();
|
||||
_updateOrDisposeSelectionOverlayIfNeeded();
|
||||
_textChangedSinceLastCaretUpdate = true;
|
||||
// TODO(abarth): Teach RenderEditable about ValueNotifier<TextEditingValue>
|
||||
// to avoid this setState().
|
||||
setState(() { /* We use widget.controller.value in build(). */ });
|
||||
@ -524,6 +540,7 @@ class EditableTextState extends State<EditableText> implements TextInputClient {
|
||||
obscureText: widget.obscureText,
|
||||
offset: offset,
|
||||
onSelectionChanged: _handleSelectionChanged,
|
||||
onCaretChanged: _handleCaretChanged,
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -544,6 +561,7 @@ class _Editable extends LeafRenderObjectWidget {
|
||||
this.obscureText,
|
||||
this.offset,
|
||||
this.onSelectionChanged,
|
||||
this.onCaretChanged,
|
||||
}) : super(key: key);
|
||||
|
||||
final TextEditingValue value;
|
||||
@ -557,6 +575,7 @@ class _Editable extends LeafRenderObjectWidget {
|
||||
final bool obscureText;
|
||||
final ViewportOffset offset;
|
||||
final SelectionChangedHandler onSelectionChanged;
|
||||
final CaretChangedHandler onCaretChanged;
|
||||
|
||||
@override
|
||||
RenderEditable createRenderObject(BuildContext context) {
|
||||
@ -571,6 +590,7 @@ class _Editable extends LeafRenderObjectWidget {
|
||||
selection: value.selection,
|
||||
offset: offset,
|
||||
onSelectionChanged: onSelectionChanged,
|
||||
onCaretChanged: onCaretChanged,
|
||||
);
|
||||
}
|
||||
|
||||
@ -586,7 +606,8 @@ class _Editable extends LeafRenderObjectWidget {
|
||||
..textAlign = textAlign
|
||||
..selection = value.selection
|
||||
..offset = offset
|
||||
..onSelectionChanged = onSelectionChanged;
|
||||
..onSelectionChanged = onSelectionChanged
|
||||
..onCaretChanged = onCaretChanged;
|
||||
}
|
||||
|
||||
TextSpan get _styledTextSpan {
|
||||
|
@ -112,6 +112,8 @@ void main() {
|
||||
expect(textFieldValue, equals(testValue));
|
||||
|
||||
await tester.pumpWidget(builder());
|
||||
// skip past scrolling animation
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
});
|
||||
}
|
||||
|
||||
@ -217,6 +219,8 @@ void main() {
|
||||
await tester.enterText(find.byType(TextField), testValue);
|
||||
|
||||
await tester.pumpWidget(builder());
|
||||
// skip past scrolling animation
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
|
||||
// Tap to reposition the caret.
|
||||
final int tapIndex = testValue.indexOf('e');
|
||||
@ -259,6 +263,8 @@ void main() {
|
||||
expect(controller.value.text, testValue);
|
||||
|
||||
await tester.pumpWidget(builder());
|
||||
// skip past scrolling animation
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
|
||||
expect(controller.selection.isCollapsed, true);
|
||||
|
||||
@ -293,6 +299,8 @@ void main() {
|
||||
await tester.enterText(find.byType(TextField), testValue);
|
||||
|
||||
await tester.pumpWidget(builder());
|
||||
// skip past scrolling animation
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
|
||||
// Long press the 'e' to select 'def'.
|
||||
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
|
||||
@ -354,6 +362,8 @@ void main() {
|
||||
final String testValue = 'abc def ghi';
|
||||
await tester.enterText(find.byType(TextField), testValue);
|
||||
await tester.pumpWidget(builder());
|
||||
// skip past scrolling animation
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
|
||||
// Tap the selection handle to bring up the "paste / select all" menu.
|
||||
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
|
||||
@ -372,6 +382,8 @@ void main() {
|
||||
// COPY should reset the selection.
|
||||
await tester.tap(find.text('COPY'));
|
||||
await tester.pumpWidget(builder());
|
||||
// skip past scrolling animation
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
expect(controller.selection.isCollapsed, true);
|
||||
|
||||
// Tap again to bring back the menu.
|
||||
@ -406,6 +418,8 @@ void main() {
|
||||
final String testValue = 'abc def ghi';
|
||||
await tester.enterText(find.byType(TextField), testValue);
|
||||
await tester.pumpWidget(builder());
|
||||
// skip past scrolling animation
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
|
||||
// Tap the selection handle to bring up the "paste / select all" menu.
|
||||
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
|
||||
@ -502,6 +516,8 @@ void main() {
|
||||
await tester.enterText(find.byType(TextField), testValue);
|
||||
|
||||
await tester.pumpWidget(builder());
|
||||
// skip past scrolling animation
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
|
||||
// Check that the text spans multiple lines.
|
||||
final Offset firstPos = textOffsetToPosition(tester, testValue.indexOf('First'));
|
||||
@ -1066,6 +1082,8 @@ void main() {
|
||||
await tester.enterText(find.byType(TextField), 'a1b\n2c3');
|
||||
expect(textController.text, '123');
|
||||
await tester.pumpWidget(builder());
|
||||
// skip past scrolling animation
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
|
||||
await tester.tapAt(textOffsetToPosition(tester, '123'.indexOf('2')));
|
||||
await tester.pumpWidget(builder());
|
||||
@ -1082,4 +1100,45 @@ void main() {
|
||||
expect(textController.text, '145623');
|
||||
}
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'Text field scrolls the caret into view',
|
||||
(WidgetTester tester) async {
|
||||
final TextEditingController controller = new TextEditingController();
|
||||
|
||||
Widget builder() {
|
||||
return overlay(new Center(
|
||||
child: new Material(
|
||||
child: new Container(
|
||||
width: 100.0,
|
||||
child: new TextField(
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
await tester.pumpWidget(builder());
|
||||
|
||||
final String longText = 'a' * 20;
|
||||
await tester.enterText(find.byType(TextField), longText);
|
||||
await tester.pumpWidget(builder());
|
||||
// skip past scrolling animation
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
|
||||
ScrollableState scrollableState = tester.firstState(find.byType(Scrollable));
|
||||
expect(scrollableState.position.pixels, equals(0.0));
|
||||
|
||||
// Move the caret to the end of the text and check that the text field
|
||||
// scrolls to make the caret visible.
|
||||
controller.selection = new TextSelection.collapsed(offset: longText.length);
|
||||
await tester.pumpWidget(builder());
|
||||
// skip past scrolling animation
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
|
||||
scrollableState = tester.firstState(find.byType(Scrollable));
|
||||
expect(scrollableState.position.pixels, isNot(equals(0.0)));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user