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
/// Remember to call [TextEditingController.dispose] when it is no longer
/// needed. This will ensure we discard any resources used by the object.
///
/// {@macro flutter.widgets.editableText.showCaretOnScreen}
///
/// See also:
///
/// * <https://developer.apple.com/documentation/uikit/uitextfield>
......
......@@ -256,7 +256,7 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete
/// Keep in mind you can also always read the current string from a TextField's
/// [TextEditingController] using [TextEditingController.text].
///
/// ## Handling emojis and other complex characters
/// ## Handling emojis and other complex characters
/// {@macro flutter.widgets.EditableText.onChanged}
///
/// In the live Dartpad example above, try typing the emoji 👨‍👩‍👦
......@@ -264,6 +264,8 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete
/// with `value.characters.length`, the emoji is correctly counted as a single
/// character.
///
/// {@macro flutter.widgets.editableText.showCaretOnScreen}
///
/// See also:
///
/// * [TextFormField], which integrates with the [Form] widget.
......
......@@ -2710,21 +2710,22 @@ class _FloatingCursorPainter extends RenderEditablePainter {
caretRect = caretRect.shift(renderEditable._paintOffset);
final Rect integralRect = caretRect.shift(renderEditable._snapToPhysicalPixel(caretRect.topLeft));
final Radius? radius = cursorRadius;
caretPaint.color = caretColor;
if (radius == null) {
canvas.drawRect(integralRect, caretPaint);
} else {
final RRect caretRRect = RRect.fromRectAndRadius(integralRect, radius);
canvas.drawRRect(caretRRect, caretPaint);
if (shouldPaint) {
final Radius? radius = cursorRadius;
caretPaint.color = caretColor;
if (radius == null) {
canvas.drawRect(integralRect, caretPaint);
} else {
final RRect caretRRect = RRect.fromRectAndRadius(integralRect, radius);
canvas.drawRRect(caretRRect, caretPaint);
}
}
caretPaintCallback(integralRect);
}
@override
void paint(Canvas canvas, Size size, RenderEditable renderEditable) {
if (!shouldPaint)
return;
// Compute the caret location even when `shouldPaint` is false.
assert(renderEditable != null);
final TextSelection? selection = renderEditable.selection;
......@@ -2749,7 +2750,7 @@ class _FloatingCursorPainter extends RenderEditablePainter {
final Color? floatingCursorColor = this.caretColor?.withOpacity(0.75);
// Floating Cursor.
if (floatingCursorRect == null || floatingCursorColor == null)
if (floatingCursorRect == null || floatingCursorColor == null || !shouldPaint)
return;
canvas.drawRRect(
......
......@@ -398,6 +398,18 @@ class ToolbarOptions {
/// methods such as [RenderEditable.selectPosition],
/// [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:
///
/// * [TextField], which is a full-featured, material-design text input field
......@@ -1721,7 +1733,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_currentPromptRectRange = null;
if (_hasInputConnection) {
_showCaretOnScreen();
if (widget.obscureText && value.text.length == _value.text.length + 1) {
_obscureShowCharTicksPending = _kObscureShowLatestCharCursorTicks;
_obscureLatestCharIndex = _value.selection.baseOffset;
......@@ -1731,6 +1742,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_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) {
// To keep the cursor from blinking while typing, we want to restart the
// cursor timer every time a new character is typed.
......@@ -2154,17 +2170,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
}
bool _textChangedSinceLastCaretUpdate = false;
Rect? _currentCaretRect;
void _handleCaretChanged(Rect 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.
......@@ -2173,7 +2181,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
bool _showCaretOnScreenScheduled = false;
void _showCaretOnScreen() {
void _scheduleShowCaretOnScreen() {
if (_showCaretOnScreenScheduled) {
return;
}
......@@ -2232,7 +2240,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@override
void didChangeMetrics() {
if (_lastBottomViewInset < WidgetsBinding.instance!.window.viewInsets.bottom) {
_showCaretOnScreen();
_scheduleShowCaretOnScreen();
}
_lastBottomViewInset = WidgetsBinding.instance!.window.viewInsets.bottom;
}
......@@ -2390,7 +2398,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_updateRemoteEditingValueIfNeeded();
_startOrStopCursorTimerIfNeeded();
_updateOrDisposeSelectionOverlayIfNeeded();
_textChangedSinceLastCaretUpdate = true;
// TODO(abarth): Teach RenderEditable about ValueNotifier<TextEditingValue>
// to avoid this setState().
setState(() { /* We use widget.controller.value in build(). */ });
......@@ -2404,7 +2411,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// Listen for changing viewInsets, which indicates keyboard showing up.
WidgetsBinding.instance!.addObserver(this);
_lastBottomViewInset = WidgetsBinding.instance!.window.viewInsets.bottom;
_showCaretOnScreen();
if (!widget.readOnly) {
_scheduleShowCaretOnScreen();
}
if (!_value.selection.isValid) {
// Place cursor at the end if the selection is invalid when we receive focus.
_handleSelectionChanged(TextSelection.collapsed(offset: _value.text.length), renderEditable, null);
......@@ -2471,6 +2480,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@override
set textEditingValue(TextEditingValue 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);
}
......
......@@ -3495,7 +3495,12 @@ void main() {
// Move the caret to the end of the text and check that the text field
// 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 skipPastScrollingAnimation(tester);
......@@ -3527,7 +3532,10 @@ void main() {
// Move the caret to the end of the text and check that the text field
// 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 skipPastScrollingAnimation(tester);
......@@ -8614,6 +8622,51 @@ void main() {
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', () {
testWidgets('By default, TextField is at least kMinInteractiveDimension high', (WidgetTester tester) async {
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