diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index bce401f38ff..cf304812379 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -1965,7 +1965,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien // 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(); + _scheduleShowCaretOnScreen(withAnimation: true); if (_hasInputConnection) { // To keep the cursor from blinking while typing, we want to restart the // cursor timer every time a new character is typed. @@ -2498,7 +2498,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien bool _showCaretOnScreenScheduled = false; - void _scheduleShowCaretOnScreen() { + void _scheduleShowCaretOnScreen({required bool withAnimation}) { if (_showCaretOnScreenScheduled) { return; } @@ -2538,17 +2538,23 @@ class EditableTextState extends State with AutomaticKeepAliveClien final RevealedOffset targetOffset = _getOffsetToRevealCaret(_currentCaretRect!); - _scrollController.animateTo( - targetOffset.offset, - duration: _caretAnimationDuration, - curve: _caretAnimationCurve, - ); - - renderEditable.showOnScreen( - rect: caretPadding.inflateRect(targetOffset.rect), - duration: _caretAnimationDuration, - curve: _caretAnimationCurve, - ); + if (withAnimation) { + _scrollController.animateTo( + targetOffset.offset, + duration: _caretAnimationDuration, + curve: _caretAnimationCurve, + ); + renderEditable.showOnScreen( + rect: caretPadding.inflateRect(targetOffset.rect), + duration: _caretAnimationDuration, + curve: _caretAnimationCurve, + ); + } else { + _scrollController.jumpTo(targetOffset.offset); + renderEditable.showOnScreen( + rect: caretPadding.inflateRect(targetOffset.rect), + ); + } }); } @@ -2561,7 +2567,9 @@ class EditableTextState extends State with AutomaticKeepAliveClien _selectionOverlay?.updateForScroll(); }); if (_lastBottomViewInset < WidgetsBinding.instance.window.viewInsets.bottom) { - _scheduleShowCaretOnScreen(); + // Because the metrics change signal from engine will come here every frame + // (on both iOS and Android). So we don't need to show caret with animation. + _scheduleShowCaretOnScreen(withAnimation: false); } } _lastBottomViewInset = WidgetsBinding.instance.window.viewInsets.bottom; @@ -2745,7 +2753,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien WidgetsBinding.instance.addObserver(this); _lastBottomViewInset = WidgetsBinding.instance.window.viewInsets.bottom; if (!widget.readOnly) { - _scheduleShowCaretOnScreen(); + _scheduleShowCaretOnScreen(withAnimation: true); } if (!_value.selection.isValid) { // Place cursor at the end if the selection is invalid when we receive focus. @@ -2900,7 +2908,7 @@ class EditableTextState extends State with AutomaticKeepAliveClien ? _value.selection != value.selection : _value != value; if (shouldShowCaret) { - _scheduleShowCaretOnScreen(); + _scheduleShowCaretOnScreen(withAnimation: true); } _formatAndSetValue(value, cause, userInteraction: true); } diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart index c50009c7e05..52c71eff2cc 100644 --- a/packages/flutter/test/widgets/editable_text_test.dart +++ b/packages/flutter/test/widgets/editable_text_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui' as ui; + import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; @@ -39,6 +41,26 @@ class _MatchesMethodCall extends Matcher { } } +// Used to set window.viewInsets since the real ui.WindowPadding has only a +// private constructor. +class _TestWindowPadding implements ui.WindowPadding { + const _TestWindowPadding({ + required this.bottom, + }); + + @override + final double bottom; + + @override + double get top => 0.0; + + @override + double get left => 0.0; + + @override + double get right => 0.0; +} + late TextEditingController controller; final FocusNode focusNode = FocusNode(debugLabel: 'EditableText Node'); final FocusScopeNode focusScopeNode = FocusScopeNode(debugLabel: 'EditableText Scope Node'); @@ -107,6 +129,51 @@ void main() { expect(tester.testTextInput.setClientArgs!['inputAction'], equals(serializedActionName)); } + // Related issue: https://github.com/flutter/flutter/issues/98115 + testWidgets('ScheduleShowCaretOnScreen with no animation when the window changes metrics', (WidgetTester tester) async { + final ScrollController scrollController = ScrollController(); + final Widget widget = MaterialApp( + home: Scaffold( + body: SingleChildScrollView( + controller: scrollController, + child: Column( + children: [ + Column( + children: List.generate( + 5, + (_) { + return Container( + height: 1200.0, + color: Colors.black12, + ); + }, + ), + ), + SizedBox( + height: 20, + child: EditableText( + controller: TextEditingController(), + backgroundCursorColor: Colors.grey, + focusNode: focusNode, + style: const TextStyle(), + cursorColor: Colors.red, + ), + ), + ], + ), + ), + ), + ); + await tester.pumpWidget(widget); + await tester.showKeyboard(find.byType(EditableText)); + TestWidgetsFlutterBinding.instance.window.viewInsetsTestValue = const _TestWindowPadding(bottom: 500); + await tester.pump(); + + // The offset of the scrollController should change immediately after window changes its metrics. + final double offsetAfter = scrollController.offset; + expect(offsetAfter, isNot(0.0)); + }); + // Regression test for https://github.com/flutter/flutter/issues/34538. testWidgets('RTL arabic correct caret placement after trailing whitespace', (WidgetTester tester) async { final TextEditingController controller = TextEditingController();