mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Disable cursor opacity animation on macOS, make iOS cursor animation discrete (#104335)
This commit is contained in:
parent
18575321bb
commit
60f30e5d3e
@ -1168,7 +1168,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
|
|||||||
forcePressEnabled = false;
|
forcePressEnabled = false;
|
||||||
textSelectionControls ??= cupertinoDesktopTextSelectionControls;
|
textSelectionControls ??= cupertinoDesktopTextSelectionControls;
|
||||||
paintCursorAboveText = true;
|
paintCursorAboveText = true;
|
||||||
cursorOpacityAnimates = true;
|
cursorOpacityAnimates = false;
|
||||||
cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor;
|
cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor;
|
||||||
selectionColor = selectionStyle.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40);
|
selectionColor = selectionStyle.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40);
|
||||||
cursorRadius ??= const Radius.circular(2.0);
|
cursorRadius ??= const Radius.circular(2.0);
|
||||||
|
@ -52,10 +52,6 @@ typedef AppPrivateCommandCallback = void Function(String, Map<String, dynamic>);
|
|||||||
// to transparent, is twice this duration.
|
// to transparent, is twice this duration.
|
||||||
const Duration _kCursorBlinkHalfPeriod = Duration(milliseconds: 500);
|
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
|
// Number of cursor ticks during which the most recently entered character
|
||||||
// is shown in an obscured text field.
|
// is shown in an obscured text field.
|
||||||
const int _kObscureShowLatestCharCursorTicks = 3;
|
const int _kObscureShowLatestCharCursorTicks = 3;
|
||||||
@ -301,6 +297,91 @@ class ToolbarOptions {
|
|||||||
final bool selectAll;
|
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.
|
/// A basic text input field.
|
||||||
///
|
///
|
||||||
/// This widget interacts with the [TextInput] service to let the user edit the
|
/// This widget interacts with the [TextInput] service to let the user edit the
|
||||||
@ -1597,7 +1678,14 @@ class EditableText extends StatefulWidget {
|
|||||||
/// State for a [EditableText].
|
/// State for a [EditableText].
|
||||||
class EditableTextState extends State<EditableText> with AutomaticKeepAliveClientMixin<EditableText>, WidgetsBindingObserver, TickerProviderStateMixin<EditableText>, TextSelectionDelegate implements TextInputClient, AutofillClient {
|
class EditableTextState extends State<EditableText> with AutomaticKeepAliveClientMixin<EditableText>, WidgetsBindingObserver, TickerProviderStateMixin<EditableText>, TextSelectionDelegate implements TextInputClient, AutofillClient {
|
||||||
Timer? _cursorTimer;
|
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 ValueNotifier<bool> _cursorVisibilityNotifier = ValueNotifier<bool>(true);
|
||||||
final GlobalKey _editableKey = GlobalKey();
|
final GlobalKey _editableKey = GlobalKey();
|
||||||
final ClipboardStatusNotifier? _clipboardStatus = kIsWeb ? null : ClipboardStatusNotifier();
|
final ClipboardStatusNotifier? _clipboardStatus = kIsWeb ? null : ClipboardStatusNotifier();
|
||||||
@ -1608,8 +1696,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
ScrollController? _internalScrollController;
|
ScrollController? _internalScrollController;
|
||||||
ScrollController get _scrollController => widget.scrollController ?? (_internalScrollController ??= ScrollController());
|
ScrollController get _scrollController => widget.scrollController ?? (_internalScrollController ??= ScrollController());
|
||||||
|
|
||||||
AnimationController? _cursorBlinkOpacityController;
|
|
||||||
|
|
||||||
final LayerLink _toolbarLayerLink = LayerLink();
|
final LayerLink _toolbarLayerLink = LayerLink();
|
||||||
final LayerLink _startHandleLayerLink = LayerLink();
|
final LayerLink _startHandleLayerLink = LayerLink();
|
||||||
final LayerLink _endHandleLayerLink = LayerLink();
|
final LayerLink _endHandleLayerLink = LayerLink();
|
||||||
@ -1637,10 +1723,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
/// - Changing the selection using a physical keyboard.
|
/// - Changing the selection using a physical keyboard.
|
||||||
bool get _shouldCreateInputConnection => kIsWeb || !widget.readOnly;
|
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
|
// The time it takes for the floating cursor to snap to the text aligned
|
||||||
// cursor position after the user has finished placing it.
|
// cursor position after the user has finished placing it.
|
||||||
static const Duration _floatingCursorResetTime = Duration(milliseconds: 125);
|
static const Duration _floatingCursorResetTime = Duration(milliseconds: 125);
|
||||||
@ -1652,7 +1734,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
@override
|
@override
|
||||||
bool get wantKeepAlive => widget.focusNode.hasFocus;
|
bool get wantKeepAlive => widget.focusNode.hasFocus;
|
||||||
|
|
||||||
Color get _cursorColor => widget.cursorColor.withOpacity(_cursorBlinkOpacityController!.value);
|
Color get _cursorColor => widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly && !widget.obscureText;
|
bool get cutEnabled => widget.toolbarOptions.cut && !widget.readOnly && !widget.obscureText;
|
||||||
@ -1806,10 +1888,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_cursorBlinkOpacityController = AnimationController(
|
|
||||||
vsync: this,
|
|
||||||
duration: _fadeDuration,
|
|
||||||
)..addListener(_onCursorColorTick);
|
|
||||||
_clipboardStatus?.addListener(_onChangedClipboardStatus);
|
_clipboardStatus?.addListener(_onChangedClipboardStatus);
|
||||||
widget.controller.addListener(_didChangeTextEditingValue);
|
widget.controller.addListener(_didChangeTextEditingValue);
|
||||||
widget.focusNode.addListener(_handleFocusChanged);
|
widget.focusNode.addListener(_handleFocusChanged);
|
||||||
@ -1846,7 +1924,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
if (_tickersEnabled != newTickerEnabled) {
|
if (_tickersEnabled != newTickerEnabled) {
|
||||||
_tickersEnabled = newTickerEnabled;
|
_tickersEnabled = newTickerEnabled;
|
||||||
if (_tickersEnabled && _cursorActive) {
|
if (_tickersEnabled && _cursorActive) {
|
||||||
_startCursorTimer();
|
_startCursorBlink();
|
||||||
} else if (!_tickersEnabled && _cursorTimer != null) {
|
} else if (!_tickersEnabled && _cursorTimer != null) {
|
||||||
// Cannot use _stopCursorTimer because it would reset _cursorActive.
|
// Cannot use _stopCursorTimer because it would reset _cursorActive.
|
||||||
_cursorTimer!.cancel();
|
_cursorTimer!.cancel();
|
||||||
@ -1946,8 +2024,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
assert(!_hasInputConnection);
|
assert(!_hasInputConnection);
|
||||||
_cursorTimer?.cancel();
|
_cursorTimer?.cancel();
|
||||||
_cursorTimer = null;
|
_cursorTimer = null;
|
||||||
_cursorBlinkOpacityController?.dispose();
|
_backingCursorBlinkOpacityController?.dispose();
|
||||||
_cursorBlinkOpacityController = null;
|
_backingCursorBlinkOpacityController = null;
|
||||||
_selectionOverlay?.dispose();
|
_selectionOverlay?.dispose();
|
||||||
_selectionOverlay = null;
|
_selectionOverlay = null;
|
||||||
widget.focusNode.removeListener(_handleFocusChanged);
|
widget.focusNode.removeListener(_handleFocusChanged);
|
||||||
@ -2026,8 +2104,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
if (_hasInputConnection) {
|
if (_hasInputConnection) {
|
||||||
// To keep the cursor from blinking while typing, we want to restart the
|
// To keep the cursor from blinking while typing, we want to restart the
|
||||||
// cursor timer every time a new character is typed.
|
// cursor timer every time a new character is typed.
|
||||||
_stopCursorTimer(resetCharTicks: false);
|
_stopCursorBlink(resetCharTicks: false);
|
||||||
_startCursorTimer();
|
_startCursorBlink();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2548,8 +2626,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
|
|
||||||
// To keep the cursor from blinking while it moves, restart the timer here.
|
// To keep the cursor from blinking while it moves, restart the timer here.
|
||||||
if (_cursorTimer != null) {
|
if (_cursorTimer != null) {
|
||||||
_stopCursorTimer(resetCharTicks: false);
|
_stopCursorBlink(resetCharTicks: false);
|
||||||
_startCursorTimer();
|
_startCursorBlink();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2703,14 +2781,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onCursorColorTick() {
|
void _onCursorColorTick() {
|
||||||
renderEditable.cursorColor = widget.cursorColor.withOpacity(_cursorBlinkOpacityController!.value);
|
renderEditable.cursorColor = widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value);
|
||||||
_cursorVisibilityNotifier.value = widget.showCursor && _cursorBlinkOpacityController!.value > 0;
|
_cursorVisibilityNotifier.value = widget.showCursor && _cursorBlinkOpacityController.value > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the blinking cursor is actually visible at this precise moment
|
/// Whether the blinking cursor is actually visible at this precise moment
|
||||||
/// (it's hidden half the time, since it blinks).
|
/// (it's hidden half the time, since it blinks).
|
||||||
@visibleForTesting
|
@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"
|
/// 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
|
/// state or the "off" state). A complete cursor blink period is twice this
|
||||||
@ -2725,83 +2803,69 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
int _obscureShowCharTicksPending = 0;
|
int _obscureShowCharTicksPending = 0;
|
||||||
int? _obscureLatestCharIndex;
|
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
|
// Indicates whether the cursor should be blinking right now (but it may
|
||||||
// actually not blink because it's disabled via TickerMode.of(context)).
|
// actually not blink because it's disabled via TickerMode.of(context)).
|
||||||
bool _cursorActive = false;
|
bool _cursorActive = false;
|
||||||
|
|
||||||
void _startCursorTimer() {
|
void _startCursorBlink() {
|
||||||
assert(_cursorTimer == null);
|
assert(!(_cursorTimer?.isActive ?? false) || !(_backingCursorBlinkOpacityController?.isAnimating ?? false));
|
||||||
_cursorActive = true;
|
_cursorActive = true;
|
||||||
if (!_tickersEnabled) {
|
if (!_tickersEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_targetCursorVisibility = true;
|
_cursorTimer?.cancel();
|
||||||
_cursorBlinkOpacityController!.value = 1.0;
|
_cursorBlinkOpacityController.value = 1.0;
|
||||||
if (EditableText.debugDeterministicCursor) {
|
if (EditableText.debugDeterministicCursor) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (widget.cursorOpacityAnimates) {
|
if (widget.cursorOpacityAnimates) {
|
||||||
_cursorTimer = Timer.periodic(_kCursorBlinkWaitForStart, _cursorWaitForStart);
|
_cursorBlinkOpacityController.animateWith(_iosBlinkCursorSimulation).whenComplete(_onCursorTick);
|
||||||
} else {
|
} 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;
|
_cursorActive = false;
|
||||||
_cursorTimer?.cancel();
|
_cursorBlinkOpacityController.value = 0.0;
|
||||||
_cursorTimer = null;
|
|
||||||
_targetCursorVisibility = false;
|
|
||||||
_cursorBlinkOpacityController!.value = 0.0;
|
|
||||||
if (EditableText.debugDeterministicCursor) {
|
if (EditableText.debugDeterministicCursor) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
_cursorBlinkOpacityController.value = 0.0;
|
||||||
if (resetCharTicks) {
|
if (resetCharTicks) {
|
||||||
_obscureShowCharTicksPending = 0;
|
_obscureShowCharTicksPending = 0;
|
||||||
}
|
}
|
||||||
if (widget.cursorOpacityAnimates) {
|
|
||||||
_cursorBlinkOpacityController!.stop();
|
|
||||||
_cursorBlinkOpacityController!.value = 0.0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _startOrStopCursorTimerIfNeeded() {
|
void _startOrStopCursorTimerIfNeeded() {
|
||||||
if (_cursorTimer == null && _hasFocus && _value.selection.isCollapsed) {
|
if (_cursorTimer == null && _hasFocus && _value.selection.isCollapsed) {
|
||||||
_startCursorTimer();
|
_startCursorBlink();
|
||||||
} else if (_cursorActive && (!_hasFocus || !_value.selection.isCollapsed)) {
|
}
|
||||||
_stopCursorTimer();
|
else if (_cursorActive && (!_hasFocus || !_value.selection.isCollapsed)) {
|
||||||
|
_stopCursorBlink();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3488,8 +3552,10 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
String text = _value.text;
|
String text = _value.text;
|
||||||
text = widget.obscuringCharacter * text.length;
|
text = widget.obscuringCharacter * text.length;
|
||||||
// Reveal the latest character in an obscured field only on mobile.
|
// 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> {
|
const Set<TargetPlatform> mobilePlatforms = <TargetPlatform> {
|
||||||
TargetPlatform.android, TargetPlatform.iOS, TargetPlatform.fuchsia,
|
TargetPlatform.android, TargetPlatform.fuchsia,
|
||||||
};
|
};
|
||||||
final bool breiflyShowPassword = WidgetsBinding.instance.platformDispatcher.brieflyShowPassword
|
final bool breiflyShowPassword = WidgetsBinding.instance.platformDispatcher.brieflyShowPassword
|
||||||
&& mobilePlatforms.contains(defaultTargetPlatform);
|
&& mobilePlatforms.contains(defaultTargetPlatform);
|
||||||
|
@ -702,40 +702,6 @@ void main() {
|
|||||||
expect(editableText.cursorOffset, const Offset(-2.0 / 3.0, 0));
|
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 {
|
testWidgets('Cursor radius is 2.0', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
const CupertinoApp(
|
const CupertinoApp(
|
||||||
|
@ -609,42 +609,6 @@ void main() {
|
|||||||
await checkCursorToggle();
|
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.
|
// Regression test for https://github.com/flutter/flutter/issues/78918.
|
||||||
testWidgets('RenderEditable sets correct text editing value', (WidgetTester tester) async {
|
testWidgets('RenderEditable sets correct text editing value', (WidgetTester tester) async {
|
||||||
final TextEditingController controller = TextEditingController(text: 'how are you');
|
final TextEditingController controller = TextEditingController(text: 'how are you');
|
||||||
@ -1328,7 +1292,7 @@ void main() {
|
|||||||
|
|
||||||
editText = (findRenderEditable(tester).text! as TextSpan).text!;
|
editText = (findRenderEditable(tester).text! as TextSpan).text!;
|
||||||
expect(editText.substring(editText.length - 1), '\u2022');
|
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 {
|
testWidgets('desktop obscureText control test', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
|
@ -166,61 +166,75 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Cursor animates', (WidgetTester tester) async {
|
testWidgets('Cursor animates on iOS', (WidgetTester tester) async {
|
||||||
const Widget widget = MaterialApp(
|
await tester.pumpWidget(
|
||||||
home: Material(
|
const MaterialApp(
|
||||||
child: TextField(
|
home: Material(
|
||||||
maxLines: 3,
|
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();
|
await tester.pump();
|
||||||
|
|
||||||
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
|
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
|
||||||
final RenderEditable renderEditable = editableTextState.renderEditable;
|
final RenderEditable renderEditable = editableTextState.renderEditable;
|
||||||
|
|
||||||
expect(renderEditable.cursorColor!.alpha, 255);
|
expect(renderEditable.cursorColor!.opacity, 1.0);
|
||||||
|
|
||||||
// Trigger initial timer. When focusing the first time, the cursor shows
|
int walltimeMicrosecond = 0;
|
||||||
// for slightly longer than the average on time.
|
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));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.pump(const Duration(milliseconds: 200));
|
// Wait for the current animation to finish. If the cursor never stops its
|
||||||
// Start timing standard cursor show period.
|
// blinking animation the test will timeout.
|
||||||
expect(renderEditable.cursorColor!.alpha, 255);
|
await tester.pumpAndSettle();
|
||||||
expect(renderEditable, paints..rrect(color: const Color(0xff2196f3)));
|
|
||||||
|
|
||||||
await tester.pump(const Duration(milliseconds: 500));
|
for (int i = 0; i < 40; i += 1) {
|
||||||
// Start to animate the cursor away.
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
expect(renderEditable.cursorColor!.alpha, 255);
|
expect(tester.hasRunningAnimations, false);
|
||||||
expect(renderEditable, paints..rrect(color: const Color(0xff2196f3)));
|
}
|
||||||
|
}, variant: TargetPlatformVariant(TargetPlatform.values.toSet()..remove(TargetPlatform.iOS)));
|
||||||
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 {
|
testWidgets('Cursor does not animate on Android', (WidgetTester tester) async {
|
||||||
final Color defaultCursorColor = Color(ThemeData.fallback().colorScheme.primary.value);
|
final Color defaultCursorColor = Color(ThemeData.fallback().colorScheme.primary.value);
|
||||||
@ -956,4 +970,40 @@ void main() {
|
|||||||
);
|
);
|
||||||
EditableText.debugDeterministicCursor = false;
|
EditableText.debugDeterministicCursor = false;
|
||||||
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
|
}, 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, '•••');
|
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', () {
|
group('a11y copy/cut/paste', () {
|
||||||
Future<void> buildApp(MockTextSelectionControls controls, WidgetTester tester) {
|
Future<void> buildApp(MockTextSelectionControls controls, WidgetTester tester) {
|
||||||
return tester.pumpWidget(MaterialApp(
|
return tester.pumpWidget(MaterialApp(
|
||||||
|
Loading…
Reference in New Issue
Block a user