mirror of
https://github.com/flutter/flutter
synced 2024-10-13 03:32:55 +00:00
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:
parent
9f20bd6cb9
commit
d2a2a5cfe2
|
@ -1 +1 @@
|
||||||
6fc7ec65d51116c3f83acb5251e57e779af2ebbb
|
b530d67675a5aa9c5458b93019ce91e20ad88758
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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'; }
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -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())),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue