@@ -52,10 +52,6 @@ typedef AppPrivateCommandCallback = void Function(String, Map<String, dynamic>);
52
52
// to transparent, is twice this duration.
53
53
const Duration _kCursorBlinkHalfPeriod = Duration (milliseconds: 500 );
54
54
55
- // The time the cursor is static in opacity before animating to become
56
- // transparent.
57
- const Duration _kCursorBlinkWaitForStart = Duration (milliseconds: 150 );
58
-
59
55
// Number of cursor ticks during which the most recently entered character
60
56
// is shown in an obscured text field.
61
57
const int _kObscureShowLatestCharCursorTicks = 3 ;
@@ -301,6 +297,91 @@ class ToolbarOptions {
301
297
final bool selectAll;
302
298
}
303
299
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
+
304
385
/// A basic text input field.
305
386
///
306
387
/// This widget interacts with the [TextInput] service to let the user edit the
@@ -1597,7 +1678,14 @@ class EditableText extends StatefulWidget {
1597
1678
/// State for a [EditableText] .
1598
1679
class EditableTextState extends State <EditableText > with AutomaticKeepAliveClientMixin <EditableText >, WidgetsBindingObserver , TickerProviderStateMixin <EditableText >, TextSelectionDelegate implements TextInputClient , AutofillClient {
1599
1680
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
+
1601
1689
final ValueNotifier <bool > _cursorVisibilityNotifier = ValueNotifier <bool >(true );
1602
1690
final GlobalKey _editableKey = GlobalKey ();
1603
1691
final ClipboardStatusNotifier ? _clipboardStatus = kIsWeb ? null : ClipboardStatusNotifier ();
@@ -1608,8 +1696,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
1608
1696
ScrollController ? _internalScrollController;
1609
1697
ScrollController get _scrollController => widget.scrollController ?? (_internalScrollController ?? = ScrollController ());
1610
1698
1611
- AnimationController ? _cursorBlinkOpacityController;
1612
-
1613
1699
final LayerLink _toolbarLayerLink = LayerLink ();
1614
1700
final LayerLink _startHandleLayerLink = LayerLink ();
1615
1701
final LayerLink _endHandleLayerLink = LayerLink ();
@@ -1637,10 +1723,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
1637
1723
/// - Changing the selection using a physical keyboard.
1638
1724
bool get _shouldCreateInputConnection => kIsWeb || ! widget.readOnly;
1639
1725
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
-
1644
1726
// The time it takes for the floating cursor to snap to the text aligned
1645
1727
// cursor position after the user has finished placing it.
1646
1728
static const Duration _floatingCursorResetTime = Duration (milliseconds: 125 );
@@ -1652,7 +1734,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
1652
1734
@override
1653
1735
bool get wantKeepAlive => widget.focusNode.hasFocus;
1654
1736
1655
- Color get _cursorColor => widget.cursorColor.withOpacity (_cursorBlinkOpacityController! .value);
1737
+ Color get _cursorColor => widget.cursorColor.withOpacity (_cursorBlinkOpacityController.value);
1656
1738
1657
1739
@override
1658
1740
bool get cutEnabled => widget.toolbarOptions.cut && ! widget.readOnly && ! widget.obscureText;
@@ -1806,10 +1888,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
1806
1888
@override
1807
1889
void initState () {
1808
1890
super .initState ();
1809
- _cursorBlinkOpacityController = AnimationController (
1810
- vsync: this ,
1811
- duration: _fadeDuration,
1812
- )..addListener (_onCursorColorTick);
1813
1891
_clipboardStatus? .addListener (_onChangedClipboardStatus);
1814
1892
widget.controller.addListener (_didChangeTextEditingValue);
1815
1893
widget.focusNode.addListener (_handleFocusChanged);
@@ -1846,7 +1924,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
1846
1924
if (_tickersEnabled != newTickerEnabled) {
1847
1925
_tickersEnabled = newTickerEnabled;
1848
1926
if (_tickersEnabled && _cursorActive) {
1849
- _startCursorTimer ();
1927
+ _startCursorBlink ();
1850
1928
} else if (! _tickersEnabled && _cursorTimer != null ) {
1851
1929
// Cannot use _stopCursorTimer because it would reset _cursorActive.
1852
1930
_cursorTimer! .cancel ();
@@ -1946,8 +2024,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
1946
2024
assert (! _hasInputConnection);
1947
2025
_cursorTimer? .cancel ();
1948
2026
_cursorTimer = null ;
1949
- _cursorBlinkOpacityController ? .dispose ();
1950
- _cursorBlinkOpacityController = null ;
2027
+ _backingCursorBlinkOpacityController ? .dispose ();
2028
+ _backingCursorBlinkOpacityController = null ;
1951
2029
_selectionOverlay? .dispose ();
1952
2030
_selectionOverlay = null ;
1953
2031
widget.focusNode.removeListener (_handleFocusChanged);
@@ -2026,8 +2104,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
2026
2104
if (_hasInputConnection) {
2027
2105
// To keep the cursor from blinking while typing, we want to restart the
2028
2106
// cursor timer every time a new character is typed.
2029
- _stopCursorTimer (resetCharTicks: false );
2030
- _startCursorTimer ();
2107
+ _stopCursorBlink (resetCharTicks: false );
2108
+ _startCursorBlink ();
2031
2109
}
2032
2110
}
2033
2111
@@ -2548,8 +2626,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
2548
2626
2549
2627
// To keep the cursor from blinking while it moves, restart the timer here.
2550
2628
if (_cursorTimer != null ) {
2551
- _stopCursorTimer (resetCharTicks: false );
2552
- _startCursorTimer ();
2629
+ _stopCursorBlink (resetCharTicks: false );
2630
+ _startCursorBlink ();
2553
2631
}
2554
2632
}
2555
2633
@@ -2703,14 +2781,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
2703
2781
}
2704
2782
2705
2783
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 ;
2708
2786
}
2709
2787
2710
2788
/// Whether the blinking cursor is actually visible at this precise moment
2711
2789
/// (it's hidden half the time, since it blinks).
2712
2790
@visibleForTesting
2713
- bool get cursorCurrentlyVisible => _cursorBlinkOpacityController! .value > 0 ;
2791
+ bool get cursorCurrentlyVisible => _cursorBlinkOpacityController.value > 0 ;
2714
2792
2715
2793
/// The cursor blink interval (the amount of time the cursor is in the "on"
2716
2794
/// state or the "off" state). A complete cursor blink period is twice this
@@ -2725,83 +2803,69 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
2725
2803
int _obscureShowCharTicksPending = 0 ;
2726
2804
int ? _obscureLatestCharIndex;
2727
2805
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
-
2760
2806
// Indicates whether the cursor should be blinking right now (but it may
2761
2807
// actually not blink because it's disabled via TickerMode.of(context)).
2762
2808
bool _cursorActive = false ;
2763
2809
2764
- void _startCursorTimer () {
2765
- assert (_cursorTimer == null );
2810
+ void _startCursorBlink () {
2811
+ assert (! ( _cursorTimer? .isActive ?? false ) || ! (_backingCursorBlinkOpacityController ? .isAnimating ?? false ) );
2766
2812
_cursorActive = true ;
2767
2813
if (! _tickersEnabled) {
2768
2814
return ;
2769
2815
}
2770
- _targetCursorVisibility = true ;
2771
- _cursorBlinkOpacityController! .value = 1.0 ;
2816
+ _cursorTimer ? . cancel () ;
2817
+ _cursorBlinkOpacityController.value = 1.0 ;
2772
2818
if (EditableText .debugDeterministicCursor) {
2773
2819
return ;
2774
2820
}
2775
2821
if (widget.cursorOpacityAnimates) {
2776
- _cursorTimer = Timer . periodic (_kCursorBlinkWaitForStart, _cursorWaitForStart );
2822
+ _cursorBlinkOpacityController. animateWith (_iosBlinkCursorSimulation). whenComplete (_onCursorTick );
2777
2823
} else {
2778
- _cursorTimer = Timer .periodic (_kCursorBlinkHalfPeriod, _cursorTick );
2824
+ _cursorTimer = Timer .periodic (_kCursorBlinkHalfPeriod, ( Timer timer) { _onCursorTick (); } );
2779
2825
}
2780
2826
}
2781
2827
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 }) {
2783
2852
_cursorActive = false ;
2784
- _cursorTimer? .cancel ();
2785
- _cursorTimer = null ;
2786
- _targetCursorVisibility = false ;
2787
- _cursorBlinkOpacityController! .value = 0.0 ;
2853
+ _cursorBlinkOpacityController.value = 0.0 ;
2788
2854
if (EditableText .debugDeterministicCursor) {
2789
2855
return ;
2790
2856
}
2857
+ _cursorBlinkOpacityController.value = 0.0 ;
2791
2858
if (resetCharTicks) {
2792
2859
_obscureShowCharTicksPending = 0 ;
2793
2860
}
2794
- if (widget.cursorOpacityAnimates) {
2795
- _cursorBlinkOpacityController! .stop ();
2796
- _cursorBlinkOpacityController! .value = 0.0 ;
2797
- }
2798
2861
}
2799
2862
2800
2863
void _startOrStopCursorTimerIfNeeded () {
2801
2864
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 ();
2805
2869
}
2806
2870
}
2807
2871
@@ -3488,8 +3552,10 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
3488
3552
String text = _value.text;
3489
3553
text = widget.obscuringCharacter * text.length;
3490
3554
// 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.
3491
3557
const Set <TargetPlatform > mobilePlatforms = < TargetPlatform > {
3492
- TargetPlatform .android, TargetPlatform .iOS, TargetPlatform . fuchsia,
3558
+ TargetPlatform .android, TargetPlatform .fuchsia,
3493
3559
};
3494
3560
final bool breiflyShowPassword = WidgetsBinding .instance.platformDispatcher.brieflyShowPassword
3495
3561
&& mobilePlatforms.contains (defaultTargetPlatform);
0 commit comments