TextField splash integration (#14055)
This commit is contained in:
parent
c09736bb58
commit
27eeb9722f
@ -14,11 +14,11 @@ import 'material.dart';
|
|||||||
const Duration _kUnconfirmedRippleDuration = const Duration(seconds: 1);
|
const Duration _kUnconfirmedRippleDuration = const Duration(seconds: 1);
|
||||||
const Duration _kFadeInDuration = const Duration(milliseconds: 75);
|
const Duration _kFadeInDuration = const Duration(milliseconds: 75);
|
||||||
const Duration _kRadiusDuration = const Duration(milliseconds: 225);
|
const Duration _kRadiusDuration = const Duration(milliseconds: 225);
|
||||||
const Duration _kFadeOutDuration = const Duration(milliseconds: 450);
|
const Duration _kFadeOutDuration = const Duration(milliseconds: 375);
|
||||||
const Duration _kCancelDuration = const Duration(milliseconds: 75);
|
const Duration _kCancelDuration = const Duration(milliseconds: 75);
|
||||||
|
|
||||||
// The fade out begins 300ms after the _fadeOutController starts. See confirm().
|
// The fade out begins 225ms after the _fadeOutController starts. See confirm().
|
||||||
const double _kFadeOutIntervalStart = 300.0 / 450.0;
|
const double _kFadeOutIntervalStart = 225.0 / 375.0;
|
||||||
|
|
||||||
RectCallback _getClipCallback(RenderBox referenceBox, bool containedInkWell, RectCallback rectCallback) {
|
RectCallback _getClipCallback(RenderBox referenceBox, bool containedInkWell, RectCallback rectCallback) {
|
||||||
if (rectCallback != null) {
|
if (rectCallback != null) {
|
||||||
@ -31,19 +31,10 @@ RectCallback _getClipCallback(RenderBox referenceBox, bool containedInkWell, Rec
|
|||||||
}
|
}
|
||||||
|
|
||||||
double _getTargetRadius(RenderBox referenceBox, bool containedInkWell, RectCallback rectCallback, Offset position) {
|
double _getTargetRadius(RenderBox referenceBox, bool containedInkWell, RectCallback rectCallback, Offset position) {
|
||||||
if (containedInkWell) {
|
|
||||||
final Size size = rectCallback != null ? rectCallback().size : referenceBox.size;
|
final Size size = rectCallback != null ? rectCallback().size : referenceBox.size;
|
||||||
return _getRippleRadiusForPositionInSize(size, position);
|
final double d1 = size.bottomRight(Offset.zero).distance;
|
||||||
}
|
final double d2 = (size.topRight(Offset.zero) - size.bottomLeft(Offset.zero)).distance;
|
||||||
return Material.defaultSplashRadius;
|
return math.max(d1, d2) / 2.0;
|
||||||
}
|
|
||||||
|
|
||||||
double _getRippleRadiusForPositionInSize(Size bounds, Offset position) {
|
|
||||||
final double d1 = (position - bounds.topLeft(Offset.zero)).distance;
|
|
||||||
final double d2 = (position - bounds.topRight(Offset.zero)).distance;
|
|
||||||
final double d3 = (position - bounds.bottomLeft(Offset.zero)).distance;
|
|
||||||
final double d4 = (position - bounds.bottomRight(Offset.zero)).distance;
|
|
||||||
return math.max(math.max(d1, d2), math.max(d3, d4)).ceilToDouble();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _InkRippleFactory extends InteractiveInkFeatureFactory {
|
class _InkRippleFactory extends InteractiveInkFeatureFactory {
|
||||||
@ -205,7 +196,9 @@ class InkRipple extends InteractiveInkFeature {
|
|||||||
@override
|
@override
|
||||||
void cancel() {
|
void cancel() {
|
||||||
_fadeInController.stop();
|
_fadeInController.stop();
|
||||||
_fadeOutController.animateTo(1.0, duration: _kCancelDuration);
|
_fadeOutController
|
||||||
|
..value = 1.0 - _fadeInController.value
|
||||||
|
..animateTo(1.0, duration: _kCancelDuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleAlphaStatusChanged(AnimationStatus status) {
|
void _handleAlphaStatusChanged(AnimationStatus status) {
|
||||||
|
@ -2,12 +2,16 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.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 'feedback.dart';
|
import 'feedback.dart';
|
||||||
|
import 'ink_well.dart' show InteractiveInkFeature;
|
||||||
import 'input_decorator.dart';
|
import 'input_decorator.dart';
|
||||||
import 'material.dart';
|
import 'material.dart';
|
||||||
import 'text_selection.dart';
|
import 'text_selection.dart';
|
||||||
@ -275,9 +279,12 @@ class TextField extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TextFieldState extends State<TextField> {
|
class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixin {
|
||||||
final GlobalKey<EditableTextState> _editableTextKey = new GlobalKey<EditableTextState>();
|
final GlobalKey<EditableTextState> _editableTextKey = new GlobalKey<EditableTextState>();
|
||||||
|
|
||||||
|
Set<InteractiveInkFeature> _splashes;
|
||||||
|
InteractiveInkFeature _currentSplash;
|
||||||
|
|
||||||
TextEditingController _controller;
|
TextEditingController _controller;
|
||||||
TextEditingController get _effectiveController => widget.controller ?? _controller;
|
TextEditingController get _effectiveController => widget.controller ?? _controller;
|
||||||
|
|
||||||
@ -332,13 +339,104 @@ class _TextFieldState extends State<TextField> {
|
|||||||
_editableTextKey.currentState?.requestKeyboard();
|
_editableTextKey.currentState?.requestKeyboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSelectionChanged(BuildContext context, SelectionChangedCause cause) {
|
void _handleSelectionChanged(TextSelection selection, SelectionChangedCause cause) {
|
||||||
if (cause == SelectionChangedCause.longPress)
|
if (cause == SelectionChangedCause.longPress)
|
||||||
Feedback.forLongPress(context);
|
Feedback.forLongPress(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
InteractiveInkFeature _createInkFeature(TapDownDetails details) {
|
||||||
|
final MaterialInkController inkController = Material.of(context);
|
||||||
|
final RenderBox referenceBox = InputDecorator.containerOf(_editableTextKey.currentContext);
|
||||||
|
final Offset position = referenceBox.globalToLocal(details.globalPosition);
|
||||||
|
final Color color = Theme.of(context).splashColor;
|
||||||
|
|
||||||
|
InteractiveInkFeature splash;
|
||||||
|
void handleRemoved() {
|
||||||
|
if (_splashes != null) {
|
||||||
|
assert(_splashes.contains(splash));
|
||||||
|
_splashes.remove(splash);
|
||||||
|
if (_currentSplash == splash)
|
||||||
|
_currentSplash = null;
|
||||||
|
updateKeepAlive();
|
||||||
|
} // else we're probably in deactivate()
|
||||||
|
}
|
||||||
|
|
||||||
|
splash = Theme.of(context).splashFactory.create(
|
||||||
|
controller: inkController,
|
||||||
|
referenceBox: referenceBox,
|
||||||
|
position: position,
|
||||||
|
color: color,
|
||||||
|
containedInkWell: true,
|
||||||
|
// TODO(hansmuller): splash clip borderRadius should match the input decorator's border.
|
||||||
|
borderRadius: BorderRadius.zero,
|
||||||
|
onRemoved: handleRemoved,
|
||||||
|
);
|
||||||
|
|
||||||
|
return splash;
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderEditable get _renderEditable => _editableTextKey.currentState.renderEditable;
|
||||||
|
|
||||||
|
void _handleTapDown(TapDownDetails details) {
|
||||||
|
_renderEditable.handleTapDown(details);
|
||||||
|
_startSplash(details);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleTap() {
|
||||||
|
_renderEditable.handleTap();
|
||||||
|
_requestKeyboard();
|
||||||
|
_confirmCurrentSplash();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleTapCancel() {
|
||||||
|
_renderEditable.handleTapCancel();
|
||||||
|
_cancelCurrentSplash();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleLongPress() {
|
||||||
|
_renderEditable.handleLongPress();
|
||||||
|
_confirmCurrentSplash();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startSplash(TapDownDetails details) {
|
||||||
|
if (_effectiveFocusNode.hasFocus)
|
||||||
|
return;
|
||||||
|
final InteractiveInkFeature splash = _createInkFeature(details);
|
||||||
|
_splashes ??= new HashSet<InteractiveInkFeature>();
|
||||||
|
_splashes.add(splash);
|
||||||
|
_currentSplash = splash;
|
||||||
|
updateKeepAlive();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _confirmCurrentSplash() {
|
||||||
|
_currentSplash?.confirm();
|
||||||
|
_currentSplash = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _cancelCurrentSplash() {
|
||||||
|
_currentSplash?.cancel();
|
||||||
|
_currentSplash = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get wantKeepAlive => _splashes != null && _splashes.isNotEmpty;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void deactivate() {
|
||||||
|
if (_splashes != null) {
|
||||||
|
final Set<InteractiveInkFeature> splashes = _splashes;
|
||||||
|
_splashes = null;
|
||||||
|
for (InteractiveInkFeature splash in splashes)
|
||||||
|
splash.dispose();
|
||||||
|
_currentSplash = null;
|
||||||
|
}
|
||||||
|
assert(_currentSplash == null);
|
||||||
|
super.deactivate();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context); // See AutomaticKeepAliveClientMixin.
|
||||||
final ThemeData themeData = Theme.of(context);
|
final ThemeData themeData = Theme.of(context);
|
||||||
final TextStyle style = widget.style ?? themeData.textTheme.subhead;
|
final TextStyle style = widget.style ?? themeData.textTheme.subhead;
|
||||||
final TextEditingController controller = _effectiveController;
|
final TextEditingController controller = _effectiveController;
|
||||||
@ -366,8 +464,9 @@ class _TextFieldState extends State<TextField> {
|
|||||||
: materialTextSelectionControls,
|
: materialTextSelectionControls,
|
||||||
onChanged: widget.onChanged,
|
onChanged: widget.onChanged,
|
||||||
onSubmitted: widget.onSubmitted,
|
onSubmitted: widget.onSubmitted,
|
||||||
onSelectionChanged: (TextSelection _, SelectionChangedCause cause) => _onSelectionChanged(context, cause),
|
onSelectionChanged: _handleSelectionChanged,
|
||||||
inputFormatters: formatters,
|
inputFormatters: formatters,
|
||||||
|
rendererIgnoresPointer: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -395,10 +494,13 @@ class _TextFieldState extends State<TextField> {
|
|||||||
_requestKeyboard();
|
_requestKeyboard();
|
||||||
},
|
},
|
||||||
child: new GestureDetector(
|
child: new GestureDetector(
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.translucent,
|
||||||
onTap: _requestKeyboard,
|
onTapDown: _handleTapDown,
|
||||||
child: child,
|
onTap: _handleTap,
|
||||||
|
onTapCancel: _handleTapCancel,
|
||||||
|
onLongPress: _handleLongPress,
|
||||||
excludeFromSemantics: true,
|
excludeFromSemantics: true,
|
||||||
|
child: child,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -131,11 +131,13 @@ class RenderEditable extends RenderBox {
|
|||||||
@required ViewportOffset offset,
|
@required ViewportOffset offset,
|
||||||
this.onSelectionChanged,
|
this.onSelectionChanged,
|
||||||
this.onCaretChanged,
|
this.onCaretChanged,
|
||||||
|
this.ignorePointer: false,
|
||||||
}) : assert(textAlign != null),
|
}) : assert(textAlign != null),
|
||||||
assert(textDirection != null, 'RenderEditable created without a textDirection.'),
|
assert(textDirection != null, 'RenderEditable created without a textDirection.'),
|
||||||
assert(maxLines == null || maxLines > 0),
|
assert(maxLines == null || maxLines > 0),
|
||||||
assert(textScaleFactor != null),
|
assert(textScaleFactor != null),
|
||||||
assert(offset != null),
|
assert(offset != null),
|
||||||
|
assert(ignorePointer != null),
|
||||||
_textPainter = new TextPainter(
|
_textPainter = new TextPainter(
|
||||||
text: text,
|
text: text,
|
||||||
textAlign: textAlign,
|
textAlign: textAlign,
|
||||||
@ -167,6 +169,13 @@ class RenderEditable extends RenderBox {
|
|||||||
/// Called during the paint phase when the caret location changes.
|
/// Called during the paint phase when the caret location changes.
|
||||||
CaretChangedHandler onCaretChanged;
|
CaretChangedHandler onCaretChanged;
|
||||||
|
|
||||||
|
/// If true [handleEvent] does nothing and it's assumed that this
|
||||||
|
/// renderer will be notified of input gestures via [handleTapDown],
|
||||||
|
/// [handleTap], [handleTapCancel], and [handleLongPress].
|
||||||
|
///
|
||||||
|
/// The default value of this property is false.
|
||||||
|
bool ignorePointer;
|
||||||
|
|
||||||
Rect _lastCaretRect;
|
Rect _lastCaretRect;
|
||||||
|
|
||||||
/// Marks the render object as needing to be laid out again and have its text
|
/// Marks the render object as needing to be laid out again and have its text
|
||||||
@ -550,6 +559,8 @@ class RenderEditable extends RenderBox {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
|
void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
|
||||||
|
if (ignorePointer)
|
||||||
|
return;
|
||||||
assert(debugHandleEvent(event, entry));
|
assert(debugHandleEvent(event, entry));
|
||||||
if (event is PointerDownEvent && onSelectionChanged != null) {
|
if (event is PointerDownEvent && onSelectionChanged != null) {
|
||||||
_tap.addPointer(event);
|
_tap.addPointer(event);
|
||||||
@ -559,11 +570,15 @@ class RenderEditable extends RenderBox {
|
|||||||
|
|
||||||
Offset _lastTapDownPosition;
|
Offset _lastTapDownPosition;
|
||||||
Offset _longPressPosition;
|
Offset _longPressPosition;
|
||||||
void _handleTapDown(TapDownDetails details) {
|
void handleTapDown(TapDownDetails details) {
|
||||||
_lastTapDownPosition = details.globalPosition + -_paintOffset;
|
_lastTapDownPosition = details.globalPosition + -_paintOffset;
|
||||||
}
|
}
|
||||||
|
void _handleTapDown(TapDownDetails details) {
|
||||||
|
assert(!ignorePointer);
|
||||||
|
handleTapDown(details);
|
||||||
|
}
|
||||||
|
|
||||||
void _handleTap() {
|
void handleTap() {
|
||||||
_layoutText(constraints.maxWidth);
|
_layoutText(constraints.maxWidth);
|
||||||
assert(_lastTapDownPosition != null);
|
assert(_lastTapDownPosition != null);
|
||||||
final Offset globalPosition = _lastTapDownPosition;
|
final Offset globalPosition = _lastTapDownPosition;
|
||||||
@ -573,14 +588,22 @@ class RenderEditable extends RenderBox {
|
|||||||
onSelectionChanged(new TextSelection.fromPosition(position), this, SelectionChangedCause.tap);
|
onSelectionChanged(new TextSelection.fromPosition(position), this, SelectionChangedCause.tap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
void _handleTap() {
|
||||||
|
assert(!ignorePointer);
|
||||||
|
handleTap();
|
||||||
|
}
|
||||||
|
|
||||||
void _handleTapCancel() {
|
void handleTapCancel() {
|
||||||
// longPress arrives after tapCancel, so remember the tap position.
|
// longPress arrives after tapCancel, so remember the tap position.
|
||||||
_longPressPosition = _lastTapDownPosition;
|
_longPressPosition = _lastTapDownPosition;
|
||||||
_lastTapDownPosition = null;
|
_lastTapDownPosition = null;
|
||||||
}
|
}
|
||||||
|
void _handleTapCancel() {
|
||||||
|
assert(!ignorePointer);
|
||||||
|
handleTapCancel();
|
||||||
|
}
|
||||||
|
|
||||||
void _handleLongPress() {
|
void handleLongPress() {
|
||||||
_layoutText(constraints.maxWidth);
|
_layoutText(constraints.maxWidth);
|
||||||
final Offset globalPosition = _longPressPosition;
|
final Offset globalPosition = _longPressPosition;
|
||||||
_longPressPosition = null;
|
_longPressPosition = null;
|
||||||
@ -589,6 +612,10 @@ class RenderEditable extends RenderBox {
|
|||||||
onSelectionChanged(_selectWordAtOffset(position), this, SelectionChangedCause.longPress);
|
onSelectionChanged(_selectWordAtOffset(position), this, SelectionChangedCause.longPress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
void _handleLongPress() {
|
||||||
|
assert(!ignorePointer);
|
||||||
|
handleLongPress();
|
||||||
|
}
|
||||||
|
|
||||||
TextSelection _selectWordAtOffset(TextPosition position) {
|
TextSelection _selectWordAtOffset(TextPosition position) {
|
||||||
assert(_textLayoutLastWidth == constraints.maxWidth);
|
assert(_textLayoutLastWidth == constraints.maxWidth);
|
||||||
|
@ -149,8 +149,8 @@ class EditableText extends StatefulWidget {
|
|||||||
/// [TextInputType.text] unless [maxLines] is greater than one, when it will
|
/// [TextInputType.text] unless [maxLines] is greater than one, when it will
|
||||||
/// default to [TextInputType.multiline].
|
/// default to [TextInputType.multiline].
|
||||||
///
|
///
|
||||||
/// The [controller], [focusNode], [style], [cursorColor], and [textAlign]
|
/// The [controller], [focusNode], [style], [cursorColor], [textAlign],
|
||||||
/// arguments must not be null.
|
/// and [rendererIgnoresPointer], arguments must not be null.
|
||||||
EditableText({
|
EditableText({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.controller,
|
@required this.controller,
|
||||||
@ -171,6 +171,7 @@ class EditableText extends StatefulWidget {
|
|||||||
this.onSubmitted,
|
this.onSubmitted,
|
||||||
this.onSelectionChanged,
|
this.onSelectionChanged,
|
||||||
List<TextInputFormatter> inputFormatters,
|
List<TextInputFormatter> inputFormatters,
|
||||||
|
this.rendererIgnoresPointer: false,
|
||||||
}) : assert(controller != null),
|
}) : assert(controller != null),
|
||||||
assert(focusNode != null),
|
assert(focusNode != null),
|
||||||
assert(obscureText != null),
|
assert(obscureText != null),
|
||||||
@ -180,6 +181,7 @@ class EditableText extends StatefulWidget {
|
|||||||
assert(textAlign != null),
|
assert(textAlign != null),
|
||||||
assert(maxLines == null || maxLines > 0),
|
assert(maxLines == null || maxLines > 0),
|
||||||
assert(autofocus != null),
|
assert(autofocus != null),
|
||||||
|
assert(rendererIgnoresPointer != null),
|
||||||
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
||||||
inputFormatters = maxLines == 1
|
inputFormatters = maxLines == 1
|
||||||
? (
|
? (
|
||||||
@ -279,6 +281,12 @@ class EditableText extends StatefulWidget {
|
|||||||
/// in the provided order when the text input changes.
|
/// in the provided order when the text input changes.
|
||||||
final List<TextInputFormatter> inputFormatters;
|
final List<TextInputFormatter> inputFormatters;
|
||||||
|
|
||||||
|
/// If true, the [RenderEditable] created by this widget will not handle
|
||||||
|
/// pointer events, see [renderEditable] and [RenderEditable.ignorePointer].
|
||||||
|
///
|
||||||
|
/// This property is false by default.
|
||||||
|
final bool rendererIgnoresPointer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
EditableTextState createState() => new EditableTextState();
|
EditableTextState createState() => new EditableTextState();
|
||||||
|
|
||||||
@ -303,6 +311,7 @@ class EditableText extends StatefulWidget {
|
|||||||
class EditableTextState extends State<EditableText> with AutomaticKeepAliveClientMixin implements TextInputClient {
|
class EditableTextState extends State<EditableText> with AutomaticKeepAliveClientMixin implements TextInputClient {
|
||||||
Timer _cursorTimer;
|
Timer _cursorTimer;
|
||||||
final ValueNotifier<bool> _showCursor = new ValueNotifier<bool>(false);
|
final ValueNotifier<bool> _showCursor = new ValueNotifier<bool>(false);
|
||||||
|
final GlobalKey _editableKey = new GlobalKey();
|
||||||
|
|
||||||
TextInputConnection _textInputConnection;
|
TextInputConnection _textInputConnection;
|
||||||
TextSelectionOverlay _selectionOverlay;
|
TextSelectionOverlay _selectionOverlay;
|
||||||
@ -628,6 +637,12 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The renderer for this widget's [Editable] descendant.
|
||||||
|
///
|
||||||
|
/// This property is typically used to notify the renderer of input gestures
|
||||||
|
/// when [ignorePointer] is true. See [RenderEditable.ignorePointer].
|
||||||
|
RenderEditable get renderEditable => _editableKey.currentContext.findRenderObject();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
FocusScope.of(context).reparentIfNeeded(widget.focusNode);
|
FocusScope.of(context).reparentIfNeeded(widget.focusNode);
|
||||||
@ -640,6 +655,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
return new CompositedTransformTarget(
|
return new CompositedTransformTarget(
|
||||||
link: _layerLink,
|
link: _layerLink,
|
||||||
child: new _Editable(
|
child: new _Editable(
|
||||||
|
key: _editableKey,
|
||||||
value: _value,
|
value: _value,
|
||||||
style: widget.style,
|
style: widget.style,
|
||||||
cursorColor: widget.cursorColor,
|
cursorColor: widget.cursorColor,
|
||||||
@ -656,6 +672,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
offset: offset,
|
offset: offset,
|
||||||
onSelectionChanged: _handleSelectionChanged,
|
onSelectionChanged: _handleSelectionChanged,
|
||||||
onCaretChanged: _handleCaretChanged,
|
onCaretChanged: _handleCaretChanged,
|
||||||
|
rendererIgnoresPointer: widget.rendererIgnoresPointer,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -682,7 +699,9 @@ class _Editable extends LeafRenderObjectWidget {
|
|||||||
this.offset,
|
this.offset,
|
||||||
this.onSelectionChanged,
|
this.onSelectionChanged,
|
||||||
this.onCaretChanged,
|
this.onCaretChanged,
|
||||||
|
this.rendererIgnoresPointer: false,
|
||||||
}) : assert(textDirection != null),
|
}) : assert(textDirection != null),
|
||||||
|
assert(rendererIgnoresPointer != null),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
final TextEditingValue value;
|
final TextEditingValue value;
|
||||||
@ -701,6 +720,7 @@ class _Editable extends LeafRenderObjectWidget {
|
|||||||
final ViewportOffset offset;
|
final ViewportOffset offset;
|
||||||
final SelectionChangedHandler onSelectionChanged;
|
final SelectionChangedHandler onSelectionChanged;
|
||||||
final CaretChangedHandler onCaretChanged;
|
final CaretChangedHandler onCaretChanged;
|
||||||
|
final bool rendererIgnoresPointer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RenderEditable createRenderObject(BuildContext context) {
|
RenderEditable createRenderObject(BuildContext context) {
|
||||||
@ -718,6 +738,7 @@ class _Editable extends LeafRenderObjectWidget {
|
|||||||
offset: offset,
|
offset: offset,
|
||||||
onSelectionChanged: onSelectionChanged,
|
onSelectionChanged: onSelectionChanged,
|
||||||
onCaretChanged: onCaretChanged,
|
onCaretChanged: onCaretChanged,
|
||||||
|
ignorePointer: rendererIgnoresPointer,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -736,7 +757,8 @@ class _Editable extends LeafRenderObjectWidget {
|
|||||||
..selection = value.selection
|
..selection = value.selection
|
||||||
..offset = offset
|
..offset = offset
|
||||||
..onSelectionChanged = onSelectionChanged
|
..onSelectionChanged = onSelectionChanged
|
||||||
..onCaretChanged = onCaretChanged;
|
..onCaretChanged = onCaretChanged
|
||||||
|
..ignorePointer = rendererIgnoresPointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
TextSpan get _styledTextSpan {
|
TextSpan get _styledTextSpan {
|
||||||
|
@ -140,7 +140,8 @@ void main() {
|
|||||||
// At this point the splash radius has expanded to its limit: 5 past the
|
// At this point the splash radius has expanded to its limit: 5 past the
|
||||||
// ink well's radius parameter. The splash center has moved to its final
|
// ink well's radius parameter. The splash center has moved to its final
|
||||||
// location at the inkwell's center and the fade-out is about to start.
|
// location at the inkwell's center and the fade-out is about to start.
|
||||||
await tester.pump(const Duration(milliseconds: 225));
|
// The fade-out begins at 225ms = 50ms + 25ms + 150ms.
|
||||||
|
await tester.pump(const Duration(milliseconds: 150));
|
||||||
expect(box, paints..something((Symbol method, List<dynamic> arguments) {
|
expect(box, paints..something((Symbol method, List<dynamic> arguments) {
|
||||||
if (method != #drawCircle)
|
if (method != #drawCircle)
|
||||||
return false;
|
return false;
|
||||||
|
194
packages/flutter/test/material/text_field_splash_test.dart
Normal file
194
packages/flutter/test/material/text_field_splash_test.dart
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
|
int confirmCount = 0;
|
||||||
|
int cancelCount = 0;
|
||||||
|
|
||||||
|
class TestInkSplash extends InkSplash {
|
||||||
|
TestInkSplash({
|
||||||
|
MaterialInkController controller,
|
||||||
|
RenderBox referenceBox,
|
||||||
|
Offset position,
|
||||||
|
Color color,
|
||||||
|
bool containedInkWell: false,
|
||||||
|
RectCallback rectCallback,
|
||||||
|
BorderRadius borderRadius,
|
||||||
|
double radius,
|
||||||
|
VoidCallback onRemoved,
|
||||||
|
}) : super(
|
||||||
|
controller: controller,
|
||||||
|
referenceBox: referenceBox,
|
||||||
|
position: position,
|
||||||
|
color: color,
|
||||||
|
containedInkWell: containedInkWell,
|
||||||
|
rectCallback: rectCallback,
|
||||||
|
borderRadius: borderRadius,
|
||||||
|
radius: radius,
|
||||||
|
onRemoved: onRemoved,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void confirm() {
|
||||||
|
confirmCount += 1;
|
||||||
|
super.confirm();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void cancel() {
|
||||||
|
cancelCount += 1;
|
||||||
|
super.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestInkSplashFactory extends InteractiveInkFeatureFactory {
|
||||||
|
const TestInkSplashFactory();
|
||||||
|
|
||||||
|
@override
|
||||||
|
InteractiveInkFeature create({
|
||||||
|
MaterialInkController controller,
|
||||||
|
RenderBox referenceBox,
|
||||||
|
Offset position,
|
||||||
|
Color color,
|
||||||
|
bool containedInkWell: false,
|
||||||
|
RectCallback rectCallback,
|
||||||
|
BorderRadius borderRadius,
|
||||||
|
double radius,
|
||||||
|
VoidCallback onRemoved,
|
||||||
|
}) {
|
||||||
|
return new TestInkSplash(
|
||||||
|
controller: controller,
|
||||||
|
referenceBox: referenceBox,
|
||||||
|
position: position,
|
||||||
|
color: color,
|
||||||
|
containedInkWell: containedInkWell,
|
||||||
|
rectCallback: rectCallback,
|
||||||
|
borderRadius: borderRadius,
|
||||||
|
radius: radius,
|
||||||
|
onRemoved: onRemoved,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('Tap and no focus causes a splash', (WidgetTester tester) async {
|
||||||
|
final Key textField1 = new UniqueKey();
|
||||||
|
final Key textField2 = new UniqueKey();
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
new MaterialApp(
|
||||||
|
home: new Theme(
|
||||||
|
data: new ThemeData.light().copyWith(splashFactory: const TestInkSplashFactory()),
|
||||||
|
child: new Material(
|
||||||
|
child: new Container(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: new Column(
|
||||||
|
children: <Widget>[
|
||||||
|
new TextField(
|
||||||
|
key: textField1,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'label',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
new TextField(
|
||||||
|
key: textField2,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'label',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
confirmCount = 0;
|
||||||
|
cancelCount = 0;
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(textField1));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(confirmCount, 1);
|
||||||
|
expect(cancelCount, 0);
|
||||||
|
|
||||||
|
// textField1 already has the focus, no new splash
|
||||||
|
await tester.tap(find.byKey(textField1));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(confirmCount, 1);
|
||||||
|
expect(cancelCount, 0);
|
||||||
|
|
||||||
|
// textField2 gets the focus and a splash
|
||||||
|
await tester.tap(find.byKey(textField2));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(confirmCount, 2);
|
||||||
|
expect(cancelCount, 0);
|
||||||
|
|
||||||
|
// Tap outside of textField1's editable. It still gets focus and splash.
|
||||||
|
await tester.tapAt(tester.getTopLeft(find.byKey(textField1)));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(confirmCount, 3);
|
||||||
|
expect(cancelCount, 0);
|
||||||
|
|
||||||
|
// Tap in the center of textField2's editable. It still gets the focus
|
||||||
|
// and the splash. There is no splash cancel.
|
||||||
|
await tester.tap(find.byKey(textField2));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(confirmCount, 4);
|
||||||
|
expect(cancelCount, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Splash cancel', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
new MaterialApp(
|
||||||
|
home: new Theme(
|
||||||
|
data: new ThemeData.light().copyWith(splashFactory: const TestInkSplashFactory()),
|
||||||
|
child: new Material(
|
||||||
|
child: new ListView(
|
||||||
|
children: <Widget>[
|
||||||
|
const TextField(
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'label1',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const TextField(
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'label2',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
new Container(
|
||||||
|
height: 1000.0,
|
||||||
|
color: const Color(0xFF00FF00),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
confirmCount = 0;
|
||||||
|
cancelCount = 0;
|
||||||
|
|
||||||
|
// Pointer is dragged below the textfield, splash is canceled.
|
||||||
|
final TestGesture gesture1 = await tester.startGesture(tester.getCenter(find.text('label1')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await gesture1.moveTo(const Offset(400.0, 300.0));
|
||||||
|
await gesture1.up();
|
||||||
|
expect(confirmCount, 0);
|
||||||
|
expect(cancelCount, 1);
|
||||||
|
|
||||||
|
// Pointer is dragged upwards causing a scroll, splash is canceled.
|
||||||
|
final TestGesture gesture2 = await tester.startGesture(tester.getCenter(find.text('label2')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await gesture2.moveBy(const Offset(0.0, -200.0), timeStamp: const Duration(milliseconds: 32));
|
||||||
|
await gesture2.up();
|
||||||
|
expect(confirmCount, 0);
|
||||||
|
expect(cancelCount, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user