Unverified Commit c58dca2a authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

Reland "Disable cursor opacity animation on macOS, make iOS cursor animation...

Reland  "Disable cursor opacity animation on macOS, make iOS cursor animation discrete (#104335)" (#106893)
parent 1704d4f5
......@@ -1168,7 +1168,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
forcePressEnabled = false;
textSelectionControls ??= cupertinoDesktopTextSelectionControls;
paintCursorAboveText = true;
cursorOpacityAnimates = true;
cursorOpacityAnimates = false;
cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor;
selectionColor = selectionStyle.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40);
cursorRadius ??= const Radius.circular(2.0);
......
......@@ -52,10 +52,6 @@ typedef AppPrivateCommandCallback = void Function(String, Map<String, dynamic>);
// 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;
......@@ -301,6 +297,91 @@ class ToolbarOptions {
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
......@@ -1606,7 +1687,14 @@ class EditableText extends StatefulWidget {
/// State for a [EditableText].
class EditableTextState extends State<EditableText> with AutomaticKeepAliveClientMixin<EditableText>, WidgetsBindingObserver, TickerProviderStateMixin<EditableText>, TextSelectionDelegate, TextInputClient implements AutofillClient {
Timer? _cursorTimer;
bool _targetCursorVisibility = false;
AnimationController get _cursorBlinkOpacityController {
return _backingCursorBlinkOpacityController ??= AnimationController(
vsync: this,
)..addListener(_onCursorColorTick);
}
AnimationController? _backingCursorBlinkOpacityController;
late final Simulation _iosBlinkCursorSimulation = _DiscreteKeyFrameSimulation.iOSBlinkingCaret();
final ValueNotifier<bool> _cursorVisibilityNotifier = ValueNotifier<bool>(true);
final GlobalKey _editableKey = GlobalKey();
final ClipboardStatusNotifier? _clipboardStatus = kIsWeb ? null : ClipboardStatusNotifier();
......@@ -1617,8 +1705,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
ScrollController? _internalScrollController;
ScrollController get _scrollController => widget.scrollController ?? (_internalScrollController ??= ScrollController());
AnimationController? _cursorBlinkOpacityController;
final LayerLink _toolbarLayerLink = LayerLink();
final LayerLink _startHandleLayerLink = LayerLink();
final LayerLink _endHandleLayerLink = LayerLink();
......@@ -1646,10 +1732,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
/// - 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);
......@@ -1661,7 +1743,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@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;
......@@ -1815,10 +1897,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@override
void initState() {
super.initState();
_cursorBlinkOpacityController = AnimationController(
vsync: this,
duration: _fadeDuration,
)..addListener(_onCursorColorTick);
_clipboardStatus?.addListener(_onChangedClipboardStatus);
widget.controller.addListener(_didChangeTextEditingValue);
widget.focusNode.addListener(_handleFocusChanged);
......@@ -1855,7 +1933,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if (_tickersEnabled != newTickerEnabled) {
_tickersEnabled = newTickerEnabled;
if (_tickersEnabled && _cursorActive) {
_startCursorTimer();
_startCursorBlink();
} else if (!_tickersEnabled && _cursorTimer != null) {
// Cannot use _stopCursorTimer because it would reset _cursorActive.
_cursorTimer!.cancel();
......@@ -1955,8 +2033,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
assert(!_hasInputConnection);
_cursorTimer?.cancel();
_cursorTimer = null;
_cursorBlinkOpacityController?.dispose();
_cursorBlinkOpacityController = null;
_backingCursorBlinkOpacityController?.dispose();
_backingCursorBlinkOpacityController = null;
_selectionOverlay?.dispose();
_selectionOverlay = null;
widget.focusNode.removeListener(_handleFocusChanged);
......@@ -2035,8 +2113,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if (_hasInputConnection) {
// To keep the cursor from blinking while typing, we want to restart the
// cursor timer every time a new character is typed.
_stopCursorTimer(resetCharTicks: false);
_startCursorTimer();
_stopCursorBlink(resetCharTicks: false);
_startCursorBlink();
}
}
......@@ -2557,8 +2635,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// To keep the cursor from blinking while it moves, restart the timer here.
if (_cursorTimer != null) {
_stopCursorTimer(resetCharTicks: false);
_startCursorTimer();
_stopCursorBlink(resetCharTicks: false);
_startCursorBlink();
}
}
......@@ -2712,14 +2790,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
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
......@@ -2734,83 +2812,67 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
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 _startCursorTimer() {
assert(_cursorTimer == null);
void _startCursorBlink() {
assert(!(_cursorTimer?.isActive ?? false) || !(_backingCursorBlinkOpacityController?.isAnimating ?? false));
_cursorActive = true;
if (!_tickersEnabled) {
return;
}
_targetCursorVisibility = true;
_cursorBlinkOpacityController!.value = 1.0;
_cursorTimer?.cancel();
_cursorBlinkOpacityController.value = 1.0;
if (EditableText.debugDeterministicCursor) {
return;
}
if (widget.cursorOpacityAnimates) {
_cursorTimer = Timer.periodic(_kCursorBlinkWaitForStart, _cursorWaitForStart);
_cursorBlinkOpacityController.animateWith(_iosBlinkCursorSimulation).whenComplete(_onCursorTick);
} else {
_cursorTimer = Timer.periodic(_kCursorBlinkHalfPeriod, _cursorTick);
_cursorTimer = Timer.periodic(_kCursorBlinkHalfPeriod, (Timer timer) { _onCursorTick(); });
}
}
void _stopCursorTimer({ bool resetCharTicks = true }) {
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 }) {
_cursorActive = false;
_cursorBlinkOpacityController.value = 0.0;
_cursorTimer?.cancel();
_cursorTimer = null;
_targetCursorVisibility = false;
_cursorBlinkOpacityController!.value = 0.0;
if (EditableText.debugDeterministicCursor) {
return;
}
if (resetCharTicks) {
_obscureShowCharTicksPending = 0;
}
if (widget.cursorOpacityAnimates) {
_cursorBlinkOpacityController!.stop();
_cursorBlinkOpacityController!.value = 0.0;
}
}
void _startOrStopCursorTimerIfNeeded() {
if (_cursorTimer == null && _hasFocus && _value.selection.isCollapsed) {
_startCursorTimer();
} else if (_cursorActive && (!_hasFocus || !_value.selection.isCollapsed)) {
_stopCursorTimer();
_startCursorBlink();
}
else if (_cursorActive && (!_hasFocus || !_value.selection.isCollapsed)) {
_stopCursorBlink();
}
}
......@@ -3497,8 +3559,10 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
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.iOS, TargetPlatform.fuchsia,
TargetPlatform.android, TargetPlatform.fuchsia,
};
final bool breiflyShowPassword = WidgetsBinding.instance.platformDispatcher.brieflyShowPassword
&& mobilePlatforms.contains(defaultTargetPlatform);
......
......@@ -702,40 +702,6 @@ void main() {
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(
......@@ -2998,7 +2964,7 @@ void main() {
// Selection should stay the same since it is set on tap up for mobile platforms.
await touchGesture.down(gPos);
await tester.pumpAndSettle();
await tester.pump();
expect(controller.selection.baseOffset, isTargetPlatformApple ? 4 : 5);
expect(controller.selection.extentOffset, isTargetPlatformApple ? 4 : 5);
......
......@@ -618,42 +618,6 @@ void main() {
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');
......@@ -1340,7 +1304,7 @@ void main() {
editText = (findRenderEditable(tester).text! as TextSpan).text!;
expect(editText.substring(editText.length - 1), '\u2022');
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android }));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }));
testWidgets('desktop obscureText control test', (WidgetTester tester) async {
await tester.pumpWidget(
......@@ -1994,7 +1958,7 @@ void main() {
// Selection should stay the same since it is set on tap up for mobile platforms.
await touchGesture.down(gPos);
await tester.pumpAndSettle();
await tester.pump();
expect(controller.selection.baseOffset, isTargetPlatformApple ? 4 : 5);
expect(controller.selection.extentOffset, isTargetPlatformApple ? 4 : 5);
......
......@@ -166,61 +166,75 @@ void main() {
);
});
testWidgets('Cursor animates', (WidgetTester tester) async {
const Widget widget = MaterialApp(
home: Material(
child: TextField(
maxLines: 3,
testWidgets('Cursor animates on iOS', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: TextField(),
),
),
);
await tester.pumpWidget(widget);
await tester.tap(find.byType(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);
expect(renderEditable.cursorColor!.opacity, 1.0);
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)),
),
);
// Trigger initial timer. When focusing the first time, the cursor shows
// for slightly longer than the average on time.
await tester.tap(find.byType(TextField));
await tester.pump();
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)));
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));
// 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: 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 }));
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 }));
testWidgets('Cursor does not animate on Android', (WidgetTester tester) async {
final Color defaultCursorColor = Color(ThemeData.fallback().colorScheme.primary.value);
......@@ -444,6 +458,37 @@ void main() {
expect(renderEditable, paintsExactlyCountTimes(#drawRect, 0));
});
testWidgets('Cursor does not show when not focused', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/106512 .
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(focusNode: focusNode, autofocus: true),
),
),
);
assert(focusNode.hasFocus);
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
final RenderEditable renderEditable = editableTextState.renderEditable;
focusNode.unfocus();
await tester.pump();
for (int i = 0; i < 10; i += 10) {
// Make sure it does not paint for a period of time.
expect(renderEditable, paintsExactlyCountTimes(#drawRect, 0));
expect(tester.hasRunningAnimations, isFalse);
await tester.pump(const Duration(milliseconds: 29));
}
// Refocus and it should paint the caret.
focusNode.requestFocus();
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(renderEditable, isNot(paintsExactlyCountTimes(#drawRect, 0)));
});
testWidgets('Cursor radius is 2.0', (WidgetTester tester) async {
const Widget widget = MaterialApp(
home: Material(
......@@ -956,4 +1001,40 @@ void main() {
);
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, '•••');
});
}
......@@ -3943,42 +3943,6 @@ void main() {
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(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment