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