Unverified Commit ac4d5099 authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

Reland #74722 (#75604)

parent e07c2483
...@@ -179,6 +179,8 @@ class _CupertinoTextFieldSelectionGestureDetectorBuilder extends TextSelectionGe ...@@ -179,6 +179,8 @@ class _CupertinoTextFieldSelectionGestureDetectorBuilder extends TextSelectionGe
/// Remember to call [TextEditingController.dispose] when it is no longer /// Remember to call [TextEditingController.dispose] when it is no longer
/// needed. This will ensure we discard any resources used by the object. /// needed. This will ensure we discard any resources used by the object.
/// ///
/// {@macro flutter.widgets.editableText.showCaretOnScreen}
///
/// See also: /// See also:
/// ///
/// * <https://developer.apple.com/documentation/uikit/uitextfield> /// * <https://developer.apple.com/documentation/uikit/uitextfield>
......
...@@ -256,7 +256,7 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete ...@@ -256,7 +256,7 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete
/// Keep in mind you can also always read the current string from a TextField's /// Keep in mind you can also always read the current string from a TextField's
/// [TextEditingController] using [TextEditingController.text]. /// [TextEditingController] using [TextEditingController.text].
/// ///
/// ## Handling emojis and other complex characters /// ## Handling emojis and other complex characters
/// {@macro flutter.widgets.EditableText.onChanged} /// {@macro flutter.widgets.EditableText.onChanged}
/// ///
/// In the live Dartpad example above, try typing the emoji 👨‍👩‍👦 /// In the live Dartpad example above, try typing the emoji 👨‍👩‍👦
...@@ -264,6 +264,8 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete ...@@ -264,6 +264,8 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete
/// with `value.characters.length`, the emoji is correctly counted as a single /// with `value.characters.length`, the emoji is correctly counted as a single
/// character. /// character.
/// ///
/// {@macro flutter.widgets.editableText.showCaretOnScreen}
///
/// See also: /// See also:
/// ///
/// * [TextFormField], which integrates with the [Form] widget. /// * [TextFormField], which integrates with the [Form] widget.
......
...@@ -2710,21 +2710,22 @@ class _FloatingCursorPainter extends RenderEditablePainter { ...@@ -2710,21 +2710,22 @@ class _FloatingCursorPainter extends RenderEditablePainter {
caretRect = caretRect.shift(renderEditable._paintOffset); caretRect = caretRect.shift(renderEditable._paintOffset);
final Rect integralRect = caretRect.shift(renderEditable._snapToPhysicalPixel(caretRect.topLeft)); final Rect integralRect = caretRect.shift(renderEditable._snapToPhysicalPixel(caretRect.topLeft));
final Radius? radius = cursorRadius; if (shouldPaint) {
caretPaint.color = caretColor; final Radius? radius = cursorRadius;
if (radius == null) { caretPaint.color = caretColor;
canvas.drawRect(integralRect, caretPaint); if (radius == null) {
} else { canvas.drawRect(integralRect, caretPaint);
final RRect caretRRect = RRect.fromRectAndRadius(integralRect, radius); } else {
canvas.drawRRect(caretRRect, caretPaint); final RRect caretRRect = RRect.fromRectAndRadius(integralRect, radius);
canvas.drawRRect(caretRRect, caretPaint);
}
} }
caretPaintCallback(integralRect); caretPaintCallback(integralRect);
} }
@override @override
void paint(Canvas canvas, Size size, RenderEditable renderEditable) { void paint(Canvas canvas, Size size, RenderEditable renderEditable) {
if (!shouldPaint) // Compute the caret location even when `shouldPaint` is false.
return;
assert(renderEditable != null); assert(renderEditable != null);
final TextSelection? selection = renderEditable.selection; final TextSelection? selection = renderEditable.selection;
...@@ -2749,7 +2750,7 @@ class _FloatingCursorPainter extends RenderEditablePainter { ...@@ -2749,7 +2750,7 @@ class _FloatingCursorPainter extends RenderEditablePainter {
final Color? floatingCursorColor = this.caretColor?.withOpacity(0.75); final Color? floatingCursorColor = this.caretColor?.withOpacity(0.75);
// Floating Cursor. // Floating Cursor.
if (floatingCursorRect == null || floatingCursorColor == null) if (floatingCursorRect == null || floatingCursorColor == null || !shouldPaint)
return; return;
canvas.drawRRect( canvas.drawRRect(
......
...@@ -398,6 +398,18 @@ class ToolbarOptions { ...@@ -398,6 +398,18 @@ class ToolbarOptions {
/// methods such as [RenderEditable.selectPosition], /// methods such as [RenderEditable.selectPosition],
/// [RenderEditable.selectWord], etc. programmatically. /// [RenderEditable.selectWord], etc. programmatically.
/// ///
/// {@template flutter.widgets.editableText.showCaretOnScreen}
/// ## Keep the caret visisble when focused
///
/// When focused, this widget will make attempts to keep the text area and its
/// caret (even when [showCursor] is `false`) visible, on these occasions:
///
/// * When the user focuses this text field and it is not [readOnly].
/// * When the user changes the selection of the text field, or changes the
/// text when the text field is not [readOnly].
/// * When the virtual keyboard pops up.
/// {@endtemplate}
///
/// See also: /// See also:
/// ///
/// * [TextField], which is a full-featured, material-design text input field /// * [TextField], which is a full-featured, material-design text input field
...@@ -1721,7 +1733,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1721,7 +1733,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_currentPromptRectRange = null; _currentPromptRectRange = null;
if (_hasInputConnection) { if (_hasInputConnection) {
_showCaretOnScreen();
if (widget.obscureText && value.text.length == _value.text.length + 1) { if (widget.obscureText && value.text.length == _value.text.length + 1) {
_obscureShowCharTicksPending = _kObscureShowLatestCharCursorTicks; _obscureShowCharTicksPending = _kObscureShowLatestCharCursorTicks;
_obscureLatestCharIndex = _value.selection.baseOffset; _obscureLatestCharIndex = _value.selection.baseOffset;
...@@ -1731,6 +1742,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1731,6 +1742,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_formatAndSetValue(value); _formatAndSetValue(value);
} }
// Wherever the value is changed by the user, schedule a showCaretOnScreen
// to make sure the user can see the changes they just made. Programmatical
// changes to `textEditingValue` do not trigger the behavior even if the
// text field is focused.
_scheduleShowCaretOnScreen();
if (_hasInputConnection) { if (_hasInputConnection) {
// To keep the cursor from blinking while typing, we want to restart the // To keep the cursor from blinking while typing, we want to restart the
// cursor timer every time a new character is typed. // cursor timer every time a new character is typed.
...@@ -2154,17 +2170,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2154,17 +2170,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
} }
} }
bool _textChangedSinceLastCaretUpdate = false;
Rect? _currentCaretRect; Rect? _currentCaretRect;
void _handleCaretChanged(Rect caretRect) { void _handleCaretChanged(Rect caretRect) {
_currentCaretRect = caretRect; _currentCaretRect = 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;
_showCaretOnScreen();
}
} }
// Animation configuration for scrolling the caret back on screen. // Animation configuration for scrolling the caret back on screen.
...@@ -2173,7 +2181,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2173,7 +2181,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
bool _showCaretOnScreenScheduled = false; bool _showCaretOnScreenScheduled = false;
void _showCaretOnScreen() { void _scheduleShowCaretOnScreen() {
if (_showCaretOnScreenScheduled) { if (_showCaretOnScreenScheduled) {
return; return;
} }
...@@ -2232,7 +2240,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2232,7 +2240,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@override @override
void didChangeMetrics() { void didChangeMetrics() {
if (_lastBottomViewInset < WidgetsBinding.instance!.window.viewInsets.bottom) { if (_lastBottomViewInset < WidgetsBinding.instance!.window.viewInsets.bottom) {
_showCaretOnScreen(); _scheduleShowCaretOnScreen();
} }
_lastBottomViewInset = WidgetsBinding.instance!.window.viewInsets.bottom; _lastBottomViewInset = WidgetsBinding.instance!.window.viewInsets.bottom;
} }
...@@ -2390,7 +2398,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2390,7 +2398,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_updateRemoteEditingValueIfNeeded(); _updateRemoteEditingValueIfNeeded();
_startOrStopCursorTimerIfNeeded(); _startOrStopCursorTimerIfNeeded();
_updateOrDisposeSelectionOverlayIfNeeded(); _updateOrDisposeSelectionOverlayIfNeeded();
_textChangedSinceLastCaretUpdate = true;
// TODO(abarth): Teach RenderEditable about ValueNotifier<TextEditingValue> // TODO(abarth): Teach RenderEditable about ValueNotifier<TextEditingValue>
// to avoid this setState(). // to avoid this setState().
setState(() { /* We use widget.controller.value in build(). */ }); setState(() { /* We use widget.controller.value in build(). */ });
...@@ -2404,7 +2411,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2404,7 +2411,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// Listen for changing viewInsets, which indicates keyboard showing up. // Listen for changing viewInsets, which indicates keyboard showing up.
WidgetsBinding.instance!.addObserver(this); WidgetsBinding.instance!.addObserver(this);
_lastBottomViewInset = WidgetsBinding.instance!.window.viewInsets.bottom; _lastBottomViewInset = WidgetsBinding.instance!.window.viewInsets.bottom;
_showCaretOnScreen(); if (!widget.readOnly) {
_scheduleShowCaretOnScreen();
}
if (!_value.selection.isValid) { if (!_value.selection.isValid) {
// Place cursor at the end if the selection is invalid when we receive focus. // Place cursor at the end if the selection is invalid when we receive focus.
_handleSelectionChanged(TextSelection.collapsed(offset: _value.text.length), renderEditable, null); _handleSelectionChanged(TextSelection.collapsed(offset: _value.text.length), renderEditable, null);
...@@ -2471,6 +2480,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2471,6 +2480,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@override @override
set textEditingValue(TextEditingValue value) { set textEditingValue(TextEditingValue value) {
_selectionOverlay?.update(value); _selectionOverlay?.update(value);
// Compare the current TextEditingValue with the pre-format new
// TextEditingValue value, in case the formatter would reject the change.
final bool shouldShowCaret = widget.readOnly
? _value.selection != value.selection
: _value != value;
if (shouldShowCaret) {
_scheduleShowCaretOnScreen();
}
_formatAndSetValue(value); _formatAndSetValue(value);
} }
......
...@@ -3495,7 +3495,12 @@ void main() { ...@@ -3495,7 +3495,12 @@ void main() {
// Move the caret to the end of the text and check that the text field // Move the caret to the end of the text and check that the text field
// scrolls to make the caret visible. // scrolls to make the caret visible.
controller.selection = TextSelection.collapsed(offset: longText.length); scrollableState = tester.firstState(find.byType(Scrollable));
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
editableTextState.textEditingValue = editableTextState.textEditingValue.copyWith(
selection: TextSelection.collapsed(offset: longText.length),
);
await tester.pump(); // TODO(ianh): Figure out why this extra pump is needed. await tester.pump(); // TODO(ianh): Figure out why this extra pump is needed.
await skipPastScrollingAnimation(tester); await skipPastScrollingAnimation(tester);
...@@ -3527,7 +3532,10 @@ void main() { ...@@ -3527,7 +3532,10 @@ void main() {
// Move the caret to the end of the text and check that the text field // Move the caret to the end of the text and check that the text field
// scrolls to make the caret visible. // scrolls to make the caret visible.
controller.selection = const TextSelection.collapsed(offset: tallText.length); final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
editableTextState.textEditingValue = editableTextState.textEditingValue.copyWith(
selection: const TextSelection.collapsed(offset: tallText.length),
);
await tester.pump(); await tester.pump();
await skipPastScrollingAnimation(tester); await skipPastScrollingAnimation(tester);
...@@ -8614,6 +8622,51 @@ void main() { ...@@ -8614,6 +8622,51 @@ void main() {
expect(scrollController.offset, 48.0); expect(scrollController.offset, 48.0);
}); });
// Regression test for https://github.com/flutter/flutter/issues/74566
testWidgets('TextField and last input character are visible on the screen when the cursor is not shown', (WidgetTester tester) async {
final ScrollController scrollController = ScrollController();
final ScrollController textFieldScrollController = ScrollController();
await tester.pumpWidget(MaterialApp(
theme: ThemeData(),
home: Scaffold(
body: Center(
child: ListView(
controller: scrollController,
children: <Widget>[
Container(height: 579), // Push field almost off screen.
TextField(
scrollController: textFieldScrollController,
showCursor: false,
),
Container(height: 1000),
],
),
),
),
));
// Tap the TextField to bring it into view.
expect(scrollController.offset, 0.0);
await tester.tapAt(tester.getTopLeft(find.byType(TextField)));
await tester.pumpAndSettle();
// The ListView has scrolled to keep the TextField visible.
expect(scrollController.offset, 48.0);
expect(textFieldScrollController.offset, 0.0);
// After entering some long text, the last input character remains on the screen.
final String testValue = 'I love Flutter!' * 10;
tester.testTextInput.updateEditingValue(TextEditingValue(
text: testValue,
selection: TextSelection.collapsed(offset: testValue.length),
));
await tester.pump();
await tester.pumpAndSettle(); // Text scroll animation.
expect(textFieldScrollController.offset, 1602.0);
});
group('height', () { group('height', () {
testWidgets('By default, TextField is at least kMinInteractiveDimension high', (WidgetTester tester) async { testWidgets('By default, TextField is at least kMinInteractiveDimension high', (WidgetTester tester) async {
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
......
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