@@ -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
@@ -1597,7 +1678,14 @@ class EditableText extends StatefulWidget {
15971678/// State for a [EditableText] .
15981679class EditableTextState extends State <EditableText > with AutomaticKeepAliveClientMixin <EditableText >, WidgetsBindingObserver , TickerProviderStateMixin <EditableText >, TextSelectionDelegate implements TextInputClient , AutofillClient {
15991680 Timer ? _cursorTimer;
1600- bool _targetCursorVisibility = false ;
1681+ AnimationController get _cursorBlinkOpacityController {
1682+ return _backingCursorBlinkOpacityController ?? = AnimationController (
1683+ vsync: this ,
1684+ )..addListener (_onCursorColorTick);
1685+ }
1686+ AnimationController ? _backingCursorBlinkOpacityController;
1687+ late final Simulation _iosBlinkCursorSimulation = _DiscreteKeyFrameSimulation .iOSBlinkingCaret ();
1688+
16011689 final ValueNotifier <bool > _cursorVisibilityNotifier = ValueNotifier <bool >(true );
16021690 final GlobalKey _editableKey = GlobalKey ();
16031691 final ClipboardStatusNotifier ? _clipboardStatus = kIsWeb ? null : ClipboardStatusNotifier ();
@@ -1608,8 +1696,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
16081696 ScrollController ? _internalScrollController;
16091697 ScrollController get _scrollController => widget.scrollController ?? (_internalScrollController ?? = ScrollController ());
16101698
1611- AnimationController ? _cursorBlinkOpacityController;
1612-
16131699 final LayerLink _toolbarLayerLink = LayerLink ();
16141700 final LayerLink _startHandleLayerLink = LayerLink ();
16151701 final LayerLink _endHandleLayerLink = LayerLink ();
@@ -1637,10 +1723,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
16371723 /// - Changing the selection using a physical keyboard.
16381724 bool get _shouldCreateInputConnection => kIsWeb || ! widget.readOnly;
16391725
1640- // This value is an eyeball estimation of the time it takes for the iOS cursor
1641- // to ease in and out.
1642- static const Duration _fadeDuration = Duration (milliseconds: 250 );
1643-
16441726 // The time it takes for the floating cursor to snap to the text aligned
16451727 // cursor position after the user has finished placing it.
16461728 static const Duration _floatingCursorResetTime = Duration (milliseconds: 125 );
@@ -1652,7 +1734,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
16521734 @override
16531735 bool get wantKeepAlive => widget.focusNode.hasFocus;
16541736
1655- Color get _cursorColor => widget.cursorColor.withOpacity (_cursorBlinkOpacityController! .value);
1737+ Color get _cursorColor => widget.cursorColor.withOpacity (_cursorBlinkOpacityController.value);
16561738
16571739 @override
16581740 bool get cutEnabled => widget.toolbarOptions.cut && ! widget.readOnly && ! widget.obscureText;
@@ -1806,10 +1888,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
18061888 @override
18071889 void initState () {
18081890 super .initState ();
1809- _cursorBlinkOpacityController = AnimationController (
1810- vsync: this ,
1811- duration: _fadeDuration,
1812- )..addListener (_onCursorColorTick);
18131891 _clipboardStatus? .addListener (_onChangedClipboardStatus);
18141892 widget.controller.addListener (_didChangeTextEditingValue);
18151893 widget.focusNode.addListener (_handleFocusChanged);
@@ -1846,7 +1924,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
18461924 if (_tickersEnabled != newTickerEnabled) {
18471925 _tickersEnabled = newTickerEnabled;
18481926 if (_tickersEnabled && _cursorActive) {
1849- _startCursorTimer ();
1927+ _startCursorBlink ();
18501928 } else if (! _tickersEnabled && _cursorTimer != null ) {
18511929 // Cannot use _stopCursorTimer because it would reset _cursorActive.
18521930 _cursorTimer! .cancel ();
@@ -1946,8 +2024,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
19462024 assert (! _hasInputConnection);
19472025 _cursorTimer? .cancel ();
19482026 _cursorTimer = null ;
1949- _cursorBlinkOpacityController ? .dispose ();
1950- _cursorBlinkOpacityController = null ;
2027+ _backingCursorBlinkOpacityController ? .dispose ();
2028+ _backingCursorBlinkOpacityController = null ;
19512029 _selectionOverlay? .dispose ();
19522030 _selectionOverlay = null ;
19532031 widget.focusNode.removeListener (_handleFocusChanged);
@@ -2026,8 +2104,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
20262104 if (_hasInputConnection) {
20272105 // To keep the cursor from blinking while typing, we want to restart the
20282106 // cursor timer every time a new character is typed.
2029- _stopCursorTimer (resetCharTicks: false );
2030- _startCursorTimer ();
2107+ _stopCursorBlink (resetCharTicks: false );
2108+ _startCursorBlink ();
20312109 }
20322110 }
20332111
@@ -2548,8 +2626,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
25482626
25492627 // To keep the cursor from blinking while it moves, restart the timer here.
25502628 if (_cursorTimer != null ) {
2551- _stopCursorTimer (resetCharTicks: false );
2552- _startCursorTimer ();
2629+ _stopCursorBlink (resetCharTicks: false );
2630+ _startCursorBlink ();
25532631 }
25542632 }
25552633
@@ -2703,14 +2781,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
27032781 }
27042782
27052783 void _onCursorColorTick () {
2706- renderEditable.cursorColor = widget.cursorColor.withOpacity (_cursorBlinkOpacityController! .value);
2707- _cursorVisibilityNotifier.value = widget.showCursor && _cursorBlinkOpacityController! .value > 0 ;
2784+ renderEditable.cursorColor = widget.cursorColor.withOpacity (_cursorBlinkOpacityController.value);
2785+ _cursorVisibilityNotifier.value = widget.showCursor && _cursorBlinkOpacityController.value > 0 ;
27082786 }
27092787
27102788 /// Whether the blinking cursor is actually visible at this precise moment
27112789 /// (it's hidden half the time, since it blinks).
27122790 @visibleForTesting
2713- bool get cursorCurrentlyVisible => _cursorBlinkOpacityController! .value > 0 ;
2791+ bool get cursorCurrentlyVisible => _cursorBlinkOpacityController.value > 0 ;
27142792
27152793 /// The cursor blink interval (the amount of time the cursor is in the "on"
27162794 /// state or the "off" state). A complete cursor blink period is twice this
@@ -2725,83 +2803,69 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
27252803 int _obscureShowCharTicksPending = 0 ;
27262804 int ? _obscureLatestCharIndex;
27272805
2728- void _cursorTick (Timer timer) {
2729- _targetCursorVisibility = ! _targetCursorVisibility;
2730- final double targetOpacity = _targetCursorVisibility ? 1.0 : 0.0 ;
2731- if (widget.cursorOpacityAnimates) {
2732- // If we want to show the cursor, we will animate the opacity to the value
2733- // of 1.0, and likewise if we want to make it disappear, to 0.0. An easing
2734- // curve is used for the animation to mimic the aesthetics of the native
2735- // iOS cursor.
2736- //
2737- // These values and curves have been obtained through eyeballing, so are
2738- // likely not exactly the same as the values for native iOS.
2739- _cursorBlinkOpacityController! .animateTo (targetOpacity, curve: Curves .easeOut);
2740- } else {
2741- _cursorBlinkOpacityController! .value = targetOpacity;
2742- }
2743-
2744- if (_obscureShowCharTicksPending > 0 ) {
2745- setState (() {
2746- _obscureShowCharTicksPending = WidgetsBinding .instance.platformDispatcher.brieflyShowPassword
2747- ? _obscureShowCharTicksPending - 1
2748- : 0 ;
2749- });
2750- }
2751- }
2752-
2753- void _cursorWaitForStart (Timer timer) {
2754- assert (_kCursorBlinkHalfPeriod > _fadeDuration);
2755- assert (! EditableText .debugDeterministicCursor);
2756- _cursorTimer? .cancel ();
2757- _cursorTimer = Timer .periodic (_kCursorBlinkHalfPeriod, _cursorTick);
2758- }
2759-
27602806 // Indicates whether the cursor should be blinking right now (but it may
27612807 // actually not blink because it's disabled via TickerMode.of(context)).
27622808 bool _cursorActive = false ;
27632809
2764- void _startCursorTimer () {
2765- assert (_cursorTimer == null );
2810+ void _startCursorBlink () {
2811+ assert (! ( _cursorTimer? .isActive ?? false ) || ! (_backingCursorBlinkOpacityController ? .isAnimating ?? false ) );
27662812 _cursorActive = true ;
27672813 if (! _tickersEnabled) {
27682814 return ;
27692815 }
2770- _targetCursorVisibility = true ;
2771- _cursorBlinkOpacityController! .value = 1.0 ;
2816+ _cursorTimer ? . cancel () ;
2817+ _cursorBlinkOpacityController.value = 1.0 ;
27722818 if (EditableText .debugDeterministicCursor) {
27732819 return ;
27742820 }
27752821 if (widget.cursorOpacityAnimates) {
2776- _cursorTimer = Timer . periodic (_kCursorBlinkWaitForStart, _cursorWaitForStart );
2822+ _cursorBlinkOpacityController. animateWith (_iosBlinkCursorSimulation). whenComplete (_onCursorTick );
27772823 } else {
2778- _cursorTimer = Timer .periodic (_kCursorBlinkHalfPeriod, _cursorTick );
2824+ _cursorTimer = Timer .periodic (_kCursorBlinkHalfPeriod, ( Timer timer) { _onCursorTick (); } );
27792825 }
27802826 }
27812827
2782- void _stopCursorTimer ({ bool resetCharTicks = true }) {
2828+ void _onCursorTick () {
2829+ if (_obscureShowCharTicksPending > 0 ) {
2830+ _obscureShowCharTicksPending = WidgetsBinding .instance.platformDispatcher.brieflyShowPassword
2831+ ? _obscureShowCharTicksPending - 1
2832+ : 0 ;
2833+ if (_obscureShowCharTicksPending == 0 ) {
2834+ setState (() { });
2835+ }
2836+ }
2837+
2838+ if (widget.cursorOpacityAnimates) {
2839+ _cursorTimer? .cancel ();
2840+ // Schedule this as an async task to avoid blocking tester.pumpAndSettle
2841+ // indefinitely.
2842+ _cursorTimer = Timer (Duration .zero, () => _cursorBlinkOpacityController.animateWith (_iosBlinkCursorSimulation).whenComplete (_onCursorTick));
2843+ } else {
2844+ if (! (_cursorTimer? .isActive ?? false ) && _tickersEnabled) {
2845+ _cursorTimer = Timer .periodic (_kCursorBlinkHalfPeriod, (Timer timer) { _onCursorTick (); });
2846+ }
2847+ _cursorBlinkOpacityController.value = _cursorBlinkOpacityController.value == 0 ? 1 : 0 ;
2848+ }
2849+ }
2850+
2851+ void _stopCursorBlink ({ bool resetCharTicks = true }) {
27832852 _cursorActive = false ;
2784- _cursorTimer? .cancel ();
2785- _cursorTimer = null ;
2786- _targetCursorVisibility = false ;
2787- _cursorBlinkOpacityController! .value = 0.0 ;
2853+ _cursorBlinkOpacityController.value = 0.0 ;
27882854 if (EditableText .debugDeterministicCursor) {
27892855 return ;
27902856 }
2857+ _cursorBlinkOpacityController.value = 0.0 ;
27912858 if (resetCharTicks) {
27922859 _obscureShowCharTicksPending = 0 ;
27932860 }
2794- if (widget.cursorOpacityAnimates) {
2795- _cursorBlinkOpacityController! .stop ();
2796- _cursorBlinkOpacityController! .value = 0.0 ;
2797- }
27982861 }
27992862
28002863 void _startOrStopCursorTimerIfNeeded () {
28012864 if (_cursorTimer == null && _hasFocus && _value.selection.isCollapsed) {
2802- _startCursorTimer ();
2803- } else if (_cursorActive && (! _hasFocus || ! _value.selection.isCollapsed)) {
2804- _stopCursorTimer ();
2865+ _startCursorBlink ();
2866+ }
2867+ else if (_cursorActive && (! _hasFocus || ! _value.selection.isCollapsed)) {
2868+ _stopCursorBlink ();
28052869 }
28062870 }
28072871
@@ -3488,8 +3552,10 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
34883552 String text = _value.text;
34893553 text = widget.obscuringCharacter * text.length;
34903554 // Reveal the latest character in an obscured field only on mobile.
3555+ // Newer verions of iOS (iOS 15+) no longer reveal the most recently
3556+ // entered character.
34913557 const Set <TargetPlatform > mobilePlatforms = < TargetPlatform > {
3492- TargetPlatform .android, TargetPlatform .iOS, TargetPlatform . fuchsia,
3558+ TargetPlatform .android, TargetPlatform .fuchsia,
34933559 };
34943560 final bool breiflyShowPassword = WidgetsBinding .instance.platformDispatcher.brieflyShowPassword
34953561 && mobilePlatforms.contains (defaultTargetPlatform);
0 commit comments