@@ -52,10 +52,6 @@ typedef AppPrivateCommandCallback = void Function(String, Map<String, dynamic>);
5252// to transparent, is twice this duration.
5353const Duration _kCursorBlinkHalfPeriod = Duration (milliseconds: 500 );
5454
55- // The time the cursor is static in opacity before animating to become
56- // transparent.
57- const Duration _kCursorBlinkWaitForStart = Duration (milliseconds: 150 );
58-
5955// Number of cursor ticks during which the most recently entered character
6056// is shown in an obscured text field.
6157const int _kObscureShowLatestCharCursorTicks = 3 ;
@@ -301,6 +297,91 @@ class ToolbarOptions {
301297 final bool selectAll;
302298}
303299
300+ // A time-value pair that represents a key frame in an animation.
301+ class _KeyFrame {
302+ const _KeyFrame (this .time, this .value);
303+ // Values extracted from iOS 15.4 UIKit.
304+ static const List <_KeyFrame > iOSBlinkingCaretKeyFrames = < _KeyFrame > [
305+ _KeyFrame (0 , 1 ), // 0
306+ _KeyFrame (0.5 , 1 ), // 1
307+ _KeyFrame (0.5375 , 0.75 ), // 2
308+ _KeyFrame (0.575 , 0.5 ), // 3
309+ _KeyFrame (0.6125 , 0.25 ), // 4
310+ _KeyFrame (0.65 , 0 ), // 5
311+ _KeyFrame (0.85 , 0 ), // 6
312+ _KeyFrame (0.8875 , 0.25 ), // 7
313+ _KeyFrame (0.925 , 0.5 ), // 8
314+ _KeyFrame (0.9625 , 0.75 ), // 9
315+ _KeyFrame (1 , 1 ), // 10
316+ ];
317+
318+ // The timing, in seconds, of the specified animation `value`.
319+ final double time;
320+ final double value;
321+ }
322+
323+ class _DiscreteKeyFrameSimulation extends Simulation {
324+ _DiscreteKeyFrameSimulation .iOSBlinkingCaret () : this ._(_KeyFrame .iOSBlinkingCaretKeyFrames, 1 );
325+ _DiscreteKeyFrameSimulation ._(this ._keyFrames, this .maxDuration)
326+ : assert (_keyFrames.isNotEmpty),
327+ assert (_keyFrames.last.time <= maxDuration),
328+ assert (() {
329+ for (int i = 0 ; i < _keyFrames.length - 1 ; i += 1 ) {
330+ if (_keyFrames[i].time > _keyFrames[i + 1 ].time) {
331+ return false ;
332+ }
333+ }
334+ return true ;
335+ }(), 'The key frame sequence must be sorted by time.' );
336+
337+ final double maxDuration;
338+
339+ final List <_KeyFrame > _keyFrames;
340+
341+ @override
342+ double dx (double time) => 0 ;
343+
344+ @override
345+ bool isDone (double time) => time >= maxDuration;
346+
347+ // The index of the KeyFrame corresponds to the most recent input `time`.
348+ int _lastKeyFrameIndex = 0 ;
349+
350+ @override
351+ double x (double time) {
352+ final int length = _keyFrames.length;
353+
354+ // Perform a linear search in the sorted key frame list, starting from the
355+ // last key frame found, since the input `time` usually monotonically
356+ // increases by a small amount.
357+ int searchIndex;
358+ final int endIndex;
359+ if (_keyFrames[_lastKeyFrameIndex].time > time) {
360+ // The simulation may have restarted. Search within the index range
361+ // [0, _lastKeyFrameIndex).
362+ searchIndex = 0 ;
363+ endIndex = _lastKeyFrameIndex;
364+ } else {
365+ searchIndex = _lastKeyFrameIndex;
366+ endIndex = length;
367+ }
368+
369+ // Find the target key frame. Don't have to check (endIndex - 1): if
370+ // (endIndex - 2) doesn't work we'll have to pick (endIndex - 1) anyways.
371+ while (searchIndex < endIndex - 1 ) {
372+ assert (_keyFrames[searchIndex].time <= time);
373+ final _KeyFrame next = _keyFrames[searchIndex + 1 ];
374+ if (time < next.time) {
375+ break ;
376+ }
377+ searchIndex += 1 ;
378+ }
379+
380+ _lastKeyFrameIndex = searchIndex;
381+ return _keyFrames[_lastKeyFrameIndex].value;
382+ }
383+ }
384+
304385/// A basic text input field.
305386///
306387/// This widget interacts with the [TextInput] service to let the user edit the
@@ -1606,7 +1687,14 @@ class EditableText extends StatefulWidget {
16061687/// State for a [EditableText] .
16071688class EditableTextState extends State <EditableText > with AutomaticKeepAliveClientMixin <EditableText >, WidgetsBindingObserver , TickerProviderStateMixin <EditableText >, TextSelectionDelegate , TextInputClient implements AutofillClient {
16081689 Timer ? _cursorTimer;
1609- bool _targetCursorVisibility = false ;
1690+ AnimationController get _cursorBlinkOpacityController {
1691+ return _backingCursorBlinkOpacityController ?? = AnimationController (
1692+ vsync: this ,
1693+ )..addListener (_onCursorColorTick);
1694+ }
1695+ AnimationController ? _backingCursorBlinkOpacityController;
1696+ late final Simulation _iosBlinkCursorSimulation = _DiscreteKeyFrameSimulation .iOSBlinkingCaret ();
1697+
16101698 final ValueNotifier <bool > _cursorVisibilityNotifier = ValueNotifier <bool >(true );
16111699 final GlobalKey _editableKey = GlobalKey ();
16121700 final ClipboardStatusNotifier ? _clipboardStatus = kIsWeb ? null : ClipboardStatusNotifier ();
@@ -1617,8 +1705,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
16171705 ScrollController ? _internalScrollController;
16181706 ScrollController get _scrollController => widget.scrollController ?? (_internalScrollController ?? = ScrollController ());
16191707
1620- AnimationController ? _cursorBlinkOpacityController;
1621-
16221708 final LayerLink _toolbarLayerLink = LayerLink ();
16231709 final LayerLink _startHandleLayerLink = LayerLink ();
16241710 final LayerLink _endHandleLayerLink = LayerLink ();
@@ -1646,10 +1732,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
16461732 /// - Changing the selection using a physical keyboard.
16471733 bool get _shouldCreateInputConnection => kIsWeb || ! widget.readOnly;
16481734
1649- // This value is an eyeball estimation of the time it takes for the iOS cursor
1650- // to ease in and out.
1651- static const Duration _fadeDuration = Duration (milliseconds: 250 );
1652-
16531735 // The time it takes for the floating cursor to snap to the text aligned
16541736 // cursor position after the user has finished placing it.
16551737 static const Duration _floatingCursorResetTime = Duration (milliseconds: 125 );
@@ -1661,7 +1743,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
16611743 @override
16621744 bool get wantKeepAlive => widget.focusNode.hasFocus;
16631745
1664- Color get _cursorColor => widget.cursorColor.withOpacity (_cursorBlinkOpacityController! .value);
1746+ Color get _cursorColor => widget.cursorColor.withOpacity (_cursorBlinkOpacityController.value);
16651747
16661748 @override
16671749 bool get cutEnabled => widget.toolbarOptions.cut && ! widget.readOnly && ! widget.obscureText;
@@ -1815,10 +1897,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
18151897 @override
18161898 void initState () {
18171899 super .initState ();
1818- _cursorBlinkOpacityController = AnimationController (
1819- vsync: this ,
1820- duration: _fadeDuration,
1821- )..addListener (_onCursorColorTick);
18221900 _clipboardStatus? .addListener (_onChangedClipboardStatus);
18231901 widget.controller.addListener (_didChangeTextEditingValue);
18241902 widget.focusNode.addListener (_handleFocusChanged);
@@ -1855,7 +1933,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
18551933 if (_tickersEnabled != newTickerEnabled) {
18561934 _tickersEnabled = newTickerEnabled;
18571935 if (_tickersEnabled && _cursorActive) {
1858- _startCursorTimer ();
1936+ _startCursorBlink ();
18591937 } else if (! _tickersEnabled && _cursorTimer != null ) {
18601938 // Cannot use _stopCursorTimer because it would reset _cursorActive.
18611939 _cursorTimer! .cancel ();
@@ -1955,8 +2033,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
19552033 assert (! _hasInputConnection);
19562034 _cursorTimer? .cancel ();
19572035 _cursorTimer = null ;
1958- _cursorBlinkOpacityController ? .dispose ();
1959- _cursorBlinkOpacityController = null ;
2036+ _backingCursorBlinkOpacityController ? .dispose ();
2037+ _backingCursorBlinkOpacityController = null ;
19602038 _selectionOverlay? .dispose ();
19612039 _selectionOverlay = null ;
19622040 widget.focusNode.removeListener (_handleFocusChanged);
@@ -2035,8 +2113,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
20352113 if (_hasInputConnection) {
20362114 // To keep the cursor from blinking while typing, we want to restart the
20372115 // cursor timer every time a new character is typed.
2038- _stopCursorTimer (resetCharTicks: false );
2039- _startCursorTimer ();
2116+ _stopCursorBlink (resetCharTicks: false );
2117+ _startCursorBlink ();
20402118 }
20412119 }
20422120
@@ -2557,8 +2635,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
25572635
25582636 // To keep the cursor from blinking while it moves, restart the timer here.
25592637 if (_cursorTimer != null ) {
2560- _stopCursorTimer (resetCharTicks: false );
2561- _startCursorTimer ();
2638+ _stopCursorBlink (resetCharTicks: false );
2639+ _startCursorBlink ();
25622640 }
25632641 }
25642642
@@ -2712,14 +2790,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
27122790 }
27132791
27142792 void _onCursorColorTick () {
2715- renderEditable.cursorColor = widget.cursorColor.withOpacity (_cursorBlinkOpacityController! .value);
2716- _cursorVisibilityNotifier.value = widget.showCursor && _cursorBlinkOpacityController! .value > 0 ;
2793+ renderEditable.cursorColor = widget.cursorColor.withOpacity (_cursorBlinkOpacityController.value);
2794+ _cursorVisibilityNotifier.value = widget.showCursor && _cursorBlinkOpacityController.value > 0 ;
27172795 }
27182796
27192797 /// Whether the blinking cursor is actually visible at this precise moment
27202798 /// (it's hidden half the time, since it blinks).
27212799 @visibleForTesting
2722- bool get cursorCurrentlyVisible => _cursorBlinkOpacityController! .value > 0 ;
2800+ bool get cursorCurrentlyVisible => _cursorBlinkOpacityController.value > 0 ;
27232801
27242802 /// The cursor blink interval (the amount of time the cursor is in the "on"
27252803 /// state or the "off" state). A complete cursor blink period is twice this
@@ -2734,83 +2812,67 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
27342812 int _obscureShowCharTicksPending = 0 ;
27352813 int ? _obscureLatestCharIndex;
27362814
2737- void _cursorTick (Timer timer) {
2738- _targetCursorVisibility = ! _targetCursorVisibility;
2739- final double targetOpacity = _targetCursorVisibility ? 1.0 : 0.0 ;
2740- if (widget.cursorOpacityAnimates) {
2741- // If we want to show the cursor, we will animate the opacity to the value
2742- // of 1.0, and likewise if we want to make it disappear, to 0.0. An easing
2743- // curve is used for the animation to mimic the aesthetics of the native
2744- // iOS cursor.
2745- //
2746- // These values and curves have been obtained through eyeballing, so are
2747- // likely not exactly the same as the values for native iOS.
2748- _cursorBlinkOpacityController! .animateTo (targetOpacity, curve: Curves .easeOut);
2749- } else {
2750- _cursorBlinkOpacityController! .value = targetOpacity;
2751- }
2752-
2753- if (_obscureShowCharTicksPending > 0 ) {
2754- setState (() {
2755- _obscureShowCharTicksPending = WidgetsBinding .instance.platformDispatcher.brieflyShowPassword
2756- ? _obscureShowCharTicksPending - 1
2757- : 0 ;
2758- });
2759- }
2760- }
2761-
2762- void _cursorWaitForStart (Timer timer) {
2763- assert (_kCursorBlinkHalfPeriod > _fadeDuration);
2764- assert (! EditableText .debugDeterministicCursor);
2765- _cursorTimer? .cancel ();
2766- _cursorTimer = Timer .periodic (_kCursorBlinkHalfPeriod, _cursorTick);
2767- }
2768-
27692815 // Indicates whether the cursor should be blinking right now (but it may
27702816 // actually not blink because it's disabled via TickerMode.of(context)).
27712817 bool _cursorActive = false ;
27722818
2773- void _startCursorTimer () {
2774- assert (_cursorTimer == null );
2819+ void _startCursorBlink () {
2820+ assert (! ( _cursorTimer? .isActive ?? false ) || ! (_backingCursorBlinkOpacityController ? .isAnimating ?? false ) );
27752821 _cursorActive = true ;
27762822 if (! _tickersEnabled) {
27772823 return ;
27782824 }
2779- _targetCursorVisibility = true ;
2780- _cursorBlinkOpacityController! .value = 1.0 ;
2825+ _cursorTimer ? . cancel () ;
2826+ _cursorBlinkOpacityController.value = 1.0 ;
27812827 if (EditableText .debugDeterministicCursor) {
27822828 return ;
27832829 }
27842830 if (widget.cursorOpacityAnimates) {
2785- _cursorTimer = Timer . periodic (_kCursorBlinkWaitForStart, _cursorWaitForStart );
2831+ _cursorBlinkOpacityController. animateWith (_iosBlinkCursorSimulation). whenComplete (_onCursorTick );
27862832 } else {
2787- _cursorTimer = Timer .periodic (_kCursorBlinkHalfPeriod, _cursorTick );
2833+ _cursorTimer = Timer .periodic (_kCursorBlinkHalfPeriod, ( Timer timer) { _onCursorTick (); } );
27882834 }
27892835 }
27902836
2791- void _stopCursorTimer ({ bool resetCharTicks = true }) {
2837+ void _onCursorTick () {
2838+ if (_obscureShowCharTicksPending > 0 ) {
2839+ _obscureShowCharTicksPending = WidgetsBinding .instance.platformDispatcher.brieflyShowPassword
2840+ ? _obscureShowCharTicksPending - 1
2841+ : 0 ;
2842+ if (_obscureShowCharTicksPending == 0 ) {
2843+ setState (() { });
2844+ }
2845+ }
2846+
2847+ if (widget.cursorOpacityAnimates) {
2848+ _cursorTimer? .cancel ();
2849+ // Schedule this as an async task to avoid blocking tester.pumpAndSettle
2850+ // indefinitely.
2851+ _cursorTimer = Timer (Duration .zero, () => _cursorBlinkOpacityController.animateWith (_iosBlinkCursorSimulation).whenComplete (_onCursorTick));
2852+ } else {
2853+ if (! (_cursorTimer? .isActive ?? false ) && _tickersEnabled) {
2854+ _cursorTimer = Timer .periodic (_kCursorBlinkHalfPeriod, (Timer timer) { _onCursorTick (); });
2855+ }
2856+ _cursorBlinkOpacityController.value = _cursorBlinkOpacityController.value == 0 ? 1 : 0 ;
2857+ }
2858+ }
2859+
2860+ void _stopCursorBlink ({ bool resetCharTicks = true }) {
27922861 _cursorActive = false ;
2862+ _cursorBlinkOpacityController.value = 0.0 ;
27932863 _cursorTimer? .cancel ();
27942864 _cursorTimer = null ;
2795- _targetCursorVisibility = false ;
2796- _cursorBlinkOpacityController! .value = 0.0 ;
2797- if (EditableText .debugDeterministicCursor) {
2798- return ;
2799- }
28002865 if (resetCharTicks) {
28012866 _obscureShowCharTicksPending = 0 ;
28022867 }
2803- if (widget.cursorOpacityAnimates) {
2804- _cursorBlinkOpacityController! .stop ();
2805- _cursorBlinkOpacityController! .value = 0.0 ;
2806- }
28072868 }
28082869
28092870 void _startOrStopCursorTimerIfNeeded () {
28102871 if (_cursorTimer == null && _hasFocus && _value.selection.isCollapsed) {
2811- _startCursorTimer ();
2812- } else if (_cursorActive && (! _hasFocus || ! _value.selection.isCollapsed)) {
2813- _stopCursorTimer ();
2872+ _startCursorBlink ();
2873+ }
2874+ else if (_cursorActive && (! _hasFocus || ! _value.selection.isCollapsed)) {
2875+ _stopCursorBlink ();
28142876 }
28152877 }
28162878
@@ -3497,8 +3559,10 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
34973559 String text = _value.text;
34983560 text = widget.obscuringCharacter * text.length;
34993561 // Reveal the latest character in an obscured field only on mobile.
3562+ // Newer verions of iOS (iOS 15+) no longer reveal the most recently
3563+ // entered character.
35003564 const Set <TargetPlatform > mobilePlatforms = < TargetPlatform > {
3501- TargetPlatform .android, TargetPlatform .iOS, TargetPlatform . fuchsia,
3565+ TargetPlatform .android, TargetPlatform .fuchsia,
35023566 };
35033567 final bool breiflyShowPassword = WidgetsBinding .instance.platformDispatcher.brieflyShowPassword
35043568 && mobilePlatforms.contains (defaultTargetPlatform);
0 commit comments