Adds a fade in and out, rounds corners, fixes offset and fixes height of cursor on iOS (#24876)

* Fixes cursor on iOS devices
This commit is contained in:
jslavitz 2019-01-28 19:49:19 -08:00 committed by GitHub
parent 9f20bd6cb9
commit d2a2a5cfe2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 977 additions and 363 deletions

View file

@ -1 +1 @@
6fc7ec65d51116c3f83acb5251e57e779af2ebbb b530d67675a5aa9c5458b93019ce91e20ad88758

View file

@ -110,6 +110,11 @@ class CupertinoButton extends StatefulWidget {
/// Defaults to round corners of 8 logical pixels. /// Defaults to round corners of 8 logical pixels.
final BorderRadius borderRadius; final BorderRadius borderRadius;
/// The shape of the button.
///
/// Defaults to a super ellipse with
// final ShapeBorder shape;
final bool _filled; final bool _filled;
/// Whether the button is enabled or disabled. Buttons are disabled by default. To /// Whether the button is enabled or disabled. Buttons are disabled by default. To

View file

@ -35,6 +35,14 @@ const Color _kSelectionHighlightColor = Color(0x667FAACF);
const Color _kInactiveTextColor = Color(0xFFC2C2C2); const Color _kInactiveTextColor = Color(0xFFC2C2C2);
const Color _kDisabledBackground = Color(0xFFFAFAFA); const Color _kDisabledBackground = Color(0xFFFAFAFA);
// An eyeballed value that moves the cursor slightly left of where it is
// rendered for text on Android so it's positioning more accurately matches the
// native iOS text cursor positioning.
//
// This value is in device pixels, not logical pixels as is typically used
// throughout the codebase.
const int _iOSHorizontalCursorOffsetPixels = -2;
/// Visibility of text field overlays based on the state of the current text entry. /// Visibility of text field overlays based on the state of the current text entry.
/// ///
/// Used to toggle the visibility behavior of the optional decorating widgets /// Used to toggle the visibility behavior of the optional decorating widgets
@ -163,7 +171,7 @@ class CupertinoTextField extends StatefulWidget {
this.inputFormatters, this.inputFormatters,
this.enabled, this.enabled,
this.cursorWidth = 2.0, this.cursorWidth = 2.0,
this.cursorRadius, this.cursorRadius = const Radius.circular(2.0),
this.cursorColor = CupertinoColors.activeBlue, this.cursorColor = CupertinoColors.activeBlue,
this.keyboardAppearance, this.keyboardAppearance,
this.scrollPadding = const EdgeInsets.all(20.0), this.scrollPadding = const EdgeInsets.all(20.0),
@ -598,6 +606,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
final TextEditingController controller = _effectiveController; final TextEditingController controller = _effectiveController;
final List<TextInputFormatter> formatters = widget.inputFormatters ?? <TextInputFormatter>[]; final List<TextInputFormatter> formatters = widget.inputFormatters ?? <TextInputFormatter>[];
final bool enabled = widget.enabled ?? true; final bool enabled = widget.enabled ?? true;
final Offset cursorOffset = Offset(_iOSHorizontalCursorOffsetPixels / MediaQuery.of(context).devicePixelRatio, 0);
if (widget.maxLength != null && widget.maxLengthEnforced) { if (widget.maxLength != null && widget.maxLengthEnforced) {
formatters.add(LengthLimitingTextInputFormatter(widget.maxLength)); formatters.add(LengthLimitingTextInputFormatter(widget.maxLength));
} }
@ -631,6 +640,9 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
cursorWidth: widget.cursorWidth, cursorWidth: widget.cursorWidth,
cursorRadius: widget.cursorRadius, cursorRadius: widget.cursorRadius,
cursorColor: widget.cursorColor, cursorColor: widget.cursorColor,
cursorOpacityAnimates: true,
cursorOffset: cursorOffset,
paintCursorAboveText: true,
backgroundCursorColor: CupertinoColors.inactiveGray, backgroundCursorColor: CupertinoColors.inactiveGray,
scrollPadding: widget.scrollPadding, scrollPadding: widget.scrollPadding,
keyboardAppearance: keyboardAppearance, keyboardAppearance: keyboardAppearance,

View file

@ -8,6 +8,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'debug.dart'; import 'debug.dart';
@ -36,6 +37,14 @@ typedef InputCounterWidgetBuilder = Widget Function(
} }
); );
// An eyeballed value that moves the cursor slightly left of where it is
// rendered for text on Android so it's positioning more accurately matches the
// native iOS text cursor positioning.
//
// This value is in device pixels, not logical pixels as is typically used
// throughout the codebase.
const int _iOSHorizontalCursorOffsetPixels = 2;
/// A material design text field. /// A material design text field.
/// ///
/// A text field lets the user enter text, either with hardware keyboard or with /// A text field lets the user enter text, either with hardware keyboard or with
@ -469,6 +478,14 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
&& widget.decoration != null && widget.decoration != null
&& widget.decoration.counterText == null; && widget.decoration.counterText == null;
Radius get _cursorRadius {
if (widget.cursorRadius != null)
return widget.cursorRadius;
if (Theme.of(context).platform == TargetPlatform.iOS)
return const Radius.circular(2.0);
return null;
}
InputDecoration _getEffectiveDecoration() { InputDecoration _getEffectiveDecoration() {
final MaterialLocalizations localizations = MaterialLocalizations.of(context); final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final ThemeData themeData = Theme.of(context); final ThemeData themeData = Theme.of(context);
@ -682,6 +699,22 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
@override @override
bool get wantKeepAlive => _splashes != null && _splashes.isNotEmpty; bool get wantKeepAlive => _splashes != null && _splashes.isNotEmpty;
bool get _cursorOpacityAnimates => Theme.of(context).platform == TargetPlatform.iOS ? true : false;
Offset get _getCursorOffset => Offset(_iOSHorizontalCursorOffsetPixels / MediaQuery.of(context).devicePixelRatio, 0);
bool get _paintCursorAboveText => Theme.of(context).platform == TargetPlatform.iOS ? true : false;
Color get _cursorColor {
if (widget.cursorColor == null) {
if (Theme.of(context).platform == TargetPlatform.iOS)
return CupertinoTheme.of(context).primaryColor;
else
return Theme.of(context).cursorColor;
}
return widget.cursorColor;
}
@override @override
void deactivate() { void deactivate() {
if (_splashes != null) { if (_splashes != null) {
@ -704,7 +737,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
assert(debugCheckHasDirectionality(context)); assert(debugCheckHasDirectionality(context));
assert( assert(
!(widget.style != null && widget.style.inherit == false && !(widget.style != null && widget.style.inherit == false &&
(widget.style.fontSize == null || widget.style.textBaseline == null)), (widget.style.fontSize == null || widget.style.textBaseline == null)),
'inherit false style must supply fontSize and textBaseline', 'inherit false style must supply fontSize and textBaseline',
); );
@ -755,8 +788,11 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
inputFormatters: formatters, inputFormatters: formatters,
rendererIgnoresPointer: true, rendererIgnoresPointer: true,
cursorWidth: widget.cursorWidth, cursorWidth: widget.cursorWidth,
cursorRadius: widget.cursorRadius, cursorRadius: _cursorRadius,
cursorColor: widget.cursorColor ?? themeData.cursorColor, cursorColor: _cursorColor,
cursorOpacityAnimates: _cursorOpacityAnimates,
cursorOffset: _getCursorOffset,
paintCursorAboveText: _paintCursorAboveText,
backgroundCursorColor: CupertinoColors.inactiveGray, backgroundCursorColor: CupertinoColors.inactiveGray,
scrollPadding: widget.scrollPadding, scrollPadding: widget.scrollPadding,
keyboardAppearance: keyboardAppearance, keyboardAppearance: keyboardAppearance,

View file

@ -21,7 +21,7 @@ const double _kCaretHeightOffset = 2.0; // pixels
// The additional size on the x and y axis with which to expand the prototype // The additional size on the x and y axis with which to expand the prototype
// cursor to render the floating cursor in pixels. // cursor to render the floating cursor in pixels.
const Offset _kFloatingCaretSizeIncrease = Offset(0.5, 2.0); const Offset _kFloatingCaretSizeIncrease = Offset(0.5, 1.0);
// The corner radius of the floating cursor in pixels. // The corner radius of the floating cursor in pixels.
const double _kFloatingCaretRadius = 1.0; const double _kFloatingCaretRadius = 1.0;
@ -149,8 +149,11 @@ class RenderEditable extends RenderBox {
Locale locale, Locale locale,
double cursorWidth = 1.0, double cursorWidth = 1.0,
Radius cursorRadius, Radius cursorRadius,
bool paintCursorAboveText = false,
Offset cursorOffset,
double devicePixelRatio = 1.0,
bool enableInteractiveSelection, bool enableInteractiveSelection,
EdgeInsets floatingCursorAddedMargin = const EdgeInsets.fromLTRB(3, 6, 3, 6), EdgeInsets floatingCursorAddedMargin = const EdgeInsets.fromLTRB(4, 4, 4, 5),
@required this.textSelectionDelegate, @required this.textSelectionDelegate,
}) : assert(textAlign != null), }) : assert(textAlign != null),
assert(textDirection != null, 'RenderEditable created without a textDirection.'), assert(textDirection != null, 'RenderEditable created without a textDirection.'),
@ -158,9 +161,11 @@ class RenderEditable extends RenderBox {
assert(textScaleFactor != null), assert(textScaleFactor != null),
assert(offset != null), assert(offset != null),
assert(ignorePointer != null), assert(ignorePointer != null),
assert(paintCursorAboveText != null),
assert(obscureText != null), assert(obscureText != null),
assert(textSelectionDelegate != null), assert(textSelectionDelegate != null),
assert(cursorWidth != null && cursorWidth >= 0.0), assert(cursorWidth != null && cursorWidth >= 0.0),
assert(devicePixelRatio != null),
_textPainter = TextPainter( _textPainter = TextPainter(
text: text, text: text,
textAlign: textAlign, textAlign: textAlign,
@ -178,8 +183,11 @@ class RenderEditable extends RenderBox {
_offset = offset, _offset = offset,
_cursorWidth = cursorWidth, _cursorWidth = cursorWidth,
_cursorRadius = cursorRadius, _cursorRadius = cursorRadius,
_paintCursorOnTop = paintCursorAboveText,
_cursorOffset = cursorOffset,
_floatingCursorAddedMargin = floatingCursorAddedMargin, _floatingCursorAddedMargin = floatingCursorAddedMargin,
_enableInteractiveSelection = enableInteractiveSelection, _enableInteractiveSelection = enableInteractiveSelection,
_devicePixelRatio = devicePixelRatio,
_obscureText = obscureText { _obscureText = obscureText {
assert(_showCursor != null); assert(_showCursor != null);
assert(!_showCursor.value || cursorColor != null); assert(!_showCursor.value || cursorColor != null);
@ -208,6 +216,18 @@ class RenderEditable extends RenderBox {
/// The default value of this property is false. /// The default value of this property is false.
bool ignorePointer; bool ignorePointer;
/// The pixel ratio of the current device.
///
/// Should be obtained by querying MediaQuery for the devicePixelRatio.
double get devicePixelRatio => _devicePixelRatio;
double _devicePixelRatio;
set devicePixelRatio(double value) {
if (devicePixelRatio == value)
return;
_devicePixelRatio = value;
markNeedsTextLayout();
}
/// Whether to hide the text being edited (e.g., for passwords). /// Whether to hide the text being edited (e.g., for passwords).
bool get obscureText => _obscureText; bool get obscureText => _obscureText;
bool _obscureText; bool _obscureText;
@ -719,6 +739,38 @@ class RenderEditable extends RenderBox {
markNeedsLayout(); markNeedsLayout();
} }
///{@template flutter.rendering.editable.paintCursorOnTop}
/// If the cursor should be painted on top of the text or underneath it.
///
/// By default, the cursor should be painted on top for iOS platforms and
/// underneath for Android platforms.
/// {@end template}
bool get paintCursorAboveText => _paintCursorOnTop;
bool _paintCursorOnTop;
set paintCursorAboveText(bool value) {
if (_paintCursorOnTop == value)
return;
_paintCursorOnTop = value;
markNeedsLayout();
}
/// {@template flutter.rendering.editable.cursorOffset}
/// The offset that is used, in pixels, when painting the cursor on screen.
///
/// By default, the cursor position should be set to an offset of
/// (-[cursorWidth] * 0.5, 0.0) on iOS platforms and (0, 0) on Android
/// platforms. The origin from where the offset is applied to is the arbitrary
/// location where the cursor ends up being rendered from by default.
/// {@end template}
Offset get cursorOffset => _cursorOffset;
Offset _cursorOffset;
set cursorOffset(Offset value) {
if (_cursorOffset == value)
return;
_cursorOffset = value;
markNeedsLayout();
}
/// How rounded the corners of the cursor should be. /// How rounded the corners of the cursor should be.
Radius get cursorRadius => _cursorRadius; Radius get cursorRadius => _cursorRadius;
Radius _cursorRadius; Radius _cursorRadius;
@ -732,7 +784,7 @@ class RenderEditable extends RenderBox {
/// The padding applied to text field. Used to determine the bounds when /// The padding applied to text field. Used to determine the bounds when
/// moving the floating cursor. /// moving the floating cursor.
/// ///
/// Defaults to a padding with left, right set to 3 and top, bottom to 6. /// Defaults to a padding with left, top and right set to 4, bottom to 5.
EdgeInsets get floatingCursorAddedMargin => _floatingCursorAddedMargin; EdgeInsets get floatingCursorAddedMargin => _floatingCursorAddedMargin;
EdgeInsets _floatingCursorAddedMargin; EdgeInsets _floatingCursorAddedMargin;
set floatingCursorAddedMargin(EdgeInsets value) { set floatingCursorAddedMargin(EdgeInsets value) {
@ -1055,7 +1107,12 @@ class RenderEditable extends RenderBox {
_layoutText(constraints.maxWidth); _layoutText(constraints.maxWidth);
final Offset caretOffset = _textPainter.getOffsetForCaret(caretPosition, _caretPrototype); final Offset caretOffset = _textPainter.getOffsetForCaret(caretPosition, _caretPrototype);
// This rect is the same as _caretPrototype but without the vertical padding. // This rect is the same as _caretPrototype but without the vertical padding.
return Rect.fromLTWH(0.0, 0.0, cursorWidth, preferredLineHeight).shift(caretOffset + _paintOffset); Rect rect = Rect.fromLTWH(0.0, 0.0, cursorWidth, preferredLineHeight).shift(caretOffset + _paintOffset);
// Add additional cursor offset (generally only if on iOS).
if (_cursorOffset != null)
rect = rect.shift(_cursorOffset);
return rect.shift(_getPixelPerfectCursorOffset(rect));
} }
@override @override
@ -1262,10 +1319,21 @@ class RenderEditable extends RenderBox {
_textLayoutLastWidth = constraintWidth; _textLayoutLastWidth = constraintWidth;
} }
/// On iOS, the cursor is taller than the the cursor on Android. The height
/// of the cursor for iOS is approximate and obtained through an eyeball
/// comparison.
Rect get _getCaretPrototype {
switch(defaultTargetPlatform){
case TargetPlatform.iOS:
return Rect.fromLTWH(0.0, -_kCaretHeightOffset + .5, cursorWidth, preferredLineHeight + 2);
default:
return Rect.fromLTWH(0.0, _kCaretHeightOffset, cursorWidth, preferredLineHeight - 2.0 * _kCaretHeightOffset);
}
}
@override @override
void performLayout() { void performLayout() {
_layoutText(constraints.maxWidth); _layoutText(constraints.maxWidth);
_caretPrototype = Rect.fromLTWH(0.0, _kCaretHeightOffset, cursorWidth, preferredLineHeight - 2.0 * _kCaretHeightOffset); _caretPrototype = _getCaretPrototype;
_selectionRects = null; _selectionRects = null;
// We grab _textPainter.size here because assigning to `size` on the next // We grab _textPainter.size here because assigning to `size` on the next
// line will trigger us to validate our intrinsic sizes, which will change // line will trigger us to validate our intrinsic sizes, which will change
@ -1283,15 +1351,30 @@ class RenderEditable extends RenderBox {
offset.applyContentDimensions(0.0, _maxScrollExtent); offset.applyContentDimensions(0.0, _maxScrollExtent);
} }
Offset _getPixelPerfectCursorOffset(Rect caretRect) {
final Offset caretPosition = localToGlobal(caretRect.topLeft);
final double pixelMultiple = 1.0 / _devicePixelRatio;
final int quotientX = (caretPosition.dx / pixelMultiple).round();
final int quotientY = (caretPosition.dy / pixelMultiple).round();
final double pixelPerfectOffsetX = quotientX * pixelMultiple - caretPosition.dx;
final double pixelPerfectOffsetY = quotientY * pixelMultiple - caretPosition.dy;
return Offset(pixelPerfectOffsetX, pixelPerfectOffsetY);
}
void _paintCaret(Canvas canvas, Offset effectiveOffset, TextPosition textPosition) { void _paintCaret(Canvas canvas, Offset effectiveOffset, TextPosition textPosition) {
assert(_textLayoutLastWidth == constraints.maxWidth); assert(_textLayoutLastWidth == constraints.maxWidth);
final Offset caretOffset = _textPainter.getOffsetForCaret(textPosition, _caretPrototype); final Offset caretOffset = _textPainter.getOffsetForCaret(textPosition, _caretPrototype);
// If the floating cursor is enabled, the text cursor's color is [backgroundCursorColor] while // If the floating cursor is enabled, the text cursor's color is [backgroundCursorColor] while
// the floating cursor's color is _cursorColor; // the floating cursor's color is _cursorColor;
final Paint paint = Paint() final Paint paint = Paint()
..color = _floatingCursorOn ? backgroundCursorColor : _cursorColor; ..color = _floatingCursorOn ? backgroundCursorColor : _cursorColor;
final Rect caretRect = _caretPrototype.shift(caretOffset + effectiveOffset); Rect caretRect = _caretPrototype.shift(caretOffset + effectiveOffset);
if (_cursorOffset != null)
caretRect = caretRect.shift(_cursorOffset);
caretRect = caretRect.shift(_getPixelPerfectCursorOffset(caretRect));
if (cursorRadius == null) { if (cursorRadius == null) {
canvas.drawRect(caretRect, paint); canvas.drawRect(caretRect, paint);
@ -1334,7 +1417,8 @@ class RenderEditable extends RenderBox {
assert(_textLayoutLastWidth == constraints.maxWidth); assert(_textLayoutLastWidth == constraints.maxWidth);
assert(_floatingCursorOn); assert(_floatingCursorOn);
final Paint paint = Paint()..color = _cursorColor; // We always want the floating cursor to render at full opacity.
final Paint paint = Paint()..color = _cursorColor.withOpacity(0.75);
double sizeAdjustmentX = _kFloatingCaretSizeIncrease.dx; double sizeAdjustmentX = _kFloatingCaretSizeIncrease.dx;
double sizeAdjustmentY = _kFloatingCaretSizeIncrease.dy; double sizeAdjustmentY = _kFloatingCaretSizeIncrease.dy;
@ -1344,10 +1428,13 @@ class RenderEditable extends RenderBox {
sizeAdjustmentY = ui.lerpDouble(sizeAdjustmentY, 0, _resetFloatingCursorAnimationValue); sizeAdjustmentY = ui.lerpDouble(sizeAdjustmentY, 0, _resetFloatingCursorAnimationValue);
} }
final Rect floatingCaretPrototype = Rect.fromLTRB(_caretPrototype.left - sizeAdjustmentX, final Rect floatingCaretPrototype = Rect.fromLTRB(
_caretPrototype.top - sizeAdjustmentY, _caretPrototype.left - sizeAdjustmentX,
_caretPrototype.right + sizeAdjustmentX, _caretPrototype.top - sizeAdjustmentY,
_caretPrototype.bottom + sizeAdjustmentY); _caretPrototype.right + sizeAdjustmentX,
_caretPrototype.bottom + sizeAdjustmentY
);
final Rect caretRect = floatingCaretPrototype.shift(effectiveOffset); final Rect caretRect = floatingCaretPrototype.shift(effectiveOffset);
const Radius floatingCursorRadius = Radius.circular(_kFloatingCaretRadius); const Radius floatingCursorRadius = Radius.circular(_kFloatingCaretRadius);
final RRect caretRRect = RRect.fromRectAndRadius(caretRect, floatingCursorRadius); final RRect caretRRect = RRect.fromRectAndRadius(caretRect, floatingCursorRadius);
@ -1424,15 +1511,24 @@ class RenderEditable extends RenderBox {
void _paintContents(PaintingContext context, Offset offset) { void _paintContents(PaintingContext context, Offset offset) {
assert(_textLayoutLastWidth == constraints.maxWidth); assert(_textLayoutLastWidth == constraints.maxWidth);
final Offset effectiveOffset = offset + _paintOffset; final Offset effectiveOffset = offset + _paintOffset;
// On iOS, the cursor is painted over the text, on Android, it's painted
// under it.
if (paintCursorAboveText)
_textPainter.paint(context.canvas, effectiveOffset);
if (_selection != null && !_floatingCursorOn) { if (_selection != null && !_floatingCursorOn) {
if (_selection.isCollapsed && _showCursor.value && cursorColor != null) { if (_selection.isCollapsed && cursorColor != null && _hasFocus) {
_paintCaret(context.canvas, effectiveOffset, _selection.extent); _paintCaret(context.canvas, effectiveOffset, _selection.extent);
} else if (!_selection.isCollapsed && _selectionColor != null) { } else if (!_selection.isCollapsed && _selectionColor != null) {
_selectionRects ??= _textPainter.getBoxesForSelection(_selection); _selectionRects ??= _textPainter.getBoxesForSelection(_selection);
_paintSelection(context.canvas, effectiveOffset); _paintSelection(context.canvas, effectiveOffset);
} }
} }
_textPainter.paint(context.canvas, effectiveOffset);
if (!paintCursorAboveText)
_textPainter.paint(context.canvas, effectiveOffset);
if (_floatingCursorOn) { if (_floatingCursorOn) {
if (_resetFloatingCursorAnimationValue == null) if (_resetFloatingCursorAnimationValue == null)
_paintCaret(context.canvas, effectiveOffset, _floatingCursorTextPosition); _paintCaret(context.canvas, effectiveOffset, _floatingCursorTextPosition);

View file

@ -5,6 +5,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -31,8 +32,15 @@ export 'package:flutter/rendering.dart' show SelectionChangedCause;
/// (including the cursor location). /// (including the cursor location).
typedef SelectionChangedCallback = void Function(TextSelection selection, SelectionChangedCause cause); typedef SelectionChangedCallback = void Function(TextSelection selection, SelectionChangedCause cause);
// The time it takes for the cursor to fade from fully opaque to fully
// transparent and vice versa. A full cursor blink, from transparent to opaque
// 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;
@ -215,6 +223,9 @@ class EditableText extends StatefulWidget {
this.rendererIgnoresPointer = false, this.rendererIgnoresPointer = false,
this.cursorWidth = 2.0, this.cursorWidth = 2.0,
this.cursorRadius, this.cursorRadius,
this.cursorOpacityAnimates = false,
this.cursorOffset,
this.paintCursorAboveText = false,
this.scrollPadding = const EdgeInsets.all(20.0), this.scrollPadding = const EdgeInsets.all(20.0),
this.keyboardAppearance = Brightness.light, this.keyboardAppearance = Brightness.light,
this.dragStartBehavior = DragStartBehavior.down, this.dragStartBehavior = DragStartBehavior.down,
@ -225,6 +236,8 @@ class EditableText extends StatefulWidget {
assert(autocorrect != null), assert(autocorrect != null),
assert(style != null), assert(style != null),
assert(cursorColor != null), assert(cursorColor != null),
assert(cursorOpacityAnimates != null),
assert(paintCursorAboveText != null),
assert(backgroundCursorColor != null), assert(backgroundCursorColor != null),
assert(textAlign != null), assert(textAlign != null),
assert(maxLines == null || maxLines > 0), assert(maxLines == null || maxLines > 0),
@ -471,6 +484,19 @@ class EditableText extends StatefulWidget {
/// {@endtemplate} /// {@endtemplate}
final Radius cursorRadius; final Radius cursorRadius;
/// Whether the cursor will animate from fully transparent to fully opaque
/// during each cursor blink.
///
/// By default, the cursor opacity will animate on iOS platforms and will not
/// animate on Android platforms.
final bool cursorOpacityAnimates;
///{@macro flutter.rendering.editable.cursorOffset}
final Offset cursorOffset;
///{@macro flutter.rendering.editable.paintCursorOnTop}
final bool paintCursorAboveText;
/// The appearance of the keyboard. /// The appearance of the keyboard.
/// ///
/// This setting is only honored on iOS devices. /// This setting is only honored on iOS devices.
@ -546,9 +572,15 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
TextSelectionOverlay _selectionOverlay; TextSelectionOverlay _selectionOverlay;
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
AnimationController _cursorBlinkOpacityController;
final LayerLink _layerLink = LayerLink(); final LayerLink _layerLink = LayerLink();
bool _didAutoFocus = false; bool _didAutoFocus = false;
// 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);
@ -558,6 +590,8 @@ 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);
// State lifecycle: // State lifecycle:
@override @override
@ -566,6 +600,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
widget.controller.addListener(_didChangeTextEditingValue); widget.controller.addListener(_didChangeTextEditingValue);
widget.focusNode.addListener(_handleFocusChanged); widget.focusNode.addListener(_handleFocusChanged);
_scrollController.addListener(() { _selectionOverlay?.updateForScroll(); }); _scrollController.addListener(() { _selectionOverlay?.updateForScroll(); });
_cursorBlinkOpacityController = AnimationController(vsync: this, duration: _fadeDuration);
_cursorBlinkOpacityController.addListener(_onCursorColorTick);
_floatingCursorResetController = AnimationController(vsync: this); _floatingCursorResetController = AnimationController(vsync: this);
_floatingCursorResetController.addListener(_onFloatingCursorResetTick); _floatingCursorResetController.addListener(_onFloatingCursorResetTick);
} }
@ -597,6 +633,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@override @override
void dispose() { void dispose() {
widget.controller.removeListener(_didChangeTextEditingValue); widget.controller.removeListener(_didChangeTextEditingValue);
_cursorBlinkOpacityController.removeListener(_onCursorColorTick);
_floatingCursorResetController.removeListener(_onFloatingCursorResetTick);
_closeInputConnectionIfNeeded(); _closeInputConnectionIfNeeded();
assert(!_hasInputConnection); assert(!_hasInputConnection);
_stopCursorTimer(); _stopCursorTimer();
@ -623,6 +661,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
} }
_lastKnownRemoteTextEditingValue = value; _lastKnownRemoteTextEditingValue = value;
_formatAndSetValue(value); _formatAndSetValue(value);
// 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();
} }
@override @override
@ -696,7 +739,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
} }
void _onFloatingCursorResetTick() { void _onFloatingCursorResetTick() {
final Offset finalPosition = renderEditable.getLocalRectForCaret(_lastTextPosition).center - _floatingCursorOffset; final Offset finalPosition = renderEditable.getLocalRectForCaret(_lastTextPosition).centerLeft - _floatingCursorOffset;
if (_floatingCursorResetController.isCompleted) { if (_floatingCursorResetController.isCompleted) {
renderEditable.setFloatingCursor(FloatingCursorDragState.End, finalPosition, _lastTextPosition); renderEditable.setFloatingCursor(FloatingCursorDragState.End, finalPosition, _lastTextPosition);
if (_lastTextPosition.offset != renderEditable.selection.baseOffset) if (_lastTextPosition.offset != renderEditable.selection.baseOffset)
@ -957,6 +1000,10 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
widget.onChanged(value.text); widget.onChanged(value.text);
} }
void _onCursorColorTick() {
renderEditable.cursorColor = widget.cursorColor.withOpacity(_cursorBlinkOpacityController.value);
}
/// 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
@ -977,21 +1024,58 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
void _cursorTick(Timer timer) { void _cursorTick(Timer timer) {
_showCursor.value = !_showCursor.value; _showCursor.value = !_showCursor.value;
if (_obscureShowCharTicksPending > 0) { if (widget.cursorOpacityAnimates) {
setState(() { _obscureShowCharTicksPending--; }); // 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.
final double toValue = _showCursor.value ? 1.0 : 0.0;
_cursorBlinkOpacityController.animateTo(toValue, curve: Curves.easeOut);
} else {
_cursorBlinkOpacityController.value = _showCursor.value ? 1.0 : 0.0;
} }
if (_obscureShowCharTicksPending > 0) {
setState(() {
_obscureShowCharTicksPending--;
});
}
}
void _cursorWaitForStart(Timer timer) {
assert(_kCursorBlinkHalfPeriod > _fadeDuration);
_cursorTimer?.cancel();
_cursorTimer = Timer.periodic(_kCursorBlinkHalfPeriod, _cursorTick);
} }
void _startCursorTimer() { void _startCursorTimer() {
_showCursor.value = true; _showCursor.value = true;
_cursorTimer = Timer.periodic(_kCursorBlinkHalfPeriod, _cursorTick); _cursorBlinkOpacityController.value = 1.0;
if (EditableText.debugDeterministicCursor)
return;
if (widget.cursorOpacityAnimates) {
_cursorTimer = Timer.periodic(_kCursorBlinkWaitForStart, _cursorWaitForStart);
} else {
_cursorTimer = Timer.periodic(_kCursorBlinkHalfPeriod, _cursorTick);
}
} }
void _stopCursorTimer() { void _stopCursorTimer({ bool resetCharTicks = true }) {
_cursorTimer?.cancel(); _cursorTimer?.cancel();
_cursorTimer = null; _cursorTimer = null;
_showCursor.value = false; _showCursor.value = false;
_obscureShowCharTicksPending = 0; _cursorBlinkOpacityController.value = 0.0;
if (EditableText.debugDeterministicCursor)
return;
if (resetCharTicks)
_obscureShowCharTicksPending = 0;
if (widget.cursorOpacityAnimates) {
_cursorBlinkOpacityController.stop();
_cursorBlinkOpacityController.value = 0.0;
}
} }
void _startOrStopCursorTimerIfNeeded() { void _startOrStopCursorTimerIfNeeded() {
@ -1047,6 +1131,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@override @override
TextEditingValue get textEditingValue => _value; TextEditingValue get textEditingValue => _value;
double get _devicePixelRatio => MediaQuery.of(context).devicePixelRatio ?? 1.0;
@override @override
set textEditingValue(TextEditingValue value) { set textEditingValue(TextEditingValue value) {
_selectionOverlay?.update(value); _selectionOverlay?.update(value);
@ -1104,7 +1190,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
key: _editableKey, key: _editableKey,
textSpan: buildTextSpan(), textSpan: buildTextSpan(),
value: _value, value: _value,
cursorColor: widget.cursorColor, cursorColor: _cursorColor,
backgroundCursorColor: widget.backgroundCursorColor, backgroundCursorColor: widget.backgroundCursorColor,
showCursor: EditableText.debugDeterministicCursor ? ValueNotifier<bool>(true) : _showCursor, showCursor: EditableText.debugDeterministicCursor ? ValueNotifier<bool>(true) : _showCursor,
hasFocus: _hasFocus, hasFocus: _hasFocus,
@ -1122,8 +1208,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
rendererIgnoresPointer: widget.rendererIgnoresPointer, rendererIgnoresPointer: widget.rendererIgnoresPointer,
cursorWidth: widget.cursorWidth, cursorWidth: widget.cursorWidth,
cursorRadius: widget.cursorRadius, cursorRadius: widget.cursorRadius,
cursorOffset: widget.cursorOffset,
paintCursorAboveText: widget.paintCursorAboveText,
enableInteractiveSelection: widget.enableInteractiveSelection, enableInteractiveSelection: widget.enableInteractiveSelection,
textSelectionDelegate: this, textSelectionDelegate: this,
devicePixelRatio: _devicePixelRatio,
), ),
), ),
); );
@ -1188,8 +1277,11 @@ class _Editable extends LeafRenderObjectWidget {
this.rendererIgnoresPointer = false, this.rendererIgnoresPointer = false,
this.cursorWidth, this.cursorWidth,
this.cursorRadius, this.cursorRadius,
this.enableInteractiveSelection, this.cursorOffset,
this.enableInteractiveSelection = true,
this.textSelectionDelegate, this.textSelectionDelegate,
this.paintCursorAboveText,
this.devicePixelRatio
}) : assert(textDirection != null), }) : assert(textDirection != null),
assert(rendererIgnoresPointer != null), assert(rendererIgnoresPointer != null),
super(key: key); super(key: key);
@ -1214,8 +1306,11 @@ class _Editable extends LeafRenderObjectWidget {
final bool rendererIgnoresPointer; final bool rendererIgnoresPointer;
final double cursorWidth; final double cursorWidth;
final Radius cursorRadius; final Radius cursorRadius;
final Offset cursorOffset;
final bool enableInteractiveSelection; final bool enableInteractiveSelection;
final TextSelectionDelegate textSelectionDelegate; final TextSelectionDelegate textSelectionDelegate;
final double devicePixelRatio;
final bool paintCursorAboveText;
@override @override
RenderEditable createRenderObject(BuildContext context) { RenderEditable createRenderObject(BuildContext context) {
@ -1239,8 +1334,11 @@ class _Editable extends LeafRenderObjectWidget {
obscureText: obscureText, obscureText: obscureText,
cursorWidth: cursorWidth, cursorWidth: cursorWidth,
cursorRadius: cursorRadius, cursorRadius: cursorRadius,
cursorOffset: cursorOffset,
paintCursorAboveText: paintCursorAboveText,
enableInteractiveSelection: enableInteractiveSelection, enableInteractiveSelection: enableInteractiveSelection,
textSelectionDelegate: textSelectionDelegate, textSelectionDelegate: textSelectionDelegate,
devicePixelRatio: devicePixelRatio,
); );
} }
@ -1265,6 +1363,9 @@ class _Editable extends LeafRenderObjectWidget {
..obscureText = obscureText ..obscureText = obscureText
..cursorWidth = cursorWidth ..cursorWidth = cursorWidth
..cursorRadius = cursorRadius ..cursorRadius = cursorRadius
..textSelectionDelegate = textSelectionDelegate; ..cursorOffset = cursorOffset
..textSelectionDelegate = textSelectionDelegate
..devicePixelRatio = devicePixelRatio
..paintCursorAboveText = paintCursorAboveText;
} }
} }

View file

@ -7,6 +7,7 @@ import 'dart:async';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
class MockClipboard { class MockClipboard {
@ -143,6 +144,72 @@ void main() {
}, },
); );
testWidgets('iOS cursor has offset', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
home: CupertinoTextField(),
),
);
final EditableText editableText = tester.firstWidget(find.byType(EditableText));
expect(editableText.cursorOffset, const Offset(-2.0 / 3.0, 0));
});
testWidgets('Cursor animates on iOS', (WidgetTester tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
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);
debugDefaultTargetPlatformOverride = null;
});
testWidgets('Cursor radius is 2.0 on iOS', (WidgetTester tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
await tester.pumpWidget(
const CupertinoApp(
home: CupertinoTextField(),
),
);
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
final RenderEditable renderEditable = editableTextState.renderEditable;
expect(renderEditable.cursorRadius, const Radius.circular(2.0));
debugDefaultTargetPlatformOverride = null;
});
testWidgets( testWidgets(
'can control text content via controller', 'can control text content via controller',
(WidgetTester tester) async { (WidgetTester tester) async {

View file

@ -277,6 +277,65 @@ void main() {
await checkCursorToggle(); await checkCursorToggle();
}); });
testWidgets('Cursor animates on iOS', (WidgetTester tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
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);
debugDefaultTargetPlatformOverride = null;
});
testWidgets('Cursor radius is 2.0 on iOS', (WidgetTester tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: TextField(),
),
),
);
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
final RenderEditable renderEditable = editableTextState.renderEditable;
expect(renderEditable.cursorRadius, const Radius.circular(2.0));
debugDefaultTargetPlatformOverride = null;
});
testWidgets('cursor has expected defaults', (WidgetTester tester) async { testWidgets('cursor has expected defaults', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
overlay( overlay(
@ -305,6 +364,7 @@ void main() {
}); });
testWidgets('cursor layout has correct width', (WidgetTester tester) async { testWidgets('cursor layout has correct width', (WidgetTester tester) async {
EditableText.debugDeterministicCursor = true;
await tester.pumpWidget( await tester.pumpWidget(
overlay( overlay(
child: const RepaintBoundary( child: const RepaintBoundary(
@ -321,9 +381,11 @@ void main() {
find.byType(TextField), find.byType(TextField),
matchesGoldenFile('text_field_test.0.0.png'), matchesGoldenFile('text_field_test.0.0.png'),
); );
EditableText.debugDeterministicCursor = false;
}, skip: !Platform.isLinux); }, skip: !Platform.isLinux);
testWidgets('cursor layout has correct radius', (WidgetTester tester) async { testWidgets('cursor layout has correct radius', (WidgetTester tester) async {
EditableText.debugDeterministicCursor = true;
await tester.pumpWidget( await tester.pumpWidget(
overlay( overlay(
child: const RepaintBoundary( child: const RepaintBoundary(
@ -341,6 +403,7 @@ void main() {
find.byType(TextField), find.byType(TextField),
matchesGoldenFile('text_field_test.1.0.png'), matchesGoldenFile('text_field_test.1.0.png'),
); );
EditableText.debugDeterministicCursor = false;
}, skip: !Platform.isLinux); }, skip: !Platform.isLinux);
testWidgets('obscureText control test', (WidgetTester tester) async { testWidgets('obscureText control test', (WidgetTester tester) async {
@ -1469,7 +1532,7 @@ void main() {
editable.getLocalRectForCaret(const TextPosition(offset: 0)).topLeft, editable.getLocalRectForCaret(const TextPosition(offset: 0)).topLeft,
); );
expect(topLeft.dx, equals(398.5)); expect(topLeft.dx, equals(401.0));
await tester.enterText(find.byType(TextField), 'abcd'); await tester.enterText(find.byType(TextField), 'abcd');
await tester.pump(); await tester.pump();
@ -1478,7 +1541,7 @@ void main() {
editable.getLocalRectForCaret(const TextPosition(offset: 2)).topLeft, editable.getLocalRectForCaret(const TextPosition(offset: 2)).topLeft,
); );
expect(topLeft.dx, equals(398.5)); expect(topLeft.dx, equals(401.0));
}); });
testWidgets('Can align to center within center', (WidgetTester tester) async { testWidgets('Can align to center within center', (WidgetTester tester) async {
@ -1501,7 +1564,7 @@ void main() {
editable.getLocalRectForCaret(const TextPosition(offset: 0)).topLeft, editable.getLocalRectForCaret(const TextPosition(offset: 0)).topLeft,
); );
expect(topLeft.dx, equals(398.5)); expect(topLeft.dx, equals(401.0));
await tester.enterText(find.byType(TextField), 'abcd'); await tester.enterText(find.byType(TextField), 'abcd');
await tester.pump(); await tester.pump();
@ -1510,7 +1573,7 @@ void main() {
editable.getLocalRectForCaret(const TextPosition(offset: 2)).topLeft, editable.getLocalRectForCaret(const TextPosition(offset: 2)).topLeft,
); );
expect(topLeft.dx, equals(398.5)); expect(topLeft.dx, equals(401.0));
}); });
testWidgets('Controller can update server', (WidgetTester tester) async { testWidgets('Controller can update server', (WidgetTester tester) async {
@ -1723,7 +1786,7 @@ void main() {
scrollableState = tester.firstState(find.byType(Scrollable)); scrollableState = tester.firstState(find.byType(Scrollable));
// For a horizontal input, scrolls to the exact position of the caret. // For a horizontal input, scrolls to the exact position of the caret.
expect(scrollableState.position.pixels, equals(222.0)); expect(scrollableState.position.pixels, equals(223.0));
}); });
testWidgets('Multiline text field scrolls the caret into view', (WidgetTester tester) async { testWidgets('Multiline text field scrolls the caret into view', (WidgetTester tester) async {
@ -3130,7 +3193,7 @@ void main() {
editable.getLocalRectForCaret(const TextPosition(offset: 10)).topLeft, editable.getLocalRectForCaret(const TextPosition(offset: 10)).topLeft,
); );
expect(topLeft.dx, equals(701.0)); expect(topLeft.dx, equals(701.6666870117188));
await tester.pumpWidget( await tester.pumpWidget(
const MaterialApp( const MaterialApp(
@ -3150,7 +3213,7 @@ void main() {
editable.getLocalRectForCaret(const TextPosition(offset: 10)).topLeft, editable.getLocalRectForCaret(const TextPosition(offset: 10)).topLeft,
); );
expect(topLeft.dx, equals(160.0)); expect(topLeft.dx, equals(160.6666717529297));
}); });
testWidgets('TextField semantics', (WidgetTester tester) async { testWidgets('TextField semantics', (WidgetTester tester) async {

View file

@ -124,11 +124,14 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: Material( home: Padding(
child: TextField( padding: const EdgeInsets.only(top: 0.25),
controller: controller, child: Material(
focusNode: focusNode, child: TextField(
style: textStyle, controller: controller,
focusNode: focusNode,
style: textStyle,
),
), ),
), ),
), ),
@ -144,16 +147,32 @@ void main() {
offset: const Offset(20, 20))); offset: const Offset(20, 20)));
await tester.pump(); await tester.pump();
expect(find.byType(EditableText), paints..rrect( expect(editable, paints
rrect: RRect.fromRectAndRadius(Rect.fromLTRB(464.5, 0, 467.5, 16.0), const Radius.circular(1.0)), color: const Color(0xff4285f4)) ..rrect(rrect: RRect.fromRectAndRadius(
Rect.fromLTRB(464.6666564941406, -1.5833333730697632, 466.6666564941406, 16.41666603088379),
const Radius.circular(2.0)),
color: const Color(0xff8e8e93))
..rrect(rrect: RRect.fromRectAndRadius(
Rect.fromLTRB(465.1666564941406, -2.416666269302368, 468.1666564941406, 17.58333396911621),
const Radius.circular(1.0)),
color: const Color(0xbf2196f3))
); );
// Moves the cursor right a few characters. // Moves the cursor right a few characters.
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update, editableTextState.updateFloatingCursor(
offset: const Offset(-250, 20))); RawFloatingCursorPoint(
state: FloatingCursorDragState.Update,
offset: const Offset(-250, 20)));
expect(find.byType(EditableText), paints..rrect( expect(find.byType(EditableText), paints
rrect: RRect.fromRectAndRadius(Rect.fromLTRB(194.5, 0, 197.5, 16.0), const Radius.circular(1.0)), color: const Color(0xff4285f4)) ..rrect(rrect: RRect.fromRectAndRadius(
Rect.fromLTRB(192.6666717529297, -1.5833333730697632, 194.6666717529297, 16.41666603088379),
const Radius.circular(2.0)),
color: const Color(0xff8e8e93))
..rrect(rrect: RRect.fromRectAndRadius(
Rect.fromLTRB(195.16665649414062, -2.416666269302368, 198.16665649414062, 17.58333396911621),
const Radius.circular(1.0)),
color: const Color(0xbf2196f3))
); );
editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.End)); editableTextState.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.End));

View file

@ -201,28 +201,33 @@ void main() {
final TextEditingController textController = TextEditingController(); final TextEditingController textController = TextEditingController();
final PageController pageController = PageController(initialPage: 1); final PageController pageController = PageController(initialPage: 1);
await tester.pumpWidget(Directionality( await tester.pumpWidget(
textDirection: TextDirection.ltr, MediaQuery(
child: Material( data: const MediaQueryData(devicePixelRatio: 1.0),
child: PageView( child: Directionality(
controller: pageController, textDirection: TextDirection.ltr,
children: <Widget>[ child: Material(
Container( child: PageView(
color: Colors.red, controller: pageController,
children: <Widget>[
Container(
color: Colors.red,
),
Container(
child: TextField(
controller: textController,
),
color: Colors.green,
),
Container(
color: Colors.red,
),
],
), ),
Container( ),
child: TextField(
controller: textController,
),
color: Colors.green,
),
Container(
color: Colors.red,
),
],
), ),
), ),
)); );
await tester.showKeyboard(find.byType(EditableText)); await tester.showKeyboard(find.byType(EditableText));
await tester.pumpAndSettle(); await tester.pumpAndSettle();

View file

@ -11,6 +11,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:flutter/foundation.dart';
import 'semantics_tester.dart'; import 'semantics_tester.dart';
@ -36,18 +37,21 @@ void main() {
String serializedActionName, String serializedActionName,
}) async { }) async {
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MediaQuery(
textDirection: TextDirection.ltr, data: const MediaQueryData(devicePixelRatio: 1.0),
child: FocusScope( child: Directionality(
node: focusScopeNode, textDirection: TextDirection.ltr,
autofocus: true, child: FocusScope(
child: EditableText( node: focusScopeNode,
backgroundCursorColor: Colors.grey, autofocus: true,
controller: controller, child: EditableText(
focusNode: focusNode, backgroundCursorColor: Colors.grey,
textInputAction: action, controller: controller,
style: textStyle, focusNode: focusNode,
cursorColor: cursorColor, textInputAction: action,
style: textStyle,
cursorColor: cursorColor,
),
), ),
), ),
), ),
@ -64,14 +68,17 @@ void main() {
testWidgets('has expected defaults', (WidgetTester tester) async { testWidgets('has expected defaults', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MediaQuery(
textDirection: TextDirection.ltr, data: const MediaQueryData(devicePixelRatio: 1.0),
child: EditableText( child: Directionality(
controller: controller, textDirection: TextDirection.ltr,
backgroundCursorColor: Colors.grey, child: EditableText(
focusNode: focusNode, controller: controller,
style: textStyle, backgroundCursorColor: Colors.grey,
cursorColor: cursorColor, focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
), ),
), ),
); );
@ -86,7 +93,9 @@ void main() {
testWidgets('cursor has expected width and radius', testWidgets('cursor has expected width and radius',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget(Directionality( await tester.pumpWidget(
MediaQuery(data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: EditableText( child: EditableText(
backgroundCursorColor: Colors.grey, backgroundCursorColor: Colors.grey,
@ -96,7 +105,7 @@ void main() {
cursorColor: cursorColor, cursorColor: cursorColor,
cursorWidth: 10.0, cursorWidth: 10.0,
cursorRadius: const Radius.circular(2.0), cursorRadius: const Radius.circular(2.0),
))); ))));
final EditableText editableText = final EditableText editableText =
tester.firstWidget(find.byType(EditableText)); tester.firstWidget(find.byType(EditableText));
@ -107,17 +116,20 @@ void main() {
testWidgets('text keyboard is requested when maxLines is default', testWidgets('text keyboard is requested when maxLines is default',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MediaQuery(
textDirection: TextDirection.ltr, data: const MediaQueryData(devicePixelRatio: 1.0),
child: FocusScope( child: Directionality(
node: focusScopeNode, textDirection: TextDirection.ltr,
autofocus: true, child: FocusScope(
child: EditableText( node: focusScopeNode,
controller: controller, autofocus: true,
backgroundCursorColor: Colors.grey, child: EditableText(
focusNode: focusNode, controller: controller,
style: textStyle, backgroundCursorColor: Colors.grey,
cursorColor: cursorColor, focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
), ),
), ),
), ),
@ -269,18 +281,21 @@ void main() {
testWidgets('multiline keyboard is requested when set explicitly', testWidgets('multiline keyboard is requested when set explicitly',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MediaQuery(
textDirection: TextDirection.ltr, data: const MediaQueryData(devicePixelRatio: 1.0),
child: FocusScope( child: Directionality(
node: focusScopeNode, textDirection: TextDirection.ltr,
autofocus: true, child: FocusScope(
child: EditableText( node: focusScopeNode,
controller: controller, autofocus: true,
backgroundCursorColor: Colors.grey, child: EditableText(
focusNode: focusNode, controller: controller,
keyboardType: TextInputType.multiline, backgroundCursorColor: Colors.grey,
style: textStyle, focusNode: focusNode,
cursorColor: cursorColor, keyboardType: TextInputType.multiline,
style: textStyle,
cursorColor: cursorColor,
),
), ),
), ),
), ),
@ -299,18 +314,21 @@ void main() {
testWidgets('Multiline keyboard with newline action is requested when maxLines = null', (WidgetTester tester) async { testWidgets('Multiline keyboard with newline action is requested when maxLines = null', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MediaQuery(
textDirection: TextDirection.ltr, data: const MediaQueryData(devicePixelRatio: 1.0),
child: FocusScope( child: Directionality(
node: focusScopeNode, textDirection: TextDirection.ltr,
autofocus: true, child: FocusScope(
child: EditableText( node: focusScopeNode,
controller: controller, autofocus: true,
backgroundCursorColor: Colors.grey, child: EditableText(
focusNode: focusNode, controller: controller,
maxLines: null, backgroundCursorColor: Colors.grey,
style: textStyle, focusNode: focusNode,
cursorColor: cursorColor, maxLines: null,
style: textStyle,
cursorColor: cursorColor,
),
), ),
), ),
), ),
@ -329,19 +347,22 @@ void main() {
testWidgets('Text keyboard is requested when explicitly set and maxLines = null', (WidgetTester tester) async { testWidgets('Text keyboard is requested when explicitly set and maxLines = null', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MediaQuery(
textDirection: TextDirection.ltr, data: const MediaQueryData(devicePixelRatio: 1.0),
child: FocusScope( child: Directionality(
node: focusScopeNode, textDirection: TextDirection.ltr,
autofocus: true, child: FocusScope(
child: EditableText( node: focusScopeNode,
backgroundCursorColor: Colors.grey, autofocus: true,
controller: controller, child: EditableText(
focusNode: focusNode, backgroundCursorColor: Colors.grey,
maxLines: null, controller: controller,
keyboardType: TextInputType.text, focusNode: focusNode,
style: textStyle, maxLines: null,
cursorColor: cursorColor, keyboardType: TextInputType.text,
style: textStyle,
cursorColor: cursorColor,
),
), ),
), ),
), ),
@ -362,19 +383,22 @@ void main() {
'Correct keyboard is requested when set explicitly and maxLines > 1', 'Correct keyboard is requested when set explicitly and maxLines > 1',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MediaQuery(
textDirection: TextDirection.ltr, data: const MediaQueryData(devicePixelRatio: 1.0),
child: FocusScope( child: Directionality(
node: focusScopeNode, textDirection: TextDirection.ltr,
autofocus: true, child: FocusScope(
child: EditableText( node: focusScopeNode,
backgroundCursorColor: Colors.grey, autofocus: true,
controller: controller, child: EditableText(
focusNode: focusNode, backgroundCursorColor: Colors.grey,
keyboardType: TextInputType.phone, controller: controller,
maxLines: 3, focusNode: focusNode,
style: textStyle, keyboardType: TextInputType.phone,
cursorColor: cursorColor, maxLines: 3,
style: textStyle,
cursorColor: cursorColor,
),
), ),
), ),
), ),
@ -394,18 +418,21 @@ void main() {
testWidgets('multiline keyboard is requested when set implicitly', testWidgets('multiline keyboard is requested when set implicitly',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MediaQuery(
textDirection: TextDirection.ltr, data: const MediaQueryData(devicePixelRatio: 1.0),
child: FocusScope( child: Directionality(
node: focusScopeNode, textDirection: TextDirection.ltr,
autofocus: true, child: FocusScope(
child: EditableText( node: focusScopeNode,
backgroundCursorColor: Colors.grey, autofocus: true,
controller: controller, child: EditableText(
focusNode: focusNode, backgroundCursorColor: Colors.grey,
maxLines: 3, // Sets multiline keyboard implicitly. controller: controller,
style: textStyle, focusNode: focusNode,
cursorColor: cursorColor, maxLines: 3, // Sets multiline keyboard implicitly.
style: textStyle,
cursorColor: cursorColor,
),
), ),
), ),
), ),
@ -425,18 +452,21 @@ void main() {
testWidgets('single line inputs have correct default keyboard', testWidgets('single line inputs have correct default keyboard',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MediaQuery(
textDirection: TextDirection.ltr, data: const MediaQueryData(devicePixelRatio: 1.0),
child: FocusScope( child: Directionality(
node: focusScopeNode, textDirection: TextDirection.ltr,
autofocus: true, child: FocusScope(
child: EditableText( node: focusScopeNode,
backgroundCursorColor: Colors.grey, autofocus: true,
controller: controller, child: EditableText(
focusNode: focusNode, backgroundCursorColor: Colors.grey,
maxLines: 1, // Sets text keyboard implicitly. controller: controller,
style: textStyle, focusNode: focusNode,
cursorColor: cursorColor, maxLines: 1, // Sets text keyboard implicitly.
style: textStyle,
cursorColor: cursorColor,
),
), ),
), ),
), ),
@ -538,6 +568,8 @@ void main() {
await tester.tap(find.text('PASTE')); await tester.tap(find.text('PASTE'));
await tester.pump(); await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 600));
expect(changedValue, clipboardContent); expect(changedValue, clipboardContent);
@ -590,6 +622,8 @@ void main() {
await tester.tap(find.text('PASTE')); await tester.tap(find.text('PASTE'));
await tester.pump(); await tester.pump();
await tester.pump(const Duration(milliseconds: 500));
await tester.pump(const Duration(milliseconds: 600));
expect(changedValue, clipboardContent); expect(changedValue, clipboardContent);
@ -807,6 +841,118 @@ void main() {
// and onSubmission callbacks. // and onSubmission callbacks.
}); });
testWidgets('Cursor animates on iOS', (WidgetTester tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
const Widget widget =
MaterialApp(
home: Material(
child: TextField(
maxLines: 3,
)
),
);
await tester.pumpWidget(widget);
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.alpha, 255);
await tester.pump(const Duration(milliseconds: 100));
await tester.pump(const Duration(milliseconds: 100));
await tester.pump(const Duration(milliseconds: 100));
await tester.pump(const Duration(milliseconds: 100));
await tester.pump(const Duration(milliseconds: 100));
expect(renderEditable.cursorColor.alpha, 255);
await tester.pump(const Duration(milliseconds: 100));
await tester.pump(const Duration(milliseconds: 100));
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: 100));
await tester.pump(const Duration(milliseconds: 100));
await tester.pump(const Duration(milliseconds: 100));
expect(renderEditable.cursorColor.alpha, 0);
debugDefaultTargetPlatformOverride = null;
});
testWidgets('Cursor does not animate on Android', (WidgetTester tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.android;
const Widget widget =
MaterialApp(
home: Material(
child: TextField(
maxLines: 3,
)
),
);
await tester.pumpWidget(widget);
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.alpha, 255);
await tester.pump(const Duration(milliseconds: 100));
expect(renderEditable.cursorColor.alpha, 255);
await tester.pump(const Duration(milliseconds: 100));
expect(renderEditable.cursorColor.alpha, 255);
await tester.pump(const Duration(milliseconds: 100));
expect(renderEditable.cursorColor.alpha, 255);
await tester.pump(const Duration(milliseconds: 100));
expect(renderEditable.cursorColor.alpha, 255);
await tester.pump(const Duration(milliseconds: 100));
expect(renderEditable.cursorColor.alpha, 0);
await tester.pump(const Duration(milliseconds: 100));
await tester.pump(const Duration(milliseconds: 100));
await tester.pump(const Duration(milliseconds: 100));
expect(renderEditable.cursorColor.alpha, 0);
debugDefaultTargetPlatformOverride = null;
});
testWidgets('Cursor radius is 2.0 on iOS', (WidgetTester tester) async {
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
const Widget widget =
MaterialApp(
home: Material(
child: TextField(
maxLines: 3,
)
),
);
await tester.pumpWidget(widget);
final EditableTextState editableTextState = tester.firstState(find.byType(EditableText));
final RenderEditable renderEditable = editableTextState.renderEditable;
expect(renderEditable.cursorRadius, const Radius.circular(2.0));
debugDefaultTargetPlatformOverride = null;
});
testWidgets( testWidgets(
'When "newline" action is called on a Editable text with maxLines != 1, onEditingComplete and onSubmitted callbacks are not invoked.', 'When "newline" action is called on a Editable text with maxLines != 1, onEditingComplete and onSubmitted callbacks are not invoked.',
(WidgetTester tester) async { (WidgetTester tester) async {
@ -867,22 +1013,25 @@ testWidgets(
return StatefulBuilder( return StatefulBuilder(
builder: (BuildContext context, StateSetter setter) { builder: (BuildContext context, StateSetter setter) {
setState = setter; setState = setter;
return Directionality( return MediaQuery(
textDirection: TextDirection.ltr, data: const MediaQueryData(devicePixelRatio: 1.0),
child: Center( child: Directionality(
child: Material( textDirection: TextDirection.ltr,
child: EditableText( child: Center(
backgroundCursorColor: Colors.grey, child: Material(
key: editableTextKey, child: EditableText(
controller: currentController, backgroundCursorColor: Colors.grey,
focusNode: FocusNode(), key: editableTextKey,
style: Typography(platform: TargetPlatform.android) controller: currentController,
.black focusNode: FocusNode(),
.subhead, style: Typography(platform: TargetPlatform.android)
cursorColor: Colors.blue, .black
selectionControls: materialTextSelectionControls, .subhead,
keyboardType: TextInputType.text, cursorColor: Colors.blue,
onChanged: (String value) {}, selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
onChanged: (String value) {},
),
), ),
), ),
), ),
@ -926,17 +1075,20 @@ testWidgets(
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MediaQuery(
data: const MediaQueryData(devicePixelRatio: 1.0),
child: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: FocusScope( child: FocusScope(
node: focusScopeNode, node: focusScopeNode,
autofocus: true, autofocus: true,
child: EditableText( child: EditableText(
backgroundCursorColor: Colors.grey, backgroundCursorColor: Colors.grey,
controller: controller, controller: controller,
focusNode: focusNode, focusNode: focusNode,
style: textStyle, style: textStyle,
cursorColor: cursorColor, cursorColor: cursorColor,
),
), ),
), ),
), ),
@ -968,16 +1120,19 @@ testWidgets(
controller.text = value1; controller.text = value1;
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MediaQuery(
textDirection: TextDirection.ltr, data: const MediaQueryData(devicePixelRatio: 1.0),
child: FocusScope( child: Directionality(
node: focusScopeNode, textDirection: TextDirection.ltr,
child: EditableText( child: FocusScope(
backgroundCursorColor: Colors.grey, node: focusScopeNode,
controller: controller, child: EditableText(
focusNode: focusNode, backgroundCursorColor: Colors.grey,
style: textStyle, controller: controller,
cursorColor: cursorColor, focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
), ),
), ),
), ),
@ -1767,21 +1922,26 @@ testWidgets(
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
controller.text = text; controller.text = text;
await tester.pumpWidget(Directionality( await tester.pumpWidget(
textDirection: TextDirection.ltr, MediaQuery(
child: FocusScope( data: const MediaQueryData(devicePixelRatio: 1.0),
node: focusScopeNode, child: Directionality(
autofocus: true, textDirection: TextDirection.ltr,
child: EditableText( child: FocusScope(
backgroundCursorColor: Colors.grey, node: focusScopeNode,
controller: controller, autofocus: true,
focusNode: focusNode, child: EditableText(
autofocus: true, backgroundCursorColor: Colors.grey,
style: textStyle, controller: controller,
cursorColor: cursorColor, focusNode: focusNode,
autofocus: true,
style: textStyle,
cursorColor: cursorColor,
),
),
), ),
), ),
)); );
expect(focusNode.hasFocus, true); expect(focusNode.hasFocus, true);
expect(controller.selection.isCollapsed, true); expect(controller.selection.isCollapsed, true);
@ -1811,17 +1971,20 @@ testWidgets(
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MediaQuery(
textDirection: TextDirection.ltr, data: const MediaQueryData(devicePixelRatio: 1.0),
child: FocusScope( child: Directionality(
node: focusScopeNode, textDirection: TextDirection.ltr,
autofocus: true, child: FocusScope(
child: EditableText( node: focusScopeNode,
backgroundCursorColor: Colors.grey, autofocus: true,
controller: controller, child: EditableText(
focusNode: focusNode, backgroundCursorColor: Colors.grey,
style: textStyle, controller: controller,
cursorColor: cursorColor, focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
), ),
), ),
), ),
@ -1864,17 +2027,20 @@ testWidgets(
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MediaQuery(
textDirection: TextDirection.ltr, data: const MediaQueryData(devicePixelRatio: 1.0),
child: FocusScope( child: Directionality(
node: focusScopeNode, textDirection: TextDirection.ltr,
autofocus: true, child: FocusScope(
child: EditableText( node: focusScopeNode,
backgroundCursorColor: Colors.grey, autofocus: true,
controller: controller, child: EditableText(
focusNode: focusNode, backgroundCursorColor: Colors.grey,
style: textStyle, controller: controller,
cursorColor: cursorColor, focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
), ),
), ),
), ),
@ -1936,7 +2102,7 @@ testWidgets(
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 11); expect(controller.selection.baseOffset, 10);
}); });
testWidgets('Formatters are skipped if text has not changed', (WidgetTester tester) async { testWidgets('Formatters are skipped if text has not changed', (WidgetTester tester) async {
@ -1946,18 +2112,21 @@ testWidgets(
return newValue; return newValue;
}); });
final TextEditingController controller = TextEditingController(); final TextEditingController controller = TextEditingController();
final EditableText editableText = EditableText( final MediaQuery mediaQuery = MediaQuery(
controller: controller, data: const MediaQueryData(devicePixelRatio: 1.0),
backgroundCursorColor: Colors.red, child: EditableText(
cursorColor: Colors.red, controller: controller,
focusNode: FocusNode(), backgroundCursorColor: Colors.red,
style: textStyle, cursorColor: Colors.red,
inputFormatters: <TextInputFormatter>[ focusNode: FocusNode(),
formatter, style: textStyle,
], inputFormatters: <TextInputFormatter>[
textDirection: TextDirection.ltr, formatter,
],
textDirection: TextDirection.ltr,
),
); );
await tester.pumpWidget(editableText); await tester.pumpWidget(mediaQuery);
final EditableTextState state = tester.firstState(find.byType(EditableText)); final EditableTextState state = tester.firstState(find.byType(EditableText));
state.updateEditingValue(const TextEditingValue( state.updateEditingValue(const TextEditingValue(
text: 'a', text: 'a',
@ -1990,14 +2159,19 @@ testWidgets(
final TextEditingController controller = TextEditingController(); final TextEditingController controller = TextEditingController();
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MediaQuery(
textDirection: TextDirection.ltr, data: const MediaQueryData(
child: EditableText( devicePixelRatio: 1.0
controller: controller, ),
focusNode: FocusNode(), child: Directionality(
style: Typography(platform: TargetPlatform.android).black.subhead, textDirection: TextDirection.ltr,
cursorColor: Colors.blue, child: EditableText(
backgroundCursorColor: Colors.grey, controller: controller,
focusNode: FocusNode(),
style: Typography(platform: TargetPlatform.android).black.subhead,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
),
), ),
), ),
); );
@ -2018,15 +2192,20 @@ testWidgets(
final TextEditingController controller = TextEditingController(); final TextEditingController controller = TextEditingController();
await tester.pumpWidget( await tester.pumpWidget(
Directionality( MediaQuery(
textDirection: TextDirection.ltr, data: const MediaQueryData(
child: EditableText( devicePixelRatio: 1.0
controller: controller, ),
focusNode: FocusNode(), child: Directionality(
style: Typography(platform: TargetPlatform.android).black.subhead, textDirection: TextDirection.ltr,
cursorColor: Colors.blue, child: EditableText(
backgroundCursorColor: Colors.grey, controller: controller,
keyboardAppearance: Brightness.dark, focusNode: FocusNode(),
style: Typography(platform: TargetPlatform.android).black.subhead,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
keyboardAppearance: Brightness.dark,
),
), ),
), ),
); );

View file

@ -11,14 +11,17 @@ void main() {
String fieldValue; String fieldValue;
Widget builder() { Widget builder() {
return Directionality( return MediaQuery(
textDirection: TextDirection.ltr, data: const MediaQueryData(devicePixelRatio: 1.0),
child: Center( child: Directionality(
child: Material( textDirection: TextDirection.ltr,
child: Form( child: Center(
key: formKey, child: Material(
child: TextFormField( child: Form(
onSaved: (String value) { fieldValue = value; }, key: formKey,
child: TextFormField(
onSaved: (String value) { fieldValue = value; },
),
), ),
), ),
), ),
@ -45,13 +48,16 @@ void main() {
String fieldValue; String fieldValue;
Widget builder() { Widget builder() {
return Directionality( return MediaQuery(
textDirection: TextDirection.ltr, data: const MediaQueryData(devicePixelRatio: 1.0),
child: Center( child: Directionality(
child: Material( textDirection: TextDirection.ltr,
child: Form( child: Center(
child: TextField( child: Material(
onChanged: (String value) { fieldValue = value; }, child: Form(
child: TextField(
onChanged: (String value) { fieldValue = value; },
),
), ),
), ),
), ),
@ -78,15 +84,18 @@ void main() {
String errorText(String value) => value + '/error'; String errorText(String value) => value + '/error';
Widget builder(bool autovalidate) { Widget builder(bool autovalidate) {
return Directionality( return MediaQuery(
textDirection: TextDirection.ltr, data: const MediaQueryData(devicePixelRatio: 1.0),
child: Center( child: Directionality(
child: Material( textDirection: TextDirection.ltr,
child: Form( child: Center(
key: formKey, child: Material(
autovalidate: autovalidate, child: Form(
child: TextFormField( key: formKey,
validator: errorText, autovalidate: autovalidate,
child: TextFormField(
validator: errorText,
),
), ),
), ),
), ),
@ -129,22 +138,25 @@ void main() {
String errorText(String input) => '${fieldKey.currentState.value}/error'; String errorText(String input) => '${fieldKey.currentState.value}/error';
Widget builder() { Widget builder() {
return Directionality( return MediaQuery(
textDirection: TextDirection.ltr, data: const MediaQueryData(devicePixelRatio: 1.0),
child: Center( child: Directionality(
child: Material( textDirection: TextDirection.ltr,
child: Form( child: Center(
key: formKey, child: Material(
autovalidate: true, child: Form(
child: ListView( key: formKey,
children: <Widget>[ autovalidate: true,
TextFormField( child: ListView(
key: fieldKey, children: <Widget>[
), TextFormField(
TextFormField( key: fieldKey,
validator: errorText, ),
), TextFormField(
], validator: errorText,
),
],
),
), ),
), ),
), ),
@ -172,14 +184,17 @@ void main() {
final GlobalKey<FormFieldState<String>> inputKey = GlobalKey<FormFieldState<String>>(); final GlobalKey<FormFieldState<String>> inputKey = GlobalKey<FormFieldState<String>>();
Widget builder() { Widget builder() {
return Directionality( return MediaQuery(
textDirection: TextDirection.ltr, data: const MediaQueryData(devicePixelRatio: 1.0),
child: Center( child: Directionality(
child: Material( textDirection: TextDirection.ltr,
child: Form( child: Center(
child: TextFormField( child: Material(
key: inputKey, child: Form(
initialValue: 'hello', child: TextFormField(
key: inputKey,
initialValue: 'hello',
),
), ),
), ),
), ),
@ -212,14 +227,17 @@ void main() {
final GlobalKey<FormFieldState<String>> inputKey = GlobalKey<FormFieldState<String>>(); final GlobalKey<FormFieldState<String>> inputKey = GlobalKey<FormFieldState<String>>();
Widget builder() { Widget builder() {
return Directionality( return MediaQuery(
textDirection: TextDirection.ltr, data: const MediaQueryData(devicePixelRatio: 1.0),
child: Center( child: Directionality(
child: Material( textDirection: TextDirection.ltr,
child: Form( child: Center(
child: TextFormField( child: Material(
key: inputKey, child: Form(
controller: controller, child: TextFormField(
key: inputKey,
controller: controller,
),
), ),
), ),
), ),
@ -254,16 +272,19 @@ void main() {
final TextEditingController controller = TextEditingController(text: 'Plover'); final TextEditingController controller = TextEditingController(text: 'Plover');
Widget builder() { Widget builder() {
return Directionality( return MediaQuery(
textDirection: TextDirection.ltr, data: const MediaQueryData(devicePixelRatio: 1.0),
child: Center( child: Directionality(
child: Material( textDirection: TextDirection.ltr,
child: Form( child: Center(
key: formKey, child: Material(
child: TextFormField( child: Form(
key: inputKey, key: formKey,
controller: controller, child: TextFormField(
// initialValue is 'Plover' key: inputKey,
controller: controller,
// initialValue is 'Plover'
),
), ),
), ),
), ),
@ -301,14 +322,17 @@ void main() {
return StatefulBuilder( return StatefulBuilder(
builder: (BuildContext context, StateSetter setter) { builder: (BuildContext context, StateSetter setter) {
setState = setter; setState = setter;
return Directionality( return MediaQuery(
textDirection: TextDirection.ltr, data: const MediaQueryData(devicePixelRatio: 1.0),
child: Center( child: Directionality(
child: Material( textDirection: TextDirection.ltr,
child: Form( child: Center(
child: TextFormField( child: Material(
key: inputKey, child: Form(
controller: currentController, child: TextFormField(
key: inputKey,
controller: currentController,
),
), ),
), ),
), ),
@ -396,16 +420,19 @@ void main() {
String fieldValue; String fieldValue;
Widget builder(bool remove) { Widget builder(bool remove) {
return Directionality( return MediaQuery(
textDirection: TextDirection.ltr, data: const MediaQueryData(devicePixelRatio: 1.0),
child: Center( child: Directionality(
child: Material( textDirection: TextDirection.ltr,
child: Form( child: Center(
key: formKey, child: Material(
child: remove ? Container() : TextFormField( child: Form(
autofocus: true, key: formKey,
onSaved: (String value) { fieldValue = value; }, child: remove ? Container() : TextFormField(
validator: (String value) { return value.isEmpty ? null : 'yes'; } autofocus: true,
onSaved: (String value) { fieldValue = value; },
validator: (String value) { return value.isEmpty ? null : 'yes'; }
),
), ),
), ),
), ),

View file

@ -6,14 +6,18 @@ import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
testWidgets('PhysicalModel - creates a physical model layer when it needs compositing', (WidgetTester tester) async { testWidgets('PhysicalModel - creates a physical model layer when it needs compositing', (WidgetTester tester) async {
debugDisableShadows = false; debugDisableShadows = false;
await tester.pumpWidget(Directionality( await tester.pumpWidget(
textDirection: TextDirection.ltr, MediaQuery(
child: PhysicalModel( data: const MediaQueryData(devicePixelRatio: 1.0),
shape: BoxShape.rectangle, child: Directionality(
color: Colors.grey, textDirection: TextDirection.ltr,
shadowColor: Colors.red, child: PhysicalModel(
elevation: 1.0, shape: BoxShape.rectangle,
child: Material(child: TextField(controller: TextEditingController())), color: Colors.grey,
shadowColor: Colors.red,
elevation: 1.0,
child: Material(child: TextField(controller: TextEditingController())),
),
), ),
), ),
); );