@@ -52,6 +52,10 @@ 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+
5559// Number of cursor ticks during which the most recently entered character
5660// is shown in an obscured text field.
5761const int _kObscureShowLatestCharCursorTicks = 3 ;
@@ -297,91 +301,6 @@ class ToolbarOptions {
297301 final bool selectAll;
298302}
299303
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-
385304/// A basic text input field.
386305///
387306/// This widget interacts with the [TextInput] service to let the user edit the
@@ -1678,14 +1597,7 @@ class EditableText extends StatefulWidget {
16781597/// State for a [EditableText] .
16791598class EditableTextState extends State <EditableText > with AutomaticKeepAliveClientMixin <EditableText >, WidgetsBindingObserver , TickerProviderStateMixin <EditableText >, TextSelectionDelegate , TextInputClient implements AutofillClient {
16801599 Timer ? _cursorTimer;
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-
1600+ bool _targetCursorVisibility = false ;
16891601 final ValueNotifier <bool > _cursorVisibilityNotifier = ValueNotifier <bool >(true );
16901602 final GlobalKey _editableKey = GlobalKey ();
16911603 final ClipboardStatusNotifier ? _clipboardStatus = kIsWeb ? null : ClipboardStatusNotifier ();
@@ -1696,6 +1608,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
16961608 ScrollController ? _internalScrollController;
16971609 ScrollController get _scrollController => widget.scrollController ?? (_internalScrollController ?? = ScrollController ());
16981610
1611+ AnimationController ? _cursorBlinkOpacityController;
1612+
16991613 final LayerLink _toolbarLayerLink = LayerLink ();
17001614 final LayerLink _startHandleLayerLink = LayerLink ();
17011615 final LayerLink _endHandleLayerLink = LayerLink ();
@@ -1723,6 +1637,10 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
17231637 /// - Changing the selection using a physical keyboard.
17241638 bool get _shouldCreateInputConnection => kIsWeb || ! widget.readOnly;
17251639
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+
17261644 // The time it takes for the floating cursor to snap to the text aligned
17271645 // cursor position after the user has finished placing it.
17281646 static const Duration _floatingCursorResetTime = Duration (milliseconds: 125 );
@@ -1734,7 +1652,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
17341652 @override
17351653 bool get wantKeepAlive => widget.focusNode.hasFocus;
17361654
1737- Color get _cursorColor => widget.cursorColor.withOpacity (_cursorBlinkOpacityController.value);
1655+ Color get _cursorColor => widget.cursorColor.withOpacity (_cursorBlinkOpacityController! .value);
17381656
17391657 @override
17401658 bool get cutEnabled => widget.toolbarOptions.cut && ! widget.readOnly && ! widget.obscureText;
@@ -1888,6 +1806,10 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
18881806 @override
18891807 void initState () {
18901808 super .initState ();
1809+ _cursorBlinkOpacityController = AnimationController (
1810+ vsync: this ,
1811+ duration: _fadeDuration,
1812+ )..addListener (_onCursorColorTick);
18911813 _clipboardStatus? .addListener (_onChangedClipboardStatus);
18921814 widget.controller.addListener (_didChangeTextEditingValue);
18931815 widget.focusNode.addListener (_handleFocusChanged);
@@ -1924,7 +1846,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
19241846 if (_tickersEnabled != newTickerEnabled) {
19251847 _tickersEnabled = newTickerEnabled;
19261848 if (_tickersEnabled && _cursorActive) {
1927- _startCursorBlink ();
1849+ _startCursorTimer ();
19281850 } else if (! _tickersEnabled && _cursorTimer != null ) {
19291851 // Cannot use _stopCursorTimer because it would reset _cursorActive.
19301852 _cursorTimer! .cancel ();
@@ -2024,8 +1946,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
20241946 assert (! _hasInputConnection);
20251947 _cursorTimer? .cancel ();
20261948 _cursorTimer = null ;
2027- _backingCursorBlinkOpacityController ? .dispose ();
2028- _backingCursorBlinkOpacityController = null ;
1949+ _cursorBlinkOpacityController ? .dispose ();
1950+ _cursorBlinkOpacityController = null ;
20291951 _selectionOverlay? .dispose ();
20301952 _selectionOverlay = null ;
20311953 widget.focusNode.removeListener (_handleFocusChanged);
@@ -2104,8 +2026,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
21042026 if (_hasInputConnection) {
21052027 // To keep the cursor from blinking while typing, we want to restart the
21062028 // cursor timer every time a new character is typed.
2107- _stopCursorBlink (resetCharTicks: false );
2108- _startCursorBlink ();
2029+ _stopCursorTimer (resetCharTicks: false );
2030+ _startCursorTimer ();
21092031 }
21102032 }
21112033
@@ -2626,8 +2548,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
26262548
26272549 // To keep the cursor from blinking while it moves, restart the timer here.
26282550 if (_cursorTimer != null ) {
2629- _stopCursorBlink (resetCharTicks: false );
2630- _startCursorBlink ();
2551+ _stopCursorTimer (resetCharTicks: false );
2552+ _startCursorTimer ();
26312553 }
26322554 }
26332555
@@ -2781,14 +2703,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
27812703 }
27822704
27832705 void _onCursorColorTick () {
2784- renderEditable.cursorColor = widget.cursorColor.withOpacity (_cursorBlinkOpacityController.value);
2785- _cursorVisibilityNotifier.value = widget.showCursor && _cursorBlinkOpacityController.value > 0 ;
2706+ renderEditable.cursorColor = widget.cursorColor.withOpacity (_cursorBlinkOpacityController! .value);
2707+ _cursorVisibilityNotifier.value = widget.showCursor && _cursorBlinkOpacityController! .value > 0 ;
27862708 }
27872709
27882710 /// Whether the blinking cursor is actually visible at this precise moment
27892711 /// (it's hidden half the time, since it blinks).
27902712 @visibleForTesting
2791- bool get cursorCurrentlyVisible => _cursorBlinkOpacityController.value > 0 ;
2713+ bool get cursorCurrentlyVisible => _cursorBlinkOpacityController! .value > 0 ;
27922714
27932715 /// The cursor blink interval (the amount of time the cursor is in the "on"
27942716 /// state or the "off" state). A complete cursor blink period is twice this
@@ -2803,69 +2725,83 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
28032725 int _obscureShowCharTicksPending = 0 ;
28042726 int ? _obscureLatestCharIndex;
28052727
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+
28062760 // Indicates whether the cursor should be blinking right now (but it may
28072761 // actually not blink because it's disabled via TickerMode.of(context)).
28082762 bool _cursorActive = false ;
28092763
2810- void _startCursorBlink () {
2811- assert (! ( _cursorTimer? .isActive ?? false ) || ! (_backingCursorBlinkOpacityController ? .isAnimating ?? false ) );
2764+ void _startCursorTimer () {
2765+ assert (_cursorTimer == null );
28122766 _cursorActive = true ;
28132767 if (! _tickersEnabled) {
28142768 return ;
28152769 }
2816- _cursorTimer ? . cancel () ;
2817- _cursorBlinkOpacityController.value = 1.0 ;
2770+ _targetCursorVisibility = true ;
2771+ _cursorBlinkOpacityController! .value = 1.0 ;
28182772 if (EditableText .debugDeterministicCursor) {
28192773 return ;
28202774 }
28212775 if (widget.cursorOpacityAnimates) {
2822- _cursorBlinkOpacityController. animateWith (_iosBlinkCursorSimulation). whenComplete (_onCursorTick );
2776+ _cursorTimer = Timer . periodic (_kCursorBlinkWaitForStart, _cursorWaitForStart );
28232777 } else {
2824- _cursorTimer = Timer .periodic (_kCursorBlinkHalfPeriod, ( Timer timer) { _onCursorTick (); } );
2778+ _cursorTimer = Timer .periodic (_kCursorBlinkHalfPeriod, _cursorTick );
28252779 }
28262780 }
28272781
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 }) {
2782+ void _stopCursorTimer ({ bool resetCharTicks = true }) {
28522783 _cursorActive = false ;
2853- _cursorBlinkOpacityController.value = 0.0 ;
2784+ _cursorTimer? .cancel ();
2785+ _cursorTimer = null ;
2786+ _targetCursorVisibility = false ;
2787+ _cursorBlinkOpacityController! .value = 0.0 ;
28542788 if (EditableText .debugDeterministicCursor) {
28552789 return ;
28562790 }
2857- _cursorBlinkOpacityController.value = 0.0 ;
28582791 if (resetCharTicks) {
28592792 _obscureShowCharTicksPending = 0 ;
28602793 }
2794+ if (widget.cursorOpacityAnimates) {
2795+ _cursorBlinkOpacityController! .stop ();
2796+ _cursorBlinkOpacityController! .value = 0.0 ;
2797+ }
28612798 }
28622799
28632800 void _startOrStopCursorTimerIfNeeded () {
28642801 if (_cursorTimer == null && _hasFocus && _value.selection.isCollapsed) {
2865- _startCursorBlink ();
2866- }
2867- else if (_cursorActive && (! _hasFocus || ! _value.selection.isCollapsed)) {
2868- _stopCursorBlink ();
2802+ _startCursorTimer ();
2803+ } else if (_cursorActive && (! _hasFocus || ! _value.selection.isCollapsed)) {
2804+ _stopCursorTimer ();
28692805 }
28702806 }
28712807
@@ -3552,10 +3488,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
35523488 String text = _value.text;
35533489 text = widget.obscuringCharacter * text.length;
35543490 // 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.
35573491 const Set <TargetPlatform > mobilePlatforms = < TargetPlatform > {
3558- TargetPlatform .android, TargetPlatform .fuchsia,
3492+ TargetPlatform .android, TargetPlatform .iOS, TargetPlatform . fuchsia,
35593493 };
35603494 final bool breiflyShowPassword = WidgetsBinding .instance.platformDispatcher.brieflyShowPassword
35613495 && mobilePlatforms.contains (defaultTargetPlatform);
0 commit comments