Revert "Disable cursor opacity animation on macOS, make iOS cursor animation discrete (#104335)" (#106762) (#106766)
Co-authored-by: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com>
diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart
index a7abaa8..451cf66 100644
--- a/packages/flutter/lib/src/material/text_field.dart
+++ b/packages/flutter/lib/src/material/text_field.dart
@@ -1168,7 +1168,7 @@
forcePressEnabled = false;
textSelectionControls ??= cupertinoDesktopTextSelectionControls;
paintCursorAboveText = true;
- cursorOpacityAnimates = false;
+ cursorOpacityAnimates = true;
cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor;
selectionColor = selectionStyle.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40);
cursorRadius ??= const Radius.circular(2.0);
diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart
index 640a7c1..9e83244 100644
--- a/packages/flutter/lib/src/widgets/editable_text.dart
+++ b/packages/flutter/lib/src/widgets/editable_text.dart
@@ -52,6 +52,10 @@
// to transparent, is twice this duration.
const Duration _kCursorBlinkHalfPeriod = Duration(milliseconds: 500);
+// The time the cursor is static in opacity before animating to become
+// transparent.
+const Duration _kCursorBlinkWaitForStart = Duration(milliseconds: 150);
+
// Number of cursor ticks during which the most recently entered character
// is shown in an obscured text field.
const int _kObscureShowLatestCharCursorTicks = 3;
@@ -297,91 +301,6 @@
final bool selectAll;
}
-// A time-value pair that represents a key frame in an animation.
-class _KeyFrame {
- const _KeyFrame(this.time, this.value);
- // Values extracted from iOS 15.4 UIKit.
- static const List<_KeyFrame> iOSBlinkingCaretKeyFrames = <_KeyFrame>[
- _KeyFrame(0, 1), // 0
- _KeyFrame(0.5, 1), // 1
- _KeyFrame(0.5375, 0.75), // 2
- _KeyFrame(0.575, 0.5), // 3
- _KeyFrame(0.6125, 0.25), // 4
- _KeyFrame(0.65, 0), // 5
- _KeyFrame(0.85, 0), // 6
- _KeyFrame(0.8875, 0.25), // 7
- _KeyFrame(0.925, 0.5), // 8
- _KeyFrame(0.9625, 0.75), // 9
- _KeyFrame(1, 1), // 10
- ];
-
- // The timing, in seconds, of the specified animation `value`.
- final double time;
- final double value;
-}
-
-class _DiscreteKeyFrameSimulation extends Simulation {
- _DiscreteKeyFrameSimulation.iOSBlinkingCaret() : this._(_KeyFrame.iOSBlinkingCaretKeyFrames, 1);
- _DiscreteKeyFrameSimulation._(this._keyFrames, this.maxDuration)
- : assert(_keyFrames.isNotEmpty),
- assert(_keyFrames.last.time <= maxDuration),
- assert(() {
- for (int i = 0; i < _keyFrames.length -1; i += 1) {
- if (_keyFrames[i].time > _keyFrames[i + 1].time) {
- return false;
- }
- }
- return true;
- }(), 'The key frame sequence must be sorted by time.');
-
- final double maxDuration;
-
- final List<_KeyFrame> _keyFrames;
-
- @override
- double dx(double time) => 0;
-
- @override
- bool isDone(double time) => time >= maxDuration;
-
- // The index of the KeyFrame corresponds to the most recent input `time`.
- int _lastKeyFrameIndex = 0;
-
- @override
- double x(double time) {
- final int length = _keyFrames.length;
-
- // Perform a linear search in the sorted key frame list, starting from the
- // last key frame found, since the input `time` usually monotonically
- // increases by a small amount.
- int searchIndex;
- final int endIndex;
- if (_keyFrames[_lastKeyFrameIndex].time > time) {
- // The simulation may have restarted. Search within the index range
- // [0, _lastKeyFrameIndex).
- searchIndex = 0;
- endIndex = _lastKeyFrameIndex;
- } else {
- searchIndex = _lastKeyFrameIndex;
- endIndex = length;
- }
-
- // Find the target key frame. Don't have to check (endIndex - 1): if
- // (endIndex - 2) doesn't work we'll have to pick (endIndex - 1) anyways.
- while (searchIndex < endIndex - 1) {
- assert(_keyFrames[searchIndex].time <= time);
- final _KeyFrame next = _keyFrames[searchIndex + 1];
- if (time < next.time) {
- break;
- }
- searchIndex += 1;
- }
-
- _lastKeyFrameIndex = searchIndex;
- return _keyFrames[_lastKeyFrameIndex].value;
- }
-}
-
/// A basic text input field.
///
/// This widget interacts with the [TextInput] service to let the user edit the
@@ -1678,14 +1597,7 @@
/// State for a [EditableText].
class EditableTextState extends State<EditableText> with AutomaticKeepAliveClientMixin<EditableText>, WidgetsBindingObserver, TickerProviderStateMixin<EditableText>, TextSelectionDelegate implements TextInputClient, AutofillClient {
Timer? _cursorTimer;
- AnimationController get _cursorBlinkOpacityController {
- return _backingCursorBlinkOpacityController ??= AnimationController(
- vsync: this,
- )..addListener(_onCursorColorTick);
- }
- AnimationController? _backingCursorBlinkOpacityController;
- late final Simulation _iosBlinkCursorSimulation = _DiscreteKeyFrameSimulation.iOSBlinkingCaret();
-
+ bool _targetCursorVisibility = false;
final ValueNotifier<bool> _cursorVisibilityNotifier = ValueNotifier<bool>(true);
final GlobalKey _editableKey = GlobalKey();
final ClipboardStatusNotifier? _clipboardStatus = kIsWeb ? null : ClipboardStatusNotifier();
@@ -1696,6 +1608,8 @@
ScrollController? _internalScrollController;
ScrollController get _scrollController => widget.scrollController ?? (_internalScrollController ??= ScrollController());
+ AnimationController? _cursorBlinkOpacityController;
+
final LayerLink _toolbarLayerLink = LayerLink();
final LayerLink _startHandleLayerLink = LayerLink();
final LayerLink _endHandleLayerLink = LayerLink();
@@ -1723,6 +1637,10 @@
/// - Changing the selection using a physical keyboard.
bool get _shouldCreateInputConnection => kIsWeb || !widget.readOnly;
+ // This value is an eyeball estimation of the time it takes for the iOS cursor
+ // to ease in and out.
+ static const Duration _fadeDuration = Duration(milliseconds: 250);
+
// The time it takes for the floating cursor to snap to the text aligned
// cursor position after the user has finished placing it.
static const Duration _floatingCursorResetTime = Duration(milliseconds: 125);
@@ -1734,7 +1652,7 @@
@override
bool get wantKeepAlive => widget.focusNode.hasFocus;
- Color get _cursorColor => widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value);
+ Color get _cursorColor => widget.cursorColor.withOpacity(_cursorBlinkOpacityController!.value);
@override
bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly && !widget.obscureText;
@@ -1888,6 +1806,10 @@
@override
void initState() {
super.initState();
+ _cursorBlinkOpacityController = AnimationController(
+ vsync: this,
+ duration: _fadeDuration,
+ )..addListener(_onCursorColorTick);
_clipboardStatus?.addListener(_onChangedClipboardStatus);
widget.controller.addListener(_didChangeTextEditingValue);
widget.focusNode.addListener(_handleFocusChanged);
@@ -1924,7 +1846,7 @@
if (_tickersEnabled != newTickerEnabled) {
_tickersEnabled = newTickerEnabled;
if (_tickersEnabled && _cursorActive) {
- _startCursorBlink();
+ _startCursorTimer();
} else if (!_tickersEnabled && _cursorTimer != null) {
// Cannot use _stopCursorTimer because it would reset _cursorActive.
_cursorTimer!.cancel();
@@ -2024,8 +1946,8 @@
assert(!_hasInputConnection);
_cursorTimer?.cancel();
_cursorTimer = null;
- _backingCursorBlinkOpacityController?.dispose();
- _backingCursorBlinkOpacityController = null;
+ _cursorBlinkOpacityController?.dispose();
+ _cursorBlinkOpacityController = null;
_selectionOverlay?.dispose();
_selectionOverlay = null;
widget.focusNode.removeListener(_handleFocusChanged);
@@ -2104,8 +2026,8 @@
if (_hasInputConnection) {
// To keep the cursor from blinking while typing, we want to restart the
// cursor timer every time a new character is typed.
- _stopCursorBlink(resetCharTicks: false);
- _startCursorBlink();
+ _stopCursorTimer(resetCharTicks: false);
+ _startCursorTimer();
}
}
@@ -2626,8 +2548,8 @@
// To keep the cursor from blinking while it moves, restart the timer here.
if (_cursorTimer != null) {
- _stopCursorBlink(resetCharTicks: false);
- _startCursorBlink();
+ _stopCursorTimer(resetCharTicks: false);
+ _startCursorTimer();
}
}
@@ -2781,14 +2703,14 @@
}
void _onCursorColorTick() {
- renderEditable.cursorColor = widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value);
- _cursorVisibilityNotifier.value = widget.showCursor && _cursorBlinkOpacityController.value > 0;
+ renderEditable.cursorColor = widget.cursorColor.withOpacity(_cursorBlinkOpacityController!.value);
+ _cursorVisibilityNotifier.value = widget.showCursor && _cursorBlinkOpacityController!.value > 0;
}
/// Whether the blinking cursor is actually visible at this precise moment
/// (it's hidden half the time, since it blinks).
@visibleForTesting
- bool get cursorCurrentlyVisible => _cursorBlinkOpacityController.value > 0;
+ bool get cursorCurrentlyVisible => _cursorBlinkOpacityController!.value > 0;
/// The cursor blink interval (the amount of time the cursor is in the "on"
/// state or the "off" state). A complete cursor blink period is twice this
@@ -2803,69 +2725,83 @@
int _obscureShowCharTicksPending = 0;
int? _obscureLatestCharIndex;
+ void _cursorTick(Timer timer) {
+ _targetCursorVisibility = !_targetCursorVisibility;
+ final double targetOpacity = _targetCursorVisibility ? 1.0 : 0.0;
+ if (widget.cursorOpacityAnimates) {
+ // If we want to show the cursor, we will animate the opacity to the value
+ // of 1.0, and likewise if we want to make it disappear, to 0.0. An easing
+ // curve is used for the animation to mimic the aesthetics of the native
+ // iOS cursor.
+ //
+ // These values and curves have been obtained through eyeballing, so are
+ // likely not exactly the same as the values for native iOS.
+ _cursorBlinkOpacityController!.animateTo(targetOpacity, curve: Curves.easeOut);
+ } else {
+ _cursorBlinkOpacityController!.value = targetOpacity;
+ }
+
+ if (_obscureShowCharTicksPending > 0) {
+ setState(() {
+ _obscureShowCharTicksPending = WidgetsBinding.instance.platformDispatcher.brieflyShowPassword
+ ? _obscureShowCharTicksPending - 1
+ : 0;
+ });
+ }
+ }
+
+ void _cursorWaitForStart(Timer timer) {
+ assert(_kCursorBlinkHalfPeriod > _fadeDuration);
+ assert(!EditableText.debugDeterministicCursor);
+ _cursorTimer?.cancel();
+ _cursorTimer = Timer.periodic(_kCursorBlinkHalfPeriod, _cursorTick);
+ }
+
// Indicates whether the cursor should be blinking right now (but it may
// actually not blink because it's disabled via TickerMode.of(context)).
bool _cursorActive = false;
- void _startCursorBlink() {
- assert(!(_cursorTimer?.isActive ?? false) || !(_backingCursorBlinkOpacityController?.isAnimating ?? false));
+ void _startCursorTimer() {
+ assert(_cursorTimer == null);
_cursorActive = true;
if (!_tickersEnabled) {
return;
}
- _cursorTimer?.cancel();
- _cursorBlinkOpacityController.value = 1.0;
+ _targetCursorVisibility = true;
+ _cursorBlinkOpacityController!.value = 1.0;
if (EditableText.debugDeterministicCursor) {
return;
}
if (widget.cursorOpacityAnimates) {
- _cursorBlinkOpacityController.animateWith(_iosBlinkCursorSimulation).whenComplete(_onCursorTick);
+ _cursorTimer = Timer.periodic(_kCursorBlinkWaitForStart, _cursorWaitForStart);
} else {
- _cursorTimer = Timer.periodic(_kCursorBlinkHalfPeriod, (Timer timer) { _onCursorTick(); });
+ _cursorTimer = Timer.periodic(_kCursorBlinkHalfPeriod, _cursorTick);
}
}
- void _onCursorTick() {
- if (_obscureShowCharTicksPending > 0) {
- _obscureShowCharTicksPending = WidgetsBinding.instance.platformDispatcher.brieflyShowPassword
- ? _obscureShowCharTicksPending - 1
- : 0;
- if (_obscureShowCharTicksPending == 0) {
- setState(() { });
- }
- }
-
- if (widget.cursorOpacityAnimates) {
- _cursorTimer?.cancel();
- // Schedule this as an async task to avoid blocking tester.pumpAndSettle
- // indefinitely.
- _cursorTimer = Timer(Duration.zero, () => _cursorBlinkOpacityController.animateWith(_iosBlinkCursorSimulation).whenComplete(_onCursorTick));
- } else {
- if (!(_cursorTimer?.isActive ?? false) && _tickersEnabled) {
- _cursorTimer = Timer.periodic(_kCursorBlinkHalfPeriod, (Timer timer) { _onCursorTick(); });
- }
- _cursorBlinkOpacityController.value = _cursorBlinkOpacityController.value == 0 ? 1 : 0;
- }
- }
-
- void _stopCursorBlink({ bool resetCharTicks = true }) {
+ void _stopCursorTimer({ bool resetCharTicks = true }) {
_cursorActive = false;
- _cursorBlinkOpacityController.value = 0.0;
+ _cursorTimer?.cancel();
+ _cursorTimer = null;
+ _targetCursorVisibility = false;
+ _cursorBlinkOpacityController!.value = 0.0;
if (EditableText.debugDeterministicCursor) {
return;
}
- _cursorBlinkOpacityController.value = 0.0;
if (resetCharTicks) {
_obscureShowCharTicksPending = 0;
}
+ if (widget.cursorOpacityAnimates) {
+ _cursorBlinkOpacityController!.stop();
+ _cursorBlinkOpacityController!.value = 0.0;
+ }
}
void _startOrStopCursorTimerIfNeeded() {
if (_cursorTimer == null && _hasFocus && _value.selection.isCollapsed) {
- _startCursorBlink();
- }
- else if (_cursorActive && (!_hasFocus || !_value.selection.isCollapsed)) {
- _stopCursorBlink();
+ _startCursorTimer();
+ } else if (_cursorActive && (!_hasFocus || !_value.selection.isCollapsed)) {
+ _stopCursorTimer();
}
}
@@ -3552,10 +3488,8 @@
String text = _value.text;
text = widget.obscuringCharacter * text.length;
// Reveal the latest character in an obscured field only on mobile.
- // Newer verions of iOS (iOS 15+) no longer reveal the most recently
- // entered character.
const Set<TargetPlatform> mobilePlatforms = <TargetPlatform> {
- TargetPlatform.android, TargetPlatform.fuchsia,
+ TargetPlatform.android, TargetPlatform.iOS, TargetPlatform.fuchsia,
};
final bool breiflyShowPassword = WidgetsBinding.instance.platformDispatcher.brieflyShowPassword
&& mobilePlatforms.contains(defaultTargetPlatform);
diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart
index 5a1e68d..7b05898 100644
--- a/packages/flutter/test/cupertino/text_field_test.dart
+++ b/packages/flutter/test/cupertino/text_field_test.dart
@@ -702,6 +702,40 @@
expect(editableText.cursorOffset, const Offset(-2.0 / 3.0, 0));
});
+ testWidgets('Cursor animates', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ const CupertinoApp(
+ home: CupertinoTextField(),
+ ),
+ );
+
+ final Finder textFinder = find.byType(CupertinoTextField);
+ await tester.tap(textFinder);
+ await tester.pump();
+
+ final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
+ final RenderEditable renderEditable = editableTextState.renderEditable;
+
+ expect(renderEditable.cursorColor!.alpha, 255);
+
+ await tester.pump(const Duration(milliseconds: 100));
+ await tester.pump(const Duration(milliseconds: 400));
+
+ expect(renderEditable.cursorColor!.alpha, 255);
+
+ await tester.pump(const Duration(milliseconds: 200));
+ await tester.pump(const Duration(milliseconds: 100));
+
+ expect(renderEditable.cursorColor!.alpha, 110);
+
+ await tester.pump(const Duration(milliseconds: 100));
+
+ expect(renderEditable.cursorColor!.alpha, 16);
+ await tester.pump(const Duration(milliseconds: 50));
+
+ expect(renderEditable.cursorColor!.alpha, 0);
+ }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
+
testWidgets('Cursor radius is 2.0', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart
index 517bd67..0b146ad 100644
--- a/packages/flutter/test/material/text_field_test.dart
+++ b/packages/flutter/test/material/text_field_test.dart
@@ -609,6 +609,42 @@
await checkCursorToggle();
});
+ testWidgets('Cursor animates', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ const MaterialApp(
+ home: Material(
+ child: TextField(),
+ ),
+ ),
+ );
+
+ final Finder textFinder = find.byType(TextField);
+ await tester.tap(textFinder);
+ await tester.pump();
+
+ final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
+ final RenderEditable renderEditable = editableTextState.renderEditable;
+
+ expect(renderEditable.cursorColor!.alpha, 255);
+
+ await tester.pump(const Duration(milliseconds: 100));
+ await tester.pump(const Duration(milliseconds: 400));
+
+ expect(renderEditable.cursorColor!.alpha, 255);
+
+ await tester.pump(const Duration(milliseconds: 200));
+ await tester.pump(const Duration(milliseconds: 100));
+
+ expect(renderEditable.cursorColor!.alpha, 110);
+
+ await tester.pump(const Duration(milliseconds: 100));
+
+ expect(renderEditable.cursorColor!.alpha, 16);
+ await tester.pump(const Duration(milliseconds: 50));
+
+ expect(renderEditable.cursorColor!.alpha, 0);
+ }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
+
// Regression test for https://github.com/flutter/flutter/issues/78918.
testWidgets('RenderEditable sets correct text editing value', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(text: 'how are you');
@@ -1292,7 +1328,7 @@
editText = (findRenderEditable(tester).text! as TextSpan).text!;
expect(editText.substring(editText.length - 1), '\u2022');
- }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }));
+ }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android }));
testWidgets('desktop obscureText control test', (WidgetTester tester) async {
await tester.pumpWidget(
diff --git a/packages/flutter/test/widgets/editable_text_cursor_test.dart b/packages/flutter/test/widgets/editable_text_cursor_test.dart
index 99c34cb..0ae6295 100644
--- a/packages/flutter/test/widgets/editable_text_cursor_test.dart
+++ b/packages/flutter/test/widgets/editable_text_cursor_test.dart
@@ -166,75 +166,61 @@
);
});
- testWidgets('Cursor animates on iOS', (WidgetTester tester) async {
- await tester.pumpWidget(
- const MaterialApp(
- home: Material(
- child: TextField(),
+ testWidgets('Cursor animates', (WidgetTester tester) async {
+ const Widget widget = MaterialApp(
+ home: Material(
+ child: TextField(
+ maxLines: 3,
),
),
);
+ await tester.pumpWidget(widget);
- final Finder textFinder = find.byType(TextField);
- await tester.tap(textFinder);
+ await tester.tap(find.byType(TextField));
await tester.pump();
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
final RenderEditable renderEditable = editableTextState.renderEditable;
- expect(renderEditable.cursorColor!.opacity, 1.0);
+ expect(renderEditable.cursorColor!.alpha, 255);
- int walltimeMicrosecond = 0;
- double lastVerifiedOpacity = 1.0;
-
- Future<void> verifyKeyFrame({ required double opacity, required int at }) async {
- const int delta = 1;
- assert(at - delta > walltimeMicrosecond);
- await tester.pump(Duration(microseconds: at - delta - walltimeMicrosecond));
-
- // Instead of verifying the opacity at each key frame, this function
- // verifies the opacity immediately *before* each key frame to avoid
- // fp precision issues.
- expect(
- renderEditable.cursorColor!.opacity,
- closeTo(lastVerifiedOpacity, 0.01),
- reason: 'opacity at ${at-delta} microseconds',
- );
-
- walltimeMicrosecond = at - delta;
- lastVerifiedOpacity = opacity;
- }
-
- await verifyKeyFrame(opacity: 1.0, at: 500000);
- await verifyKeyFrame(opacity: 0.75, at: 537500);
- await verifyKeyFrame(opacity: 0.5, at: 575000);
- await verifyKeyFrame(opacity: 0.25, at: 612500);
- await verifyKeyFrame(opacity: 0.0, at: 650000);
- await verifyKeyFrame(opacity: 0.0, at: 850000);
- await verifyKeyFrame(opacity: 0.25, at: 887500);
- await verifyKeyFrame(opacity: 0.5, at: 925000);
- await verifyKeyFrame(opacity: 0.75, at: 962500);
- await verifyKeyFrame(opacity: 1.0, at: 1000000);
- }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));
-
- testWidgets('Cursor does not animate on non-iOS platforms', (WidgetTester tester) async {
- await tester.pumpWidget(
- const MaterialApp(
- home: Material(child: TextField(maxLines: 3)),
- ),
- );
-
- await tester.tap(find.byType(TextField));
+ // Trigger initial timer. When focusing the first time, the cursor shows
+ // for slightly longer than the average on time.
await tester.pump();
- // Wait for the current animation to finish. If the cursor never stops its
- // blinking animation the test will timeout.
- await tester.pumpAndSettle();
+ await tester.pump(const Duration(milliseconds: 200));
+ // Start timing standard cursor show period.
+ expect(renderEditable.cursorColor!.alpha, 255);
+ expect(renderEditable, paints..rrect(color: const Color(0xff2196f3)));
- for (int i = 0; i < 40; i += 1) {
- await tester.pump(const Duration(milliseconds: 100));
- expect(tester.hasRunningAnimations, false);
- }
- }, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));
+ await tester.pump(const Duration(milliseconds: 500));
+ // Start to animate the cursor away.
+ expect(renderEditable.cursorColor!.alpha, 255);
+ expect(renderEditable, paints..rrect(color: const Color(0xff2196f3)));
+
+ await tester.pump(const Duration(milliseconds: 100));
+ expect(renderEditable.cursorColor!.alpha, 110);
+ expect(renderEditable, paints..rrect(color: const Color(0x6e2196f3)));
+
+ await tester.pump(const Duration(milliseconds: 100));
+ expect(renderEditable.cursorColor!.alpha, 16);
+ expect(renderEditable, paints..rrect(color: const Color(0x102196f3)));
+
+ await tester.pump(const Duration(milliseconds: 100));
+ expect(renderEditable.cursorColor!.alpha, 0);
+ // Don't try to draw the cursor.
+ expect(renderEditable, paintsExactlyCountTimes(#drawRRect, 0));
+
+ // Wait some more while the cursor is gone. It'll trigger the cursor to
+ // start animating in again.
+ await tester.pump(const Duration(milliseconds: 300));
+ expect(renderEditable.cursorColor!.alpha, 0);
+ expect(renderEditable, paintsExactlyCountTimes(#drawRRect, 0));
+
+ await tester.pump(const Duration(milliseconds: 50));
+ // Cursor starts coming back.
+ expect(renderEditable.cursorColor!.alpha, 79);
+ expect(renderEditable, paints..rrect(color: const Color(0x4f2196f3)));
+ }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('Cursor does not animate on Android', (WidgetTester tester) async {
final Color defaultCursorColor = Color(ThemeData.fallback().colorScheme.primary.value);
@@ -970,40 +956,4 @@
);
EditableText.debugDeterministicCursor = false;
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
-
- testWidgets('password briefly does not show last character when disabled by system', (WidgetTester tester) async {
- final bool debugDeterministicCursor = EditableText.debugDeterministicCursor;
- EditableText.debugDeterministicCursor = false;
- addTearDown(() {
- EditableText.debugDeterministicCursor = debugDeterministicCursor;
- });
-
- await tester.pumpWidget(MaterialApp(
- home: EditableText(
- backgroundCursorColor: Colors.grey,
- controller: controller,
- obscureText: true,
- focusNode: focusNode,
- style: textStyle,
- cursorColor: cursorColor,
- ),
- ));
-
- await tester.enterText(find.byType(EditableText), 'AA');
- await tester.pump();
- await tester.enterText(find.byType(EditableText), 'AAA');
- await tester.pump();
-
- tester.binding.platformDispatcher.brieflyShowPasswordTestValue = false;
- addTearDown(() {
- tester.binding.platformDispatcher.brieflyShowPasswordTestValue = true;
- });
- expect((findRenderEditable(tester).text! as TextSpan).text, '••A');
- await tester.pump(const Duration(milliseconds: 500));
- expect((findRenderEditable(tester).text! as TextSpan).text, '•••');
- await tester.pump(const Duration(milliseconds: 500));
- await tester.pump(const Duration(milliseconds: 500));
- await tester.pump(const Duration(milliseconds: 500));
- expect((findRenderEditable(tester).text! as TextSpan).text, '•••');
- });
}
diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart
index 55acff9..3e46034 100644
--- a/packages/flutter/test/widgets/editable_text_test.dart
+++ b/packages/flutter/test/widgets/editable_text_test.dart
@@ -3943,6 +3943,42 @@
expect((findRenderEditable(tester).text! as TextSpan).text, '•••');
});
+ testWidgets('password briefly does not show last character on Android if turned off', (WidgetTester tester) async {
+ final bool debugDeterministicCursor = EditableText.debugDeterministicCursor;
+ EditableText.debugDeterministicCursor = false;
+ addTearDown(() {
+ EditableText.debugDeterministicCursor = debugDeterministicCursor;
+ });
+
+ await tester.pumpWidget(MaterialApp(
+ home: EditableText(
+ backgroundCursorColor: Colors.grey,
+ controller: controller,
+ obscureText: true,
+ focusNode: focusNode,
+ style: textStyle,
+ cursorColor: cursorColor,
+ ),
+ ));
+
+ await tester.enterText(find.byType(EditableText), 'AA');
+ await tester.pump();
+ await tester.enterText(find.byType(EditableText), 'AAA');
+ await tester.pump();
+
+ tester.binding.platformDispatcher.brieflyShowPasswordTestValue = false;
+ addTearDown(() {
+ tester.binding.platformDispatcher.brieflyShowPasswordTestValue = true;
+ });
+ expect((findRenderEditable(tester).text! as TextSpan).text, '••A');
+ await tester.pump(const Duration(milliseconds: 500));
+ expect((findRenderEditable(tester).text! as TextSpan).text, '•••');
+ await tester.pump(const Duration(milliseconds: 500));
+ await tester.pump(const Duration(milliseconds: 500));
+ await tester.pump(const Duration(milliseconds: 500));
+ expect((findRenderEditable(tester).text! as TextSpan).text, '•••');
+ });
+
group('a11y copy/cut/paste', () {
Future<void> buildApp(MockTextSelectionControls controls, WidgetTester tester) {
return tester.pumpWidget(MaterialApp(