Created
November 20, 2019 17:06
-
-
Save andrea689/969653dc4f697b8478d1d167f8d2588f to your computer and use it in GitHub Desktop.
TextFieldCustom
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2015 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 'dart:math' as math; | |
import 'dart:ui' show lerpDouble; | |
import 'package:flutter/foundation.dart'; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter/rendering.dart'; | |
import 'package:flutter/widgets.dart'; | |
const Duration _kTransitionDuration = Duration(milliseconds: 200); | |
const Curve _kTransitionCurve = Curves.fastOutSlowIn; | |
// Defines the gap in the InputDecorator's outline border where the | |
// floating label will appear. | |
class _InputBorderGap extends ChangeNotifier { | |
double _start; | |
double get start => _start; | |
set start(double value) { | |
if (value != _start) { | |
_start = value; | |
notifyListeners(); | |
} | |
} | |
double _extent = 0.0; | |
double get extent => _extent; | |
set extent(double value) { | |
if (value != _extent) { | |
_extent = value; | |
notifyListeners(); | |
} | |
} | |
@override | |
bool operator ==(dynamic other) { | |
if (identical(this, other)) | |
return true; | |
if (runtimeType != other.runtimeType) | |
return false; | |
final _InputBorderGap typedOther = other; | |
return typedOther.start == start && typedOther.extent == extent; | |
} | |
@override | |
int get hashCode => hashValues(start, extent); | |
} | |
// Used to interpolate between two InputBorders. | |
class _InputBorderTween extends Tween<InputBorder> { | |
_InputBorderTween({InputBorder begin, InputBorder end}) : super(begin: begin, end: end); | |
@override | |
InputBorder lerp(double t) => ShapeBorder.lerp(begin, end, t); | |
} | |
// Passes the _InputBorderGap parameters along to an InputBorder's paint method. | |
class _InputBorderPainter extends CustomPainter { | |
_InputBorderPainter({ | |
@required Listenable repaint, | |
@required this.borderAnimation, | |
@required this.border, | |
@required this.gapAnimation, | |
@required this.gap, | |
@required this.textDirection, | |
@required this.fillColor, | |
@required this.hoverAnimation, | |
@required this.hoverColorTween, | |
}) : super(repaint: repaint); | |
final Animation<double> borderAnimation; | |
final _InputBorderTween border; | |
final Animation<double> gapAnimation; | |
final _InputBorderGap gap; | |
final TextDirection textDirection; | |
final Color fillColor; | |
final ColorTween hoverColorTween; | |
final Animation<double> hoverAnimation; | |
Color get blendedColor => Color.alphaBlend(hoverColorTween.evaluate(hoverAnimation), fillColor); | |
@override | |
void paint(Canvas canvas, Size size) { | |
final InputBorder borderValue = border.evaluate(borderAnimation); | |
final Rect canvasRect = Offset.zero & size; | |
final Color blendedFillColor = blendedColor; | |
if (blendedFillColor.alpha > 0) { | |
canvas.drawPath( | |
borderValue.getOuterPath(canvasRect, textDirection: textDirection), | |
Paint() | |
..color = blendedFillColor | |
..style = PaintingStyle.fill, | |
); | |
} | |
borderValue.paint( | |
canvas, | |
canvasRect, | |
gapStart: gap.start, | |
gapExtent: gap.extent, | |
gapPercentage: gapAnimation.value, | |
textDirection: textDirection, | |
); | |
} | |
@override | |
bool shouldRepaint(_InputBorderPainter oldPainter) { | |
return borderAnimation != oldPainter.borderAnimation | |
|| hoverAnimation != oldPainter.hoverAnimation | |
|| gapAnimation != oldPainter.gapAnimation | |
|| border != oldPainter.border | |
|| gap != oldPainter.gap | |
|| textDirection != oldPainter.textDirection; | |
} | |
} | |
// An analog of AnimatedContainer, which can animate its shaped border, for | |
// _InputBorder. This specialized animated container is needed because the | |
// _InputBorderGap, which is computed at layout time, is required by the | |
// _InputBorder's paint method. | |
class _BorderContainer extends StatefulWidget { | |
const _BorderContainer({ | |
Key key, | |
@required this.border, | |
@required this.gap, | |
@required this.gapAnimation, | |
@required this.fillColor, | |
@required this.hoverColor, | |
@required this.isHovering, | |
this.child, | |
}) : assert(border != null), | |
assert(gap != null), | |
assert(fillColor != null), | |
super(key: key); | |
final InputBorder border; | |
final _InputBorderGap gap; | |
final Animation<double> gapAnimation; | |
final Color fillColor; | |
final Color hoverColor; | |
final bool isHovering; | |
final Widget child; | |
@override | |
_BorderContainerState createState() => _BorderContainerState(); | |
} | |
class _BorderContainerState extends State<_BorderContainer> with TickerProviderStateMixin { | |
static const Duration _kHoverDuration = Duration(milliseconds: 15); | |
AnimationController _controller; | |
AnimationController _hoverColorController; | |
Animation<double> _borderAnimation; | |
_InputBorderTween _border; | |
Animation<double> _hoverAnimation; | |
ColorTween _hoverColorTween; | |
@override | |
void initState() { | |
super.initState(); | |
_hoverColorController = AnimationController( | |
duration: _kHoverDuration, | |
value: widget.isHovering ? 1.0 : 0.0, | |
vsync: this, | |
); | |
_controller = AnimationController( | |
duration: _kTransitionDuration, | |
vsync: this, | |
); | |
_borderAnimation = CurvedAnimation( | |
parent: _controller, | |
curve: _kTransitionCurve, | |
); | |
_border = _InputBorderTween( | |
begin: widget.border, | |
end: widget.border, | |
); | |
_hoverAnimation = CurvedAnimation( | |
parent: _hoverColorController, | |
curve: Curves.linear, | |
); | |
_hoverColorTween = ColorTween(begin: Colors.transparent, end: widget.hoverColor); | |
} | |
@override | |
void dispose() { | |
_controller.dispose(); | |
_hoverColorController.dispose(); | |
super.dispose(); | |
} | |
@override | |
void didUpdateWidget(_BorderContainer oldWidget) { | |
super.didUpdateWidget(oldWidget); | |
if (widget.border != oldWidget.border) { | |
_border = _InputBorderTween( | |
begin: oldWidget.border, | |
end: widget.border, | |
); | |
_controller | |
..value = 0.0 | |
..forward(); | |
} | |
if (widget.hoverColor != oldWidget.hoverColor) { | |
_hoverColorTween = ColorTween(begin: Colors.transparent, end: widget.hoverColor); | |
} | |
if (widget.isHovering != oldWidget.isHovering) { | |
if (widget.isHovering) { | |
_hoverColorController.forward(); | |
} else { | |
_hoverColorController.reverse(); | |
} | |
} | |
} | |
@override | |
Widget build(BuildContext context) { | |
return CustomPaint( | |
foregroundPainter: _InputBorderPainter( | |
repaint: Listenable.merge(<Listenable>[ | |
_borderAnimation, | |
widget.gap, | |
_hoverColorController, | |
]), | |
borderAnimation: _borderAnimation, | |
border: _border, | |
gapAnimation: widget.gapAnimation, | |
gap: widget.gap, | |
textDirection: Directionality.of(context), | |
fillColor: widget.fillColor, | |
hoverColorTween: _hoverColorTween, | |
hoverAnimation: _hoverAnimation, | |
), | |
child: widget.child, | |
); | |
} | |
} | |
// Used to "shake" the floating label to the left to the left and right | |
// when the errorText first appears. | |
class _Shaker extends AnimatedWidget { | |
const _Shaker({ | |
Key key, | |
Animation<double> animation, | |
this.child, | |
}) : super(key: key, listenable: animation); | |
final Widget child; | |
Animation<double> get animation => listenable; | |
double get translateX { | |
const double shakeDelta = 4.0; | |
final double t = animation.value; | |
if (t <= 0.25) | |
return -t * shakeDelta; | |
else if (t < 0.75) | |
return (t - 0.5) * shakeDelta; | |
else | |
return (1.0 - t) * 4.0 * shakeDelta; | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Transform( | |
transform: Matrix4.translationValues(translateX, 0.0, 0.0), | |
child: child, | |
); | |
} | |
} | |
// Display the helper and error text. When the error text appears | |
// it fades and the helper text fades out. The error text also | |
// slides upwards a little when it first appears. | |
class _HelperError extends StatefulWidget { | |
const _HelperError({ | |
Key key, | |
this.textAlign, | |
this.helperText, | |
this.helperStyle, | |
this.errorText, | |
this.errorStyle, | |
this.errorMaxLines, | |
}) : super(key: key); | |
final TextAlign textAlign; | |
final String helperText; | |
final TextStyle helperStyle; | |
final String errorText; | |
final TextStyle errorStyle; | |
final int errorMaxLines; | |
@override | |
_HelperErrorState createState() => _HelperErrorState(); | |
} | |
class _HelperErrorState extends State<_HelperError> with SingleTickerProviderStateMixin { | |
// If the height of this widget and the counter are zero ("empty") at | |
// layout time, no space is allocated for the subtext. | |
static const Widget empty = SizedBox(); | |
AnimationController _controller; | |
Widget _helper; | |
Widget _error; | |
@override | |
void initState() { | |
super.initState(); | |
_controller = AnimationController( | |
duration: _kTransitionDuration, | |
vsync: this, | |
); | |
if (widget.errorText != null) { | |
_error = _buildError(); | |
_controller.value = 1.0; | |
} else if (widget.helperText != null) { | |
_helper = _buildHelper(); | |
} | |
_controller.addListener(_handleChange); | |
} | |
@override | |
void dispose() { | |
_controller.dispose(); | |
super.dispose(); | |
} | |
void _handleChange() { | |
setState(() { | |
// The _controller's value has changed. | |
}); | |
} | |
@override | |
void didUpdateWidget(_HelperError old) { | |
super.didUpdateWidget(old); | |
final String newErrorText = widget.errorText; | |
final String newHelperText = widget.helperText; | |
final String oldErrorText = old.errorText; | |
final String oldHelperText = old.helperText; | |
final bool errorTextStateChanged = (newErrorText != null) != (oldErrorText != null); | |
final bool helperTextStateChanged = newErrorText == null && (newHelperText != null) != (oldHelperText != null); | |
if (errorTextStateChanged || helperTextStateChanged) { | |
if (newErrorText != null) { | |
_error = _buildError(); | |
_controller.forward(); | |
} else if (newHelperText != null) { | |
_helper = _buildHelper(); | |
_controller.reverse(); | |
} else { | |
_controller.reverse(); | |
} | |
} | |
} | |
Widget _buildHelper() { | |
assert(widget.helperText != null); | |
return Semantics( | |
container: true, | |
child: Opacity( | |
opacity: 1.0 - _controller.value, | |
child: Text( | |
widget.helperText, | |
style: widget.helperStyle, | |
textAlign: widget.textAlign, | |
overflow: TextOverflow.ellipsis, | |
), | |
), | |
); | |
} | |
Widget _buildError() { | |
assert(widget.errorText != null); | |
return Semantics( | |
container: true, | |
liveRegion: true, | |
child: Opacity( | |
opacity: _controller.value, | |
child: FractionalTranslation( | |
translation: Tween<Offset>( | |
begin: const Offset(0.0, -0.25), | |
end: const Offset(0.0, 0.0), | |
).evaluate(_controller.view), | |
child: Text( | |
widget.errorText, | |
style: widget.errorStyle, | |
textAlign: widget.textAlign, | |
overflow: TextOverflow.ellipsis, | |
maxLines: widget.errorMaxLines, | |
), | |
), | |
), | |
); | |
} | |
@override | |
Widget build(BuildContext context) { | |
if (_controller.isDismissed) { | |
_error = null; | |
if (widget.helperText != null) { | |
return _helper = _buildHelper(); | |
} else { | |
_helper = null; | |
return empty; | |
} | |
} | |
if (_controller.isCompleted) { | |
_helper = null; | |
if (widget.errorText != null) { | |
return _error = _buildError(); | |
} else { | |
_error = null; | |
return empty; | |
} | |
} | |
if (_helper == null && widget.errorText != null) | |
return _buildError(); | |
if (_error == null && widget.helperText != null) | |
return _buildHelper(); | |
if (widget.errorText != null) { | |
return Stack( | |
children: <Widget>[ | |
Opacity( | |
opacity: 1.0 - _controller.value, | |
child: _helper, | |
), | |
_buildError(), | |
], | |
); | |
} | |
if (widget.helperText != null) { | |
return Stack( | |
children: <Widget>[ | |
_buildHelper(), | |
Opacity( | |
opacity: _controller.value, | |
child: _error, | |
), | |
], | |
); | |
} | |
return empty; | |
} | |
} | |
// Identifies the children of a _RenderDecorationElement. | |
enum _DecorationSlot { | |
icon, | |
input, | |
label, | |
hint, | |
prefix, | |
suffix, | |
prefixIcon, | |
suffixIcon, | |
helperError, | |
counter, | |
container, | |
} | |
// An analog of InputDecoration for the _Decorator widget. | |
class _Decoration { | |
const _Decoration({ | |
@required this.contentPadding, | |
@required this.isCollapsed, | |
@required this.floatingLabelHeight, | |
@required this.floatingLabelProgress, | |
this.border, | |
this.borderGap, | |
this.icon, | |
this.input, | |
this.label, | |
this.hint, | |
this.prefix, | |
this.suffix, | |
this.prefixIcon, | |
this.suffixIcon, | |
this.helperError, | |
this.counter, | |
this.container, | |
this.alignLabelWithHint, | |
}) : assert(contentPadding != null), | |
assert(isCollapsed != null), | |
assert(floatingLabelHeight != null), | |
assert(floatingLabelProgress != null); | |
final EdgeInsetsGeometry contentPadding; | |
final bool isCollapsed; | |
final double floatingLabelHeight; | |
final double floatingLabelProgress; | |
final InputBorder border; | |
final _InputBorderGap borderGap; | |
final bool alignLabelWithHint; | |
final Widget icon; | |
final Widget input; | |
final Widget label; | |
final Widget hint; | |
final Widget prefix; | |
final Widget suffix; | |
final Widget prefixIcon; | |
final Widget suffixIcon; | |
final Widget helperError; | |
final Widget counter; | |
final Widget container; | |
@override | |
bool operator ==(dynamic other) { | |
if (identical(this, other)) | |
return true; | |
if (other.runtimeType != runtimeType) | |
return false; | |
final _Decoration typedOther = other; | |
return typedOther.contentPadding == contentPadding | |
&& typedOther.floatingLabelHeight == floatingLabelHeight | |
&& typedOther.floatingLabelProgress == floatingLabelProgress | |
&& typedOther.border == border | |
&& typedOther.borderGap == borderGap | |
&& typedOther.icon == icon | |
&& typedOther.input == input | |
&& typedOther.label == label | |
&& typedOther.hint == hint | |
&& typedOther.prefix == prefix | |
&& typedOther.suffix == suffix | |
&& typedOther.prefixIcon == prefixIcon | |
&& typedOther.suffixIcon == suffixIcon | |
&& typedOther.helperError == helperError | |
&& typedOther.counter == counter | |
&& typedOther.container == container | |
&& typedOther.alignLabelWithHint == alignLabelWithHint; | |
} | |
@override | |
int get hashCode { | |
return hashValues( | |
contentPadding, | |
floatingLabelHeight, | |
floatingLabelProgress, | |
border, | |
borderGap, | |
icon, | |
input, | |
label, | |
hint, | |
prefix, | |
suffix, | |
prefixIcon, | |
suffixIcon, | |
helperError, | |
counter, | |
container, | |
alignLabelWithHint, | |
); | |
} | |
} | |
// A container for the layout values computed by _RenderDecoration._layout. | |
// These values are used by _RenderDecoration.performLayout to position | |
// all of the renderer children of a _RenderDecoration. | |
class _RenderDecorationLayout { | |
const _RenderDecorationLayout({ | |
this.boxToBaseline, | |
this.inputBaseline, // for InputBorderType.underline | |
this.outlineBaseline, // for InputBorderType.outline | |
this.subtextBaseline, | |
this.containerHeight, | |
this.subtextHeight, | |
}); | |
final Map<RenderBox, double> boxToBaseline; | |
final double inputBaseline; | |
final double outlineBaseline; | |
final double subtextBaseline; // helper/error counter | |
final double containerHeight; | |
final double subtextHeight; | |
} | |
// The workhorse: layout and paint a _Decorator widget's _Decoration. | |
class _RenderDecoration extends RenderBox { | |
_RenderDecoration({ | |
@required _Decoration decoration, | |
@required TextDirection textDirection, | |
@required TextBaseline textBaseline, | |
@required bool isFocused, | |
@required bool expands, | |
TextAlignVertical textAlignVertical, | |
}) : assert(decoration != null), | |
assert(textDirection != null), | |
assert(textBaseline != null), | |
assert(expands != null), | |
_decoration = decoration, | |
_textDirection = textDirection, | |
_textBaseline = textBaseline, | |
_textAlignVertical = textAlignVertical, | |
_isFocused = isFocused, | |
_expands = expands; | |
static const double subtextGap = 8.0; | |
final Map<_DecorationSlot, RenderBox> slotToChild = <_DecorationSlot, RenderBox>{}; | |
final Map<RenderBox, _DecorationSlot> childToSlot = <RenderBox, _DecorationSlot>{}; | |
RenderBox _updateChild(RenderBox oldChild, RenderBox newChild, _DecorationSlot slot) { | |
if (oldChild != null) { | |
dropChild(oldChild); | |
childToSlot.remove(oldChild); | |
slotToChild.remove(slot); | |
} | |
if (newChild != null) { | |
childToSlot[newChild] = slot; | |
slotToChild[slot] = newChild; | |
adoptChild(newChild); | |
} | |
return newChild; | |
} | |
RenderBox _icon; | |
RenderBox get icon => _icon; | |
set icon(RenderBox value) { | |
_icon = _updateChild(_icon, value, _DecorationSlot.icon); | |
} | |
RenderBox _input; | |
RenderBox get input => _input; | |
set input(RenderBox value) { | |
_input = _updateChild(_input, value, _DecorationSlot.input); | |
} | |
RenderBox _label; | |
RenderBox get label => _label; | |
set label(RenderBox value) { | |
_label = _updateChild(_label, value, _DecorationSlot.label); | |
} | |
RenderBox _hint; | |
RenderBox get hint => _hint; | |
set hint(RenderBox value) { | |
_hint = _updateChild(_hint, value, _DecorationSlot.hint); | |
} | |
RenderBox _prefix; | |
RenderBox get prefix => _prefix; | |
set prefix(RenderBox value) { | |
_prefix = _updateChild(_prefix, value, _DecorationSlot.prefix); | |
} | |
RenderBox _suffix; | |
RenderBox get suffix => _suffix; | |
set suffix(RenderBox value) { | |
_suffix = _updateChild(_suffix, value, _DecorationSlot.suffix); | |
} | |
RenderBox _prefixIcon; | |
RenderBox get prefixIcon => _prefixIcon; | |
set prefixIcon(RenderBox value) { | |
_prefixIcon = _updateChild(_prefixIcon, value, _DecorationSlot.prefixIcon); | |
} | |
RenderBox _suffixIcon; | |
RenderBox get suffixIcon => _suffixIcon; | |
set suffixIcon(RenderBox value) { | |
_suffixIcon = _updateChild(_suffixIcon, value, _DecorationSlot.suffixIcon); | |
} | |
RenderBox _helperError; | |
RenderBox get helperError => _helperError; | |
set helperError(RenderBox value) { | |
_helperError = _updateChild(_helperError, value, _DecorationSlot.helperError); | |
} | |
RenderBox _counter; | |
RenderBox get counter => _counter; | |
set counter(RenderBox value) { | |
_counter = _updateChild(_counter, value, _DecorationSlot.counter); | |
} | |
RenderBox _container; | |
RenderBox get container => _container; | |
set container(RenderBox value) { | |
_container = _updateChild(_container, value, _DecorationSlot.container); | |
} | |
// The returned list is ordered for hit testing. | |
Iterable<RenderBox> get _children sync* { | |
if (icon != null) | |
yield icon; | |
if (input != null) | |
yield input; | |
if (prefixIcon != null) | |
yield prefixIcon; | |
if (suffixIcon != null) | |
yield suffixIcon; | |
if (prefix != null) | |
yield prefix; | |
if (suffix != null) | |
yield suffix; | |
if (label != null) | |
yield label; | |
if (hint != null) | |
yield hint; | |
if (helperError != null) | |
yield helperError; | |
if (counter != null) | |
yield counter; | |
if (container != null) | |
yield container; | |
} | |
_Decoration get decoration => _decoration; | |
_Decoration _decoration; | |
set decoration(_Decoration value) { | |
assert(value != null); | |
if (_decoration == value) | |
return; | |
_decoration = value; | |
markNeedsLayout(); | |
} | |
TextDirection get textDirection => _textDirection; | |
TextDirection _textDirection; | |
set textDirection(TextDirection value) { | |
assert(value != null); | |
if (_textDirection == value) | |
return; | |
_textDirection = value; | |
markNeedsLayout(); | |
} | |
TextBaseline get textBaseline => _textBaseline; | |
TextBaseline _textBaseline; | |
set textBaseline(TextBaseline value) { | |
assert(value != null); | |
if (_textBaseline == value) | |
return; | |
_textBaseline = value; | |
markNeedsLayout(); | |
} | |
TextAlignVertical get textAlignVertical { | |
if (_textAlignVertical == null) { | |
return _isOutlineAligned ? TextAlignVertical.center : TextAlignVertical.top; | |
} | |
return _textAlignVertical; | |
} | |
TextAlignVertical _textAlignVertical; | |
set textAlignVertical(TextAlignVertical value) { | |
assert(value != null); | |
if (_textAlignVertical == value) { | |
return; | |
} | |
// No need to relayout if the effective value is still the same. | |
if (textAlignVertical.y == value.y) { | |
_textAlignVertical = value; | |
return; | |
} | |
_textAlignVertical = value; | |
markNeedsLayout(); | |
} | |
bool get isFocused => _isFocused; | |
bool _isFocused; | |
set isFocused(bool value) { | |
assert(value != null); | |
if (_isFocused == value) | |
return; | |
_isFocused = value; | |
markNeedsSemanticsUpdate(); | |
} | |
bool get expands => _expands; | |
bool _expands = false; | |
set expands(bool value) { | |
assert(value != null); | |
if (_expands == value) | |
return; | |
_expands = value; | |
markNeedsLayout(); | |
} | |
// Indicates that the decoration should be aligned to accommodate an outline | |
// border. | |
bool get _isOutlineAligned { | |
return !decoration.isCollapsed && decoration.border.isOutline; | |
} | |
@override | |
void attach(PipelineOwner owner) { | |
super.attach(owner); | |
for (RenderBox child in _children) | |
child.attach(owner); | |
} | |
@override | |
void detach() { | |
super.detach(); | |
for (RenderBox child in _children) | |
child.detach(); | |
} | |
@override | |
void redepthChildren() { | |
_children.forEach(redepthChild); | |
} | |
@override | |
void visitChildren(RenderObjectVisitor visitor) { | |
_children.forEach(visitor); | |
} | |
@override | |
void visitChildrenForSemantics(RenderObjectVisitor visitor) { | |
if (icon != null) | |
visitor(icon); | |
if (prefix != null) | |
visitor(prefix); | |
if (prefixIcon != null) | |
visitor(prefixIcon); | |
if (isFocused && hint != null) { | |
// Bypass opacity to always read hint when focused. This prevents the | |
// label from changing when text is entered. | |
final RenderProxyBox typedHint = hint; | |
visitor(typedHint.child); | |
} else if (!isFocused && label != null) { | |
visitor(label); | |
} | |
if (input != null) | |
visitor(input); | |
if (suffixIcon != null) | |
visitor(suffixIcon); | |
if (suffix != null) | |
visitor(suffix); | |
if (container != null) | |
visitor(container); | |
if (helperError != null) | |
visitor(helperError); | |
if (counter != null) | |
visitor(counter); | |
} | |
@override | |
List<DiagnosticsNode> debugDescribeChildren() { | |
final List<DiagnosticsNode> value = <DiagnosticsNode>[]; | |
void add(RenderBox child, String name) { | |
if (child != null) | |
value.add(child.toDiagnosticsNode(name: name)); | |
} | |
add(icon, 'icon'); | |
add(input, 'input'); | |
add(label, 'label'); | |
add(hint, 'hint'); | |
add(prefix, 'prefix'); | |
add(suffix, 'suffix'); | |
add(prefixIcon, 'prefixIcon'); | |
add(suffixIcon, 'suffixIcon'); | |
add(helperError, 'helperError'); | |
add(counter, 'counter'); | |
add(container, 'container'); | |
return value; | |
} | |
@override | |
bool get sizedByParent => false; | |
static double _minWidth(RenderBox box, double height) { | |
return box == null ? 0.0 : box.getMinIntrinsicWidth(height); | |
} | |
static double _maxWidth(RenderBox box, double height) { | |
return box == null ? 0.0 : box.getMaxIntrinsicWidth(height); | |
} | |
static double _minHeight(RenderBox box, double width) { | |
return box == null ? 0.0 : box.getMinIntrinsicHeight(width); | |
} | |
static Size _boxSize(RenderBox box) => box == null ? Size.zero : box.size; | |
static BoxParentData _boxParentData(RenderBox box) => box.parentData; | |
EdgeInsets get contentPadding => decoration.contentPadding; | |
// Lay out the given box if needed, and return its baseline. | |
double _layoutLineBox(RenderBox box, BoxConstraints constraints) { | |
if (box == null) { | |
return 0.0; | |
} | |
box.layout(constraints, parentUsesSize: true); | |
final double baseline = box.getDistanceToBaseline(textBaseline); | |
assert(baseline != null && baseline >= 0.0); | |
return baseline; | |
} | |
// Returns a value used by performLayout to position all of the renderers. | |
// This method applies layout to all of the renderers except the container. | |
// For convenience, the container is laid out in performLayout(). | |
_RenderDecorationLayout _layout(BoxConstraints layoutConstraints) { | |
assert( | |
layoutConstraints.maxWidth < double.infinity, | |
'An InputDecorator, which is typically created by a TextField, cannot ' | |
'have an unbounded width.\n' | |
'This happens when the parent widget does not provide a finite width ' | |
'constraint. For example, if the InputDecorator is contained by a Row, ' | |
'then its width must be constrained. An Expanded widget or a SizedBox ' | |
'can be used to constrain the width of the InputDecorator or the ' | |
'TextField that contains it.', | |
); | |
// Margin on each side of subtext (counter and helperError) | |
final Map<RenderBox, double> boxToBaseline = <RenderBox, double>{}; | |
final BoxConstraints boxConstraints = layoutConstraints.loosen(); | |
// Layout all the widgets used by InputDecorator | |
boxToBaseline[prefix] = _layoutLineBox(prefix, boxConstraints); | |
boxToBaseline[suffix] = _layoutLineBox(suffix, boxConstraints); | |
boxToBaseline[icon] = _layoutLineBox(icon, boxConstraints); | |
boxToBaseline[prefixIcon] = _layoutLineBox(prefixIcon, boxConstraints); | |
boxToBaseline[suffixIcon] = _layoutLineBox(suffixIcon, boxConstraints); | |
final double inputWidth = math.max(0.0, constraints.maxWidth - ( | |
_boxSize(icon).width | |
+ contentPadding.left | |
+ _boxSize(prefixIcon).width | |
+ _boxSize(prefix).width | |
+ _boxSize(suffix).width | |
+ _boxSize(suffixIcon).width | |
+ contentPadding.right)); | |
boxToBaseline[label] = _layoutLineBox( | |
label, | |
boxConstraints.copyWith(maxWidth: inputWidth), | |
); | |
boxToBaseline[hint] = _layoutLineBox( | |
hint, | |
boxConstraints.copyWith(minWidth: inputWidth, maxWidth: inputWidth), | |
); | |
boxToBaseline[counter] = _layoutLineBox(counter, boxConstraints); | |
// The helper or error text can occupy the full width less the space | |
// occupied by the icon and counter. | |
boxToBaseline[helperError] = _layoutLineBox( | |
helperError, | |
boxConstraints.copyWith( | |
maxWidth: math.max(0.0, boxConstraints.maxWidth | |
- _boxSize(icon).width | |
- _boxSize(counter).width | |
- contentPadding.horizontal, | |
), | |
), | |
); | |
// The height of the input needs to accommodate label above and counter and | |
// helperError below, when they exist. | |
final double labelHeight = label == null | |
? 0 | |
: decoration.floatingLabelHeight; | |
final double topHeight = decoration.border.isOutline | |
? math.max(labelHeight - boxToBaseline[label], 0) | |
: labelHeight; | |
final double counterHeight = counter == null | |
? 0 | |
: boxToBaseline[counter] + subtextGap; | |
final bool helperErrorExists = helperError?.size != null | |
&& helperError.size.height > 0; | |
final double helperErrorHeight = !helperErrorExists | |
? 0 | |
: helperError.size.height + subtextGap; | |
final double bottomHeight = math.max( | |
counterHeight, | |
helperErrorHeight, | |
); | |
boxToBaseline[input] = _layoutLineBox( | |
input, | |
boxConstraints.deflate(EdgeInsets.only( | |
top: contentPadding.top + topHeight, | |
bottom: contentPadding.bottom + bottomHeight, | |
)).copyWith( | |
minWidth: inputWidth, | |
maxWidth: inputWidth, | |
), | |
); | |
// The field can be occupied by a hint or by the input itself | |
final double hintHeight = hint == null ? 0 : hint.size.height; | |
final double inputDirectHeight = input == null ? 0 : input.size.height; | |
final double inputHeight = math.max(hintHeight, inputDirectHeight); | |
final double inputInternalBaseline = math.max( | |
boxToBaseline[input], | |
boxToBaseline[hint], | |
); | |
// Calculate the amount that prefix/suffix affects height above and below | |
// the input. | |
final double prefixHeight = prefix == null ? 0 : prefix.size.height; | |
final double suffixHeight = suffix == null ? 0 : suffix.size.height; | |
final double fixHeight = math.max( | |
boxToBaseline[prefix], | |
boxToBaseline[suffix], | |
); | |
final double fixAboveInput = math.max(0, fixHeight - inputInternalBaseline); | |
final double fixBelowBaseline = math.max( | |
prefixHeight - boxToBaseline[prefix], | |
suffixHeight - boxToBaseline[suffix], | |
); | |
final double fixBelowInput = math.max( | |
0, | |
fixBelowBaseline - (inputHeight - inputInternalBaseline), | |
); | |
// Calculate the height of the input text container. | |
final double prefixIconHeight = prefixIcon == null ? 0 : prefixIcon.size.height; | |
final double suffixIconHeight = suffixIcon == null ? 0 : suffixIcon.size.height; | |
final double fixIconHeight = math.max(prefixIconHeight, suffixIconHeight); | |
final double contentHeight = math.max( | |
fixIconHeight, | |
topHeight | |
+ contentPadding.top | |
+ fixAboveInput | |
+ inputHeight | |
+ fixBelowInput | |
+ contentPadding.bottom, | |
); | |
final double maxContainerHeight = boxConstraints.maxHeight - bottomHeight; | |
final double containerHeight = expands | |
? maxContainerHeight | |
: math.min(contentHeight, maxContainerHeight); | |
// Try to consider the prefix/suffix as part of the text when aligning it. | |
// If the prefix/suffix overflows however, allow it to extend outside of the | |
// input and align the remaining part of the text and prefix/suffix. | |
final double overflow = math.max(0, contentHeight - maxContainerHeight); | |
// Map textAlignVertical from -1:1 to 0:1 so that it can be used to scale | |
// the baseline from its minimum to maximum values. | |
final double textAlignVerticalFactor = (textAlignVertical.y + 1.0) / 2.0; | |
// Adjust to try to fit top overflow inside the input on an inverse scale of | |
// textAlignVertical, so that top aligned text adjusts the most and bottom | |
// aligned text doesn't adjust at all. | |
final double baselineAdjustment = fixAboveInput - overflow * (1 - textAlignVerticalFactor); | |
// The baselines that will be used to draw the actual input text content. | |
final double topInputBaseline = contentPadding.top | |
+ topHeight | |
+ inputInternalBaseline | |
+ baselineAdjustment; | |
final double maxContentHeight = containerHeight | |
- contentPadding.top | |
- topHeight | |
- contentPadding.bottom; | |
final double alignableHeight = fixAboveInput + inputHeight + fixBelowInput; | |
final double maxVerticalOffset = maxContentHeight - alignableHeight; | |
final double textAlignVerticalOffset = maxVerticalOffset * textAlignVerticalFactor; | |
final double inputBaseline = topInputBaseline + textAlignVerticalOffset; | |
// The three main alignments for the baseline when an outline is present are | |
// | |
// * top (-1.0): topmost point considering padding. | |
// * center (0.0): the absolute center of the input ignoring padding but | |
// accommodating the border and floating label. | |
// * bottom (1.0): bottommost point considering padding. | |
// | |
// That means that if the padding is uneven, center is not the exact | |
// midpoint of top and bottom. To account for this, the above center and | |
// below center alignments are interpolated independently. | |
final double outlineCenterBaseline = inputInternalBaseline | |
+ baselineAdjustment / 2.0 | |
+ (containerHeight - (2.0 + inputHeight)) / 2.0; | |
final double outlineTopBaseline = topInputBaseline; | |
final double outlineBottomBaseline = topInputBaseline + maxVerticalOffset; | |
final double outlineBaseline = _interpolateThree( | |
outlineTopBaseline, | |
outlineCenterBaseline, | |
outlineBottomBaseline, | |
textAlignVertical, | |
); | |
// Find the positions of the text below the input when it exists. | |
double subtextCounterBaseline = 0; | |
double subtextHelperBaseline = 0; | |
double subtextCounterHeight = 0; | |
double subtextHelperHeight = 0; | |
if (counter != null) { | |
subtextCounterBaseline = | |
containerHeight + subtextGap + boxToBaseline[counter]; | |
subtextCounterHeight = counter.size.height + subtextGap; | |
} | |
if (helperErrorExists) { | |
subtextHelperBaseline = | |
containerHeight + subtextGap + boxToBaseline[helperError]; | |
subtextHelperHeight = helperErrorHeight; | |
} | |
final double subtextBaseline = math.max( | |
subtextCounterBaseline, | |
subtextHelperBaseline, | |
); | |
final double subtextHeight = math.max( | |
subtextCounterHeight, | |
subtextHelperHeight, | |
); | |
return _RenderDecorationLayout( | |
boxToBaseline: boxToBaseline, | |
containerHeight: containerHeight, | |
inputBaseline: inputBaseline, | |
outlineBaseline: outlineBaseline, | |
subtextBaseline: subtextBaseline, | |
subtextHeight: subtextHeight, | |
); | |
} | |
// Interpolate between three stops using textAlignVertical. This is used to | |
// calculate the outline baseline, which ignores padding when the alignment is | |
// middle. When the alignment is less than zero, it interpolates between the | |
// centered text box's top and the top of the content padding. When the | |
// alignment is greater than zero, it interpolates between the centered box's | |
// top and the position that would align the bottom of the box with the bottom | |
// padding. | |
double _interpolateThree(double begin, double middle, double end, TextAlignVertical textAlignVertical) { | |
if (textAlignVertical.y <= 0) { | |
// It's possible for begin, middle, and end to not be in order because of | |
// excessive padding. Those cases are handled by using middle. | |
if (begin >= middle) { | |
return middle; | |
} | |
// Do a standard linear interpolation on the first half, between begin and | |
// middle. | |
final double t = textAlignVertical.y + 1; | |
return begin + (middle - begin) * t; | |
} | |
if (middle >= end) { | |
return middle; | |
} | |
// Do a standard linear interpolation on the second half, between middle and | |
// end. | |
final double t = textAlignVertical.y; | |
return middle + (end - middle) * t; | |
} | |
@override | |
double computeMinIntrinsicWidth(double height) { | |
return _minWidth(icon, height) | |
+ contentPadding.left | |
+ _minWidth(prefixIcon, height) | |
+ _minWidth(prefix, height) | |
+ math.max(_minWidth(input, height), _minWidth(hint, height)) | |
+ _minWidth(suffix, height) | |
+ _minWidth(suffixIcon, height) | |
+ contentPadding.right; | |
} | |
@override | |
double computeMaxIntrinsicWidth(double height) { | |
return _maxWidth(icon, height) | |
+ contentPadding.left | |
+ _maxWidth(prefixIcon, height) | |
+ _maxWidth(prefix, height) | |
+ math.max(_maxWidth(input, height), _maxWidth(hint, height)) | |
+ _maxWidth(suffix, height) | |
+ _maxWidth(suffixIcon, height) | |
+ contentPadding.right; | |
} | |
double _lineHeight(double width, List<RenderBox> boxes) { | |
double height = 0.0; | |
for (RenderBox box in boxes) { | |
if (box == null) | |
continue; | |
height = math.max(_minHeight(box, width), height); | |
} | |
return height; | |
// TODO(hansmuller): this should compute the overall line height for the | |
// boxes when they've been baseline-aligned. | |
// See https://github.com/flutter/flutter/issues/13715 | |
} | |
@override | |
double computeMinIntrinsicHeight(double width) { | |
double subtextHeight = _lineHeight(width, <RenderBox>[helperError, counter]); | |
if (subtextHeight > 0.0) | |
subtextHeight += subtextGap; | |
return contentPadding.top | |
+ (label == null ? 0.0 : decoration.floatingLabelHeight) | |
+ _lineHeight(width, <RenderBox>[prefix, input, suffix]) | |
+ subtextHeight | |
+ contentPadding.bottom; | |
} | |
@override | |
double computeMaxIntrinsicHeight(double width) { | |
return computeMinIntrinsicHeight(width); | |
} | |
@override | |
double computeDistanceToActualBaseline(TextBaseline baseline) { | |
return _boxParentData(input).offset.dy + input.computeDistanceToActualBaseline(baseline); | |
} | |
// Records where the label was painted. | |
Matrix4 _labelTransform; | |
@override | |
void performLayout() { | |
_labelTransform = null; | |
final _RenderDecorationLayout layout = _layout(constraints); | |
final double overallWidth = constraints.maxWidth; | |
final double overallHeight = layout.containerHeight + layout.subtextHeight; | |
if (container != null) { | |
final BoxConstraints containerConstraints = BoxConstraints.tightFor( | |
height: layout.containerHeight, | |
width: overallWidth - _boxSize(icon).width, | |
); | |
container.layout(containerConstraints, parentUsesSize: true); | |
double x; | |
switch (textDirection) { | |
case TextDirection.rtl: | |
x = 0.0; | |
break; | |
case TextDirection.ltr: | |
x = _boxSize(icon).width; | |
break; | |
} | |
_boxParentData(container).offset = Offset(x, 0.0); | |
} | |
double height; | |
double centerLayout(RenderBox box, double x) { | |
_boxParentData(box).offset = Offset(x, (height - box.size.height) / 2.0); | |
return box.size.width; | |
} | |
double baseline; | |
double baselineLayout(RenderBox box, double x) { | |
_boxParentData(box).offset = Offset(x, baseline - layout.boxToBaseline[box]); | |
return box.size.width; | |
} | |
final double left = contentPadding.left; | |
final double right = overallWidth - contentPadding.right; | |
height = layout.containerHeight; | |
baseline = _isOutlineAligned ? layout.outlineBaseline : layout.inputBaseline; | |
if (icon != null) { | |
double x; | |
switch (textDirection) { | |
case TextDirection.rtl: | |
x = overallWidth - icon.size.width; | |
break; | |
case TextDirection.ltr: | |
x = 0.0; | |
break; | |
} | |
centerLayout(icon, x); | |
} | |
switch (textDirection) { | |
case TextDirection.rtl: { | |
double start = right - _boxSize(icon).width; | |
double end = left; | |
if (prefixIcon != null) { | |
start += contentPadding.left; | |
start -= centerLayout(prefixIcon, start - prefixIcon.size.width); | |
} | |
if (label != null) { | |
if (decoration.alignLabelWithHint) { | |
baselineLayout(label, start - label.size.width); | |
} else { | |
centerLayout(label, start - label.size.width); | |
} | |
} | |
if (prefix != null) | |
start -= baselineLayout(prefix, start - prefix.size.width); | |
if (input != null) | |
baselineLayout(input, start - input.size.width); | |
if (hint != null) | |
baselineLayout(hint, start - hint.size.width); | |
if (suffixIcon != null) { | |
end -= contentPadding.left; | |
end += centerLayout(suffixIcon, end); | |
} | |
if (suffix != null) | |
end += baselineLayout(suffix, end); | |
break; | |
} | |
case TextDirection.ltr: { | |
double start = left + _boxSize(icon).width; | |
double end = right; | |
if (prefixIcon != null) { | |
start -= contentPadding.left; | |
start += centerLayout(prefixIcon, start); | |
} | |
if (label != null) { | |
if (decoration.alignLabelWithHint) { | |
baselineLayout(label, start); | |
} else { | |
centerLayout(label, start); | |
} | |
} | |
if (prefix != null) | |
start += baselineLayout(prefix, start); | |
if (input != null) | |
baselineLayout(input, start); | |
if (hint != null) | |
baselineLayout(hint, start); | |
if (suffixIcon != null) { | |
end += contentPadding.right; | |
end -= centerLayout(suffixIcon, end - suffixIcon.size.width); | |
} | |
if (suffix != null) | |
end -= baselineLayout(suffix, end - suffix.size.width); | |
break; | |
} | |
} | |
if (helperError != null || counter != null) { | |
height = layout.subtextHeight; | |
baseline = layout.subtextBaseline; | |
switch (textDirection) { | |
case TextDirection.rtl: | |
if (helperError != null) | |
baselineLayout(helperError, right - helperError.size.width - _boxSize(icon).width); | |
if (counter != null) | |
baselineLayout(counter, left); | |
break; | |
case TextDirection.ltr: | |
if (helperError != null) | |
baselineLayout(helperError, left + _boxSize(icon).width); | |
if (counter != null) | |
baselineLayout(counter, right - counter.size.width); | |
break; | |
} | |
} | |
if (label != null) { | |
final double labelX = _boxParentData(label).offset.dx; | |
switch (textDirection) { | |
case TextDirection.rtl: | |
decoration.borderGap.start = labelX + label.size.width; | |
break; | |
case TextDirection.ltr: | |
// The value of _InputBorderGap.start is relative to the origin of the | |
// _BorderContainer which is inset by the icon's width. | |
decoration.borderGap.start = labelX - _boxSize(icon).width; | |
break; | |
} | |
decoration.borderGap.extent = label.size.width * 0.75; | |
} else { | |
decoration.borderGap.start = null; | |
decoration.borderGap.extent = 0.0; | |
} | |
size = constraints.constrain(Size(overallWidth, overallHeight)); | |
assert(size.width == constraints.constrainWidth(overallWidth)); | |
assert(size.height == constraints.constrainHeight(overallHeight)); | |
} | |
void _paintLabel(PaintingContext context, Offset offset) { | |
context.paintChild(label, offset); | |
} | |
@override | |
void paint(PaintingContext context, Offset offset) { | |
void doPaint(RenderBox child) { | |
if (child != null) | |
context.paintChild(child, _boxParentData(child).offset + offset); | |
} | |
doPaint(container); | |
if (label != null) { | |
final Offset labelOffset = _boxParentData(label).offset; | |
final double labelHeight = label.size.height; | |
final double t = decoration.floatingLabelProgress; | |
// The center of the outline border label ends up a little below the | |
// center of the top border line. | |
final bool isOutlineBorder = decoration.border != null && decoration.border.isOutline; | |
final double floatingY = isOutlineBorder ? -labelHeight * 0.25 : contentPadding.top; | |
final double scale = lerpDouble(1.0, 0.75, t); | |
double dx; | |
switch (textDirection) { | |
case TextDirection.rtl: | |
dx = labelOffset.dx + label.size.width * (1.0 - scale); // origin is on the right | |
break; | |
case TextDirection.ltr: | |
dx = labelOffset.dx; // origin on the left | |
break; | |
} | |
final double dy = lerpDouble(0.0, floatingY - labelOffset.dy, t); | |
_labelTransform = Matrix4.identity() | |
..translate(dx, labelOffset.dy + dy) | |
..scale(scale); | |
context.pushTransform(needsCompositing, offset, _labelTransform, _paintLabel); | |
} | |
doPaint(icon); | |
doPaint(prefix); | |
doPaint(suffix); | |
doPaint(prefixIcon); | |
doPaint(suffixIcon); | |
doPaint(hint); | |
doPaint(input); | |
doPaint(helperError); | |
doPaint(counter); | |
} | |
@override | |
bool hitTestSelf(Offset position) => true; | |
@override | |
bool hitTestChildren(BoxHitTestResult result, { @required Offset position }) { | |
assert(position != null); | |
for (RenderBox child in _children) { | |
// TODO(hansmuller): label must be handled specially since we've transformed it | |
final Offset offset = _boxParentData(child).offset; | |
final bool isHit = result.addWithPaintOffset( | |
offset: offset, | |
position: position, | |
hitTest: (BoxHitTestResult result, Offset transformed) { | |
assert(transformed == position - offset); | |
return child.hitTest(result, position: transformed); | |
}, | |
); | |
if (isHit) | |
return true; | |
} | |
return false; | |
} | |
@override | |
void applyPaintTransform(RenderObject child, Matrix4 transform) { | |
if (child == label && _labelTransform != null) { | |
final Offset labelOffset = _boxParentData(label).offset; | |
transform | |
..multiply(_labelTransform) | |
..translate(-labelOffset.dx, -labelOffset.dy); | |
} | |
super.applyPaintTransform(child, transform); | |
} | |
} | |
class _RenderDecorationElement extends RenderObjectElement { | |
_RenderDecorationElement(_Decorator widget) : super(widget); | |
final Map<_DecorationSlot, Element> slotToChild = <_DecorationSlot, Element>{}; | |
final Map<Element, _DecorationSlot> childToSlot = <Element, _DecorationSlot>{}; | |
@override | |
_Decorator get widget => super.widget; | |
@override | |
_RenderDecoration get renderObject => super.renderObject; | |
@override | |
void visitChildren(ElementVisitor visitor) { | |
slotToChild.values.forEach(visitor); | |
} | |
@override | |
void forgetChild(Element child) { | |
assert(slotToChild.values.contains(child)); | |
assert(childToSlot.keys.contains(child)); | |
final _DecorationSlot slot = childToSlot[child]; | |
childToSlot.remove(child); | |
slotToChild.remove(slot); | |
} | |
void _mountChild(Widget widget, _DecorationSlot slot) { | |
final Element oldChild = slotToChild[slot]; | |
final Element newChild = updateChild(oldChild, widget, slot); | |
if (oldChild != null) { | |
slotToChild.remove(slot); | |
childToSlot.remove(oldChild); | |
} | |
if (newChild != null) { | |
slotToChild[slot] = newChild; | |
childToSlot[newChild] = slot; | |
} | |
} | |
@override | |
void mount(Element parent, dynamic newSlot) { | |
super.mount(parent, newSlot); | |
_mountChild(widget.decoration.icon, _DecorationSlot.icon); | |
_mountChild(widget.decoration.input, _DecorationSlot.input); | |
_mountChild(widget.decoration.label, _DecorationSlot.label); | |
_mountChild(widget.decoration.hint, _DecorationSlot.hint); | |
_mountChild(widget.decoration.prefix, _DecorationSlot.prefix); | |
_mountChild(widget.decoration.suffix, _DecorationSlot.suffix); | |
_mountChild(widget.decoration.prefixIcon, _DecorationSlot.prefixIcon); | |
_mountChild(widget.decoration.suffixIcon, _DecorationSlot.suffixIcon); | |
_mountChild(widget.decoration.helperError, _DecorationSlot.helperError); | |
_mountChild(widget.decoration.counter, _DecorationSlot.counter); | |
_mountChild(widget.decoration.container, _DecorationSlot.container); | |
} | |
void _updateChild(Widget widget, _DecorationSlot slot) { | |
final Element oldChild = slotToChild[slot]; | |
final Element newChild = updateChild(oldChild, widget, slot); | |
if (oldChild != null) { | |
childToSlot.remove(oldChild); | |
slotToChild.remove(slot); | |
} | |
if (newChild != null) { | |
slotToChild[slot] = newChild; | |
childToSlot[newChild] = slot; | |
} | |
} | |
@override | |
void update(_Decorator newWidget) { | |
super.update(newWidget); | |
assert(widget == newWidget); | |
_updateChild(widget.decoration.icon, _DecorationSlot.icon); | |
_updateChild(widget.decoration.input, _DecorationSlot.input); | |
_updateChild(widget.decoration.label, _DecorationSlot.label); | |
_updateChild(widget.decoration.hint, _DecorationSlot.hint); | |
_updateChild(widget.decoration.prefix, _DecorationSlot.prefix); | |
_updateChild(widget.decoration.suffix, _DecorationSlot.suffix); | |
_updateChild(widget.decoration.prefixIcon, _DecorationSlot.prefixIcon); | |
_updateChild(widget.decoration.suffixIcon, _DecorationSlot.suffixIcon); | |
_updateChild(widget.decoration.helperError, _DecorationSlot.helperError); | |
_updateChild(widget.decoration.counter, _DecorationSlot.counter); | |
_updateChild(widget.decoration.container, _DecorationSlot.container); | |
} | |
void _updateRenderObject(RenderObject child, _DecorationSlot slot) { | |
switch (slot) { | |
case _DecorationSlot.icon: | |
renderObject.icon = child; | |
break; | |
case _DecorationSlot.input: | |
renderObject.input = child; | |
break; | |
case _DecorationSlot.label: | |
renderObject.label = child; | |
break; | |
case _DecorationSlot.hint: | |
renderObject.hint = child; | |
break; | |
case _DecorationSlot.prefix: | |
renderObject.prefix = child; | |
break; | |
case _DecorationSlot.suffix: | |
renderObject.suffix = child; | |
break; | |
case _DecorationSlot.prefixIcon: | |
renderObject.prefixIcon = child; | |
break; | |
case _DecorationSlot.suffixIcon: | |
renderObject.suffixIcon = child; | |
break; | |
case _DecorationSlot.helperError: | |
renderObject.helperError = child; | |
break; | |
case _DecorationSlot.counter: | |
renderObject.counter = child; | |
break; | |
case _DecorationSlot.container: | |
renderObject.container = child; | |
break; | |
} | |
} | |
@override | |
void insertChildRenderObject(RenderObject child, dynamic slotValue) { | |
assert(child is RenderBox); | |
assert(slotValue is _DecorationSlot); | |
final _DecorationSlot slot = slotValue; | |
_updateRenderObject(child, slot); | |
assert(renderObject.childToSlot.keys.contains(child)); | |
assert(renderObject.slotToChild.keys.contains(slot)); | |
} | |
@override | |
void removeChildRenderObject(RenderObject child) { | |
assert(child is RenderBox); | |
assert(renderObject.childToSlot.keys.contains(child)); | |
_updateRenderObject(null, renderObject.childToSlot[child]); | |
assert(!renderObject.childToSlot.keys.contains(child)); | |
assert(!renderObject.slotToChild.keys.contains(slot)); | |
} | |
@override | |
void moveChildRenderObject(RenderObject child, dynamic slotValue) { | |
assert(false, 'not reachable'); | |
} | |
} | |
class _Decorator extends RenderObjectWidget { | |
const _Decorator({ | |
Key key, | |
@required this.textAlignVertical, | |
@required this.decoration, | |
@required this.textDirection, | |
@required this.textBaseline, | |
@required this.isFocused, | |
@required this.expands, | |
}) : assert(decoration != null), | |
assert(textDirection != null), | |
assert(textBaseline != null), | |
assert(expands != null), | |
super(key: key); | |
final _Decoration decoration; | |
final TextDirection textDirection; | |
final TextBaseline textBaseline; | |
final TextAlignVertical textAlignVertical; | |
final bool isFocused; | |
final bool expands; | |
@override | |
_RenderDecorationElement createElement() => _RenderDecorationElement(this); | |
@override | |
_RenderDecoration createRenderObject(BuildContext context) { | |
return _RenderDecoration( | |
decoration: decoration, | |
textDirection: textDirection, | |
textBaseline: textBaseline, | |
textAlignVertical: textAlignVertical, | |
isFocused: isFocused, | |
expands: expands, | |
); | |
} | |
@override | |
void updateRenderObject(BuildContext context, _RenderDecoration renderObject) { | |
renderObject | |
..decoration = decoration | |
..textDirection = textDirection | |
..textBaseline = textBaseline | |
..expands = expands | |
..isFocused = isFocused; | |
} | |
} | |
class _AffixText extends StatelessWidget { | |
const _AffixText({ | |
this.labelIsFloating, | |
this.text, | |
this.style, | |
this.child, | |
}); | |
final bool labelIsFloating; | |
final String text; | |
final TextStyle style; | |
final Widget child; | |
@override | |
Widget build(BuildContext context) { | |
return DefaultTextStyle.merge( | |
style: style, | |
child: AnimatedOpacity( | |
duration: _kTransitionDuration, | |
curve: _kTransitionCurve, | |
opacity: labelIsFloating ? 1.0 : 0.0, | |
child: child ?? Text(text, style: style,), | |
), | |
); | |
} | |
} | |
/// Defines the appearance of a Material Design text field. | |
/// | |
/// [InputDecorator] displays the visual elements of a Material Design text | |
/// field around its input [child]. The visual elements themselves are defined | |
/// by an [InputDecoration] object and their layout and appearance depend | |
/// on the `baseStyle`, `textAlign`, `isFocused`, and `isEmpty` parameters. | |
/// | |
/// [TextField] uses this widget to decorate its [EditableText] child. | |
/// | |
/// [InputDecorator] can be used to create widgets that look and behave like a | |
/// [TextField] but support other kinds of input. | |
/// | |
/// Requires one of its ancestors to be a [Material] widget. | |
/// | |
/// See also: | |
/// | |
/// * [TextField], which uses an [InputDecorator] to display a border, | |
/// labels, and icons, around its [EditableText] child. | |
/// * [Decoration] and [DecoratedBox], for drawing arbitrary decorations | |
/// around other widgets. | |
class InputDecoratorCustom extends StatefulWidget { | |
/// Creates a widget that displays a border, labels, and icons, | |
/// for a [TextField]. | |
/// | |
/// The [isFocused], [isHovering], [expands], and [isEmpty] arguments must not | |
/// be null. | |
const InputDecoratorCustom({ | |
Key key, | |
this.decoration, | |
this.baseStyle, | |
this.textAlign, | |
this.textAlignVertical, | |
this.hintText, | |
this.isFocused = false, | |
this.isHovering = false, | |
this.expands = false, | |
this.isEmpty = false, | |
this.child, | |
}) : assert(isFocused != null), | |
assert(isHovering != null), | |
assert(expands != null), | |
assert(isEmpty != null), | |
super(key: key); | |
/// The text and styles to use when decorating the child. | |
/// | |
/// If null, `const InputDecoration()` is used. Null [InputDecoration] | |
/// properties are initialized with the corresponding values from | |
/// [ThemeData.inputDecorationTheme]. | |
final InputDecoration decoration; | |
/// The style on which to base the label, hint, counter, and error styles | |
/// if the [decoration] does not provide explicit styles. | |
/// | |
/// If null, `baseStyle` defaults to the `subhead` style from the | |
/// current [Theme], see [ThemeData.textTheme]. | |
/// | |
/// The [TextStyle.textBaseline] of the [baseStyle] is used to determine | |
/// the baseline used for text alignment. | |
final TextStyle baseStyle; | |
/// How the text in the decoration should be aligned horizontally. | |
final TextAlign textAlign; | |
/// {@template flutter.widgets.inputDecorator.textAlignVertical} | |
/// How the text should be aligned vertically. | |
/// | |
/// Determines the alignment of the baseline within the available space of | |
/// the input (typically a TextField). For example, TextAlignVertical.top will | |
/// place the baseline such that the text, and any attached decoration like | |
/// prefix and suffix, is as close to the top of the input as possible without | |
/// overflowing. The heights of the prefix and suffix are similarly included | |
/// for other alignment values. If the height is greater than the height | |
/// available, then the prefix and suffix will be allowed to overflow first | |
/// before the text scrolls. | |
/// {@endtemplate} | |
final TextAlignVertical textAlignVertical; | |
final Text hintText; | |
/// Whether the input field has focus. | |
/// | |
/// Determines the position of the label text and the color and weight of the | |
/// border, as well as the container fill color, which is a blend of | |
/// [InputDecoration.focusColor] with [InputDecoration.fillColor] when | |
/// focused, and [InputDecoration.fillColor] when not. | |
/// | |
/// Defaults to false. | |
/// | |
/// See also: | |
/// | |
/// - [InputDecoration.hoverColor], which is also blended into the focus | |
/// color and fill color when the [isHovering] is true to produce the final | |
/// color. | |
final bool isFocused; | |
/// Whether the input field is being hovered over by a mouse pointer. | |
/// | |
/// Determines the container fill color, which is a blend of | |
/// [InputDecoration.hoverColor] with [InputDecoration.fillColor] when | |
/// true, and [InputDecoration.fillColor] when not. | |
/// | |
/// Defaults to false. | |
/// | |
/// See also: | |
/// | |
/// - [InputDecoration.focusColor], which is also blended into the hover | |
/// color and fill color when [isFocused] is true to produce the final | |
/// color. | |
final bool isHovering; | |
/// If true, the height of the input field will be as large as possible. | |
/// | |
/// If wrapped in a widget that constrains its child's height, like Expanded | |
/// or SizedBox, the input field will only be affected if [expands] is set to | |
/// true. | |
/// | |
/// See [TextField.minLines] and [TextField.maxLines] for related ways to | |
/// affect the height of an input. When [expands] is true, both must be null | |
/// in order to avoid ambiguity in determining the height. | |
/// | |
/// Defaults to false. | |
final bool expands; | |
/// Whether the input field is empty. | |
/// | |
/// Determines the position of the label text and whether to display the hint | |
/// text. | |
/// | |
/// Defaults to false. | |
final bool isEmpty; | |
/// The widget below this widget in the tree. | |
/// | |
/// Typically an [EditableText], [DropdownButton], or [InkWell]. | |
final Widget child; | |
/// Whether the label needs to get out of the way of the input, either by | |
/// floating or disappearing. | |
bool get _labelShouldWithdraw => !isEmpty || isFocused; | |
@override | |
_InputDecoratorState createState() => _InputDecoratorState(); | |
/// The RenderBox that defines this decorator's "container". That's the | |
/// area which is filled if [InputDecoration.filled] is true. It's the area | |
/// adjacent to [InputDecoration.icon] and above the widgets that contain | |
/// [InputDecoration.helperText], [InputDecoration.errorText], and | |
/// [InputDecoration.counterText]. | |
/// | |
/// [TextField] renders ink splashes within the container. | |
static RenderBox containerOf(BuildContext context) { | |
final _RenderDecoration result = context.ancestorRenderObjectOfType(const TypeMatcher<_RenderDecoration>()); | |
return result?.container; | |
} | |
@override | |
void debugFillProperties(DiagnosticPropertiesBuilder properties) { | |
super.debugFillProperties(properties); | |
properties.add(DiagnosticsProperty<InputDecoration>('decoration', decoration)); | |
properties.add(DiagnosticsProperty<TextStyle>('baseStyle', baseStyle, defaultValue: null)); | |
properties.add(DiagnosticsProperty<bool>('isFocused', isFocused)); | |
properties.add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false)); | |
properties.add(DiagnosticsProperty<bool>('isEmpty', isEmpty)); | |
} | |
} | |
class _InputDecoratorState extends State<InputDecoratorCustom> with TickerProviderStateMixin { | |
AnimationController _floatingLabelController; | |
AnimationController _shakingLabelController; | |
final _InputBorderGap _borderGap = _InputBorderGap(); | |
@override | |
void initState() { | |
super.initState(); | |
_floatingLabelController = AnimationController( | |
duration: _kTransitionDuration, | |
vsync: this, | |
value: (widget.decoration.hasFloatingPlaceholder && widget._labelShouldWithdraw) ? 1.0 : 0.0, | |
); | |
_floatingLabelController.addListener(_handleChange); | |
_shakingLabelController = AnimationController( | |
duration: _kTransitionDuration, | |
vsync: this, | |
); | |
} | |
@override | |
void didChangeDependencies() { | |
super.didChangeDependencies(); | |
_effectiveDecoration = null; | |
} | |
@override | |
void dispose() { | |
_floatingLabelController.dispose(); | |
_shakingLabelController.dispose(); | |
super.dispose(); | |
} | |
void _handleChange() { | |
setState(() { | |
// The _floatingLabelController's value has changed. | |
}); | |
} | |
InputDecoration _effectiveDecoration; | |
InputDecoration get decoration { | |
_effectiveDecoration ??= widget.decoration.applyDefaults( | |
Theme.of(context).inputDecorationTheme | |
); | |
return _effectiveDecoration; | |
} | |
TextAlign get textAlign => widget.textAlign; | |
bool get isFocused => widget.isFocused && decoration.enabled; | |
bool get isHovering => widget.isHovering && decoration.enabled; | |
bool get isEmpty => widget.isEmpty; | |
@override | |
void didUpdateWidget(InputDecoratorCustom old) { | |
super.didUpdateWidget(old); | |
if (widget.decoration != old.decoration) | |
_effectiveDecoration = null; | |
if (widget._labelShouldWithdraw != old._labelShouldWithdraw && widget.decoration.hasFloatingPlaceholder) { | |
if (widget._labelShouldWithdraw) | |
_floatingLabelController.forward(); | |
else | |
_floatingLabelController.reverse(); | |
} | |
final String errorText = decoration.errorText; | |
final String oldErrorText = old.decoration.errorText; | |
if (_floatingLabelController.isCompleted && errorText != null && errorText != oldErrorText) { | |
_shakingLabelController | |
..value = 0.0 | |
..forward(); | |
} | |
} | |
Color _getActiveColor(ThemeData themeData) { | |
if (isFocused) { | |
switch (themeData.brightness) { | |
case Brightness.dark: | |
return themeData.accentColor; | |
case Brightness.light: | |
return themeData.primaryColor; | |
} | |
} | |
return themeData.hintColor; | |
} | |
Color _getDefaultBorderColor(ThemeData themeData) { | |
if (isFocused) { | |
switch (themeData.brightness) { | |
case Brightness.dark: | |
return themeData.accentColor; | |
case Brightness.light: | |
return themeData.primaryColor; | |
} | |
} | |
if (decoration.filled) { | |
return themeData.hintColor; | |
} | |
final Color enabledColor = themeData.colorScheme.onSurface.withOpacity(0.38); | |
if (isHovering) { | |
final Color hoverColor = decoration.hoverColor ?? themeData.inputDecorationTheme?.hoverColor ?? themeData.hoverColor; | |
return Color.alphaBlend(hoverColor.withOpacity(0.12), enabledColor); | |
} | |
return enabledColor; | |
} | |
Color _getFillColor(ThemeData themeData) { | |
if (decoration.filled != true) // filled == null same as filled == false | |
return Colors.transparent; | |
if (decoration.fillColor != null) | |
return decoration.fillColor; | |
// dark theme: 10% white (enabled), 5% white (disabled) | |
// light theme: 4% black (enabled), 2% black (disabled) | |
const Color darkEnabled = Color(0x1AFFFFFF); | |
const Color darkDisabled = Color(0x0DFFFFFF); | |
const Color lightEnabled = Color(0x0A000000); | |
const Color lightDisabled = Color(0x05000000); | |
switch (themeData.brightness) { | |
case Brightness.dark: | |
return decoration.enabled ? darkEnabled : darkDisabled; | |
case Brightness.light: | |
return decoration.enabled ? lightEnabled : lightDisabled; | |
} | |
return lightEnabled; | |
} | |
Color _getHoverColor(ThemeData themeData) { | |
if (decoration.filled == null || !decoration.filled || isFocused || !decoration.enabled) | |
return Colors.transparent; | |
return decoration.hoverColor ?? themeData.inputDecorationTheme?.hoverColor ?? themeData.hoverColor; | |
} | |
Color _getDefaultIconColor(ThemeData themeData) { | |
if (!decoration.enabled) | |
return themeData.disabledColor; | |
switch (themeData.brightness) { | |
case Brightness.dark: | |
return Colors.white70; | |
case Brightness.light: | |
return Colors.black45; | |
default: | |
return themeData.iconTheme.color; | |
} | |
} | |
// True if the label will be shown and the hint will not. | |
// If we're not focused, there's no value, and labelText was provided, | |
// then the label appears where the hint would. | |
bool get _hasInlineLabel => !widget._labelShouldWithdraw && decoration.labelText != null; | |
// If the label is a floating placeholder, it's always shown. | |
bool get _shouldShowLabel => _hasInlineLabel || decoration.hasFloatingPlaceholder; | |
// The base style for the inline label or hint when they're displayed "inline", | |
// i.e. when they appear in place of the empty text field. | |
TextStyle _getInlineStyle(ThemeData themeData) { | |
return themeData.textTheme.subhead.merge(widget.baseStyle) | |
.copyWith(color: decoration.enabled ? themeData.hintColor : themeData.disabledColor); | |
} | |
TextStyle _getFloatingLabelStyle(ThemeData themeData) { | |
final Color color = decoration.errorText != null | |
? decoration.errorStyle?.color ?? themeData.errorColor | |
: _getActiveColor(themeData); | |
final TextStyle style = themeData.textTheme.subhead.merge(widget.baseStyle); | |
return style | |
.copyWith(color: decoration.enabled ? color : themeData.disabledColor) | |
.merge(decoration.labelStyle); | |
} | |
TextStyle _getHelperStyle(ThemeData themeData) { | |
final Color color = decoration.enabled ? themeData.hintColor : Colors.transparent; | |
return themeData.textTheme.caption.copyWith(color: color).merge(decoration.helperStyle); | |
} | |
TextStyle _getErrorStyle(ThemeData themeData) { | |
final Color color = decoration.enabled ? themeData.errorColor : Colors.transparent; | |
return themeData.textTheme.caption.copyWith(color: color).merge(decoration.errorStyle); | |
} | |
InputBorder _getDefaultBorder(ThemeData themeData) { | |
if (decoration.border?.borderSide == BorderSide.none) { | |
return decoration.border; | |
} | |
Color borderColor; | |
if (decoration.enabled) { | |
borderColor = decoration.errorText == null | |
? _getDefaultBorderColor(themeData) | |
: themeData.errorColor; | |
} else { | |
borderColor = (decoration.filled == true && decoration.border?.isOutline != true) | |
? Colors.transparent | |
: themeData.disabledColor; | |
} | |
double borderWeight; | |
if (decoration.isCollapsed || decoration?.border == InputBorder.none || !decoration.enabled) | |
borderWeight = 0.0; | |
else | |
borderWeight = isFocused ? 2.0 : 1.0; | |
final InputBorder border = decoration.border ?? const UnderlineInputBorder(); | |
return border.copyWith(borderSide: BorderSide(color: borderColor, width: borderWeight)); | |
} | |
@override | |
Widget build(BuildContext context) { | |
final ThemeData themeData = Theme.of(context); | |
final TextStyle inlineStyle = _getInlineStyle(themeData); | |
final TextBaseline textBaseline = inlineStyle.textBaseline; | |
final TextStyle hintStyle = inlineStyle.merge(decoration.hintStyle); | |
final Widget hint = widget.hintText == null ? null : AnimatedOpacity( | |
opacity: (isEmpty && !_hasInlineLabel) ? 1.0 : 0.0, | |
duration: _kTransitionDuration, | |
curve: _kTransitionCurve, | |
child: widget.hintText, | |
); | |
final bool isError = decoration.errorText != null; | |
InputBorder border; | |
if (!decoration.enabled) | |
border = isError ? decoration.errorBorder : decoration.disabledBorder; | |
else if (isFocused) | |
border = isError ? decoration.focusedErrorBorder : decoration.focusedBorder; | |
else | |
border = isError ? decoration.errorBorder : decoration.enabledBorder; | |
border ??= _getDefaultBorder(themeData); | |
final Widget container = _BorderContainer( | |
border: border, | |
gap: _borderGap, | |
gapAnimation: _floatingLabelController.view, | |
fillColor: _getFillColor(themeData), | |
hoverColor: _getHoverColor(themeData), | |
isHovering: isHovering, | |
); | |
final TextStyle inlineLabelStyle = inlineStyle.merge(decoration.labelStyle); | |
final Widget label = decoration.labelText == null ? null : _Shaker( | |
animation: _shakingLabelController.view, | |
child: AnimatedOpacity( | |
duration: _kTransitionDuration, | |
curve: _kTransitionCurve, | |
opacity: _shouldShowLabel ? 1.0 : 0.0, | |
child: AnimatedDefaultTextStyle( | |
duration:_kTransitionDuration, | |
curve: _kTransitionCurve, | |
style: widget._labelShouldWithdraw | |
? _getFloatingLabelStyle(themeData) | |
: inlineLabelStyle, | |
child: Text( | |
decoration.labelText, | |
overflow: TextOverflow.ellipsis, | |
textAlign: textAlign, | |
), | |
), | |
), | |
); | |
final Widget prefix = decoration.prefix == null && decoration.prefixText == null ? null : | |
_AffixText( | |
labelIsFloating: widget._labelShouldWithdraw, | |
text: decoration.prefixText, | |
style: decoration.prefixStyle ?? hintStyle, | |
child: decoration.prefix, | |
); | |
final Widget suffix = decoration.suffix == null && decoration.suffixText == null ? null : | |
_AffixText( | |
labelIsFloating: widget._labelShouldWithdraw, | |
text: decoration.suffixText, | |
style: decoration.suffixStyle ?? hintStyle, | |
child: decoration.suffix, | |
); | |
final Color activeColor = _getActiveColor(themeData); | |
final bool decorationIsDense = decoration.isDense == true; // isDense == null, same as false | |
final double iconSize = decorationIsDense ? 18.0 : 24.0; | |
final Color iconColor = isFocused ? activeColor : _getDefaultIconColor(themeData); | |
final Widget icon = decoration.icon == null ? null : | |
Padding( | |
padding: const EdgeInsetsDirectional.only(end: 16.0), | |
child: IconTheme.merge( | |
data: IconThemeData( | |
color: iconColor, | |
size: iconSize, | |
), | |
child: decoration.icon, | |
), | |
); | |
final Widget prefixIcon = decoration.prefixIcon == null ? null : | |
Center( | |
widthFactor: 1.0, | |
heightFactor: 1.0, | |
child: ConstrainedBox( | |
constraints: const BoxConstraints(minWidth: 48.0, minHeight: 48.0), | |
child: IconTheme.merge( | |
data: IconThemeData( | |
color: iconColor, | |
size: iconSize, | |
), | |
child: decoration.prefixIcon, | |
), | |
), | |
); | |
final Widget suffixIcon = decoration.suffixIcon == null ? null : | |
Center( | |
widthFactor: 1.0, | |
heightFactor: 1.0, | |
child: ConstrainedBox( | |
constraints: const BoxConstraints(minWidth: 48.0, minHeight: 48.0), | |
child: IconTheme.merge( | |
data: IconThemeData( | |
color: iconColor, | |
size: iconSize, | |
), | |
child: decoration.suffixIcon, | |
), | |
), | |
); | |
final Widget helperError = _HelperError( | |
textAlign: textAlign, | |
helperText: decoration.helperText, | |
helperStyle: _getHelperStyle(themeData), | |
errorText: decoration.errorText, | |
errorStyle: _getErrorStyle(themeData), | |
errorMaxLines: decoration.errorMaxLines, | |
); | |
Widget counter; | |
if (decoration.counter != null) { | |
counter = decoration.counter; | |
} else if (decoration.counterText != null && decoration.counterText != '') { | |
counter = Semantics( | |
container: true, | |
liveRegion: isFocused, | |
child: Text( | |
decoration.counterText, | |
style: _getHelperStyle(themeData).merge(decoration.counterStyle), | |
overflow: TextOverflow.ellipsis, | |
semanticsLabel: decoration.semanticCounterText, | |
), | |
); | |
} | |
// The _Decoration widget and _RenderDecoration assume that contentPadding | |
// has been resolved to EdgeInsets. | |
final TextDirection textDirection = Directionality.of(context); | |
final EdgeInsets decorationContentPadding = decoration.contentPadding?.resolve(textDirection); | |
EdgeInsets contentPadding; | |
double floatingLabelHeight; | |
if (decoration.isCollapsed) { | |
floatingLabelHeight = 0.0; | |
contentPadding = decorationContentPadding ?? EdgeInsets.zero; | |
} else if (!border.isOutline) { | |
// 4.0: the vertical gap between the inline elements and the floating label. | |
floatingLabelHeight = (4.0 + 0.75 * inlineLabelStyle.fontSize) * MediaQuery.textScaleFactorOf(context); | |
if (decoration.filled == true) { // filled == null same as filled == false | |
contentPadding = decorationContentPadding ?? (decorationIsDense | |
? const EdgeInsets.fromLTRB(12.0, 8.0, 12.0, 8.0) | |
: const EdgeInsets.fromLTRB(12.0, 12.0, 12.0, 12.0)); | |
} else { | |
// Not left or right padding for underline borders that aren't filled | |
// is a small concession to backwards compatibility. This eliminates | |
// the most noticeable layout change introduced by #13734. | |
contentPadding = decorationContentPadding ?? (decorationIsDense | |
? const EdgeInsets.fromLTRB(0.0, 8.0, 0.0, 8.0) | |
: const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 12.0)); | |
} | |
} else { | |
floatingLabelHeight = 0.0; | |
contentPadding = decorationContentPadding ?? (decorationIsDense | |
? const EdgeInsets.fromLTRB(12.0, 20.0, 12.0, 12.0) | |
: const EdgeInsets.fromLTRB(12.0, 24.0, 12.0, 16.0)); | |
} | |
return _Decorator( | |
decoration: _Decoration( | |
contentPadding: contentPadding, | |
isCollapsed: decoration.isCollapsed, | |
floatingLabelHeight: floatingLabelHeight, | |
floatingLabelProgress: _floatingLabelController.value, | |
border: border, | |
borderGap: _borderGap, | |
icon: icon, | |
input: widget.child, | |
label: label, | |
alignLabelWithHint: decoration.alignLabelWithHint, | |
hint: hint, | |
prefix: prefix, | |
suffix: suffix, | |
prefixIcon: prefixIcon, | |
suffixIcon: suffixIcon, | |
helperError: helperError, | |
counter: counter, | |
container: container, | |
), | |
textDirection: textDirection, | |
textBaseline: textBaseline, | |
textAlignVertical: widget.textAlignVertical, | |
isFocused: isFocused, | |
expands: widget.expands, | |
); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2015 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 'dart:collection'; | |
import 'package:flutter/cupertino.dart'; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter/rendering.dart'; | |
import 'package:flutter/services.dart'; | |
import 'package:flutter/widgets.dart'; | |
import 'package:flutter/foundation.dart'; | |
import 'package:flutter/gestures.dart'; | |
import 'package:prova/input_decorator_star.dart'; | |
export 'package:flutter/services.dart' show TextInputType, TextInputAction, TextCapitalization; | |
/// Signature for the [TextField.buildCounter] callback. | |
typedef InputCounterWidgetBuilder = Widget Function( | |
/// The build context for the TextField | |
BuildContext context, { | |
/// The length of the string currently in the input. | |
@required int currentLength, | |
/// The maximum string length that can be entered into the TextField. | |
@required int maxLength, | |
/// Whether or not the TextField is currently focused. Mainly provided for | |
/// the [liveRegion] parameter in the [Semantics] widget for accessibility. | |
@required bool isFocused, | |
}); | |
class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDetectorBuilder { | |
_TextFieldSelectionGestureDetectorBuilder({ | |
@required _TextFieldState state | |
}) : _state = state, | |
super(delegate: state); | |
final _TextFieldState _state; | |
@override | |
void onTapDown(TapDownDetails details) { | |
super.onTapDown(details); | |
_state._startSplash(details.globalPosition); | |
} | |
@override | |
void onForcePressStart(ForcePressDetails details) { | |
super.onForcePressStart(details); | |
if (delegate.selectionEnabled && shouldShowSelectionToolbar) { | |
editableText.showToolbar(); | |
} | |
} | |
@override | |
void onForcePressEnd(ForcePressDetails details) { | |
// Not required. | |
} | |
@override | |
void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) { | |
if (delegate.selectionEnabled) { | |
switch (Theme.of(_state.context).platform) { | |
case TargetPlatform.iOS: | |
renderEditable.selectPositionAt( | |
from: details.globalPosition, | |
cause: SelectionChangedCause.longPress, | |
); | |
break; | |
case TargetPlatform.android: | |
case TargetPlatform.fuchsia: | |
renderEditable.selectWordsInRange( | |
from: details.globalPosition - details.offsetFromOrigin, | |
to: details.globalPosition, | |
cause: SelectionChangedCause.longPress, | |
); | |
break; | |
} | |
} | |
} | |
@override | |
void onSingleTapUp(TapUpDetails details) { | |
editableText.hideToolbar(); | |
if (delegate.selectionEnabled) { | |
switch (Theme.of(_state.context).platform) { | |
case TargetPlatform.iOS: | |
renderEditable.selectWordEdge(cause: SelectionChangedCause.tap); | |
break; | |
case TargetPlatform.android: | |
case TargetPlatform.fuchsia: | |
renderEditable.selectPosition(cause: SelectionChangedCause.tap); | |
break; | |
} | |
} | |
_state._requestKeyboard(); | |
_state._confirmCurrentSplash(); | |
if (_state.widget.onTap != null) | |
_state.widget.onTap(); | |
} | |
@override | |
void onSingleTapCancel() { | |
_state._cancelCurrentSplash(); | |
} | |
@override | |
void onSingleLongTapStart(LongPressStartDetails details) { | |
if (delegate.selectionEnabled) { | |
switch (Theme.of(_state.context).platform) { | |
case TargetPlatform.iOS: | |
renderEditable.selectPositionAt( | |
from: details.globalPosition, | |
cause: SelectionChangedCause.longPress, | |
); | |
break; | |
case TargetPlatform.android: | |
case TargetPlatform.fuchsia: | |
renderEditable.selectWord(cause: SelectionChangedCause.longPress); | |
Feedback.forLongPress(_state.context); | |
break; | |
} | |
} | |
_state._confirmCurrentSplash(); | |
} | |
@override | |
void onDragSelectionStart(DragStartDetails details) { | |
super.onDragSelectionStart(details); | |
_state._startSplash(details.globalPosition); | |
} | |
} | |
/// A material design text field. | |
/// | |
/// A text field lets the user enter text, either with hardware keyboard or with | |
/// an onscreen keyboard. | |
/// | |
/// The text field calls the [onChanged] callback whenever the user changes the | |
/// text in the field. If the user indicates that they are done typing in the | |
/// field (e.g., by pressing a button on the soft keyboard), the text field | |
/// calls the [onSubmitted] callback. | |
/// | |
/// To control the text that is displayed in the text field, use the | |
/// [controller]. For example, to set the initial value of the text field, use | |
/// a [controller] that already contains some text. The [controller] can also | |
/// control the selection and composing region (and to observe changes to the | |
/// text, selection, and composing region). | |
/// | |
/// By default, a text field has a [decoration] that draws a divider below the | |
/// text field. You can use the [decoration] property to control the decoration, | |
/// for example by adding a label or an icon. If you set the [decoration] | |
/// property to null, the decoration will be removed entirely, including the | |
/// extra padding introduced by the decoration to save space for the labels. | |
/// | |
/// If [decoration] is non-null (which is the default), the text field requires | |
/// one of its ancestors to be a [Material] widget. When the [TextField] is | |
/// tapped an ink splash that paints on the material is triggered, see | |
/// [ThemeData.splashFactory]. | |
/// | |
/// To integrate the [TextField] into a [Form] with other [FormField] widgets, | |
/// consider using [TextFormField]. | |
/// | |
/// Remember to [dispose] of the [TextEditingController] when it is no longer needed. | |
/// This will ensure we discard any resources used by the object. | |
/// | |
/// {@tool sample} | |
/// This example shows how to create a [TextField] that will obscure input. The | |
/// [InputDecoration] surrounds the field in a border using [OutlineInputBorder] | |
/// and adds a label. | |
/// | |
///  | |
/// | |
/// ```dart | |
/// TextField( | |
/// obscureText: true, | |
/// decoration: InputDecoration( | |
/// border: OutlineInputBorder(), | |
/// labelText: 'Password', | |
/// ), | |
/// ) | |
/// ``` | |
/// {@end-tool} | |
/// | |
/// See also: | |
/// | |
/// * <https://material.io/design/components/text-fields.html> | |
/// * [TextFormField], which integrates with the [Form] widget. | |
/// * [InputDecorator], which shows the labels and other visual elements that | |
/// surround the actual text editing widget. | |
/// * [EditableText], which is the raw text editing control at the heart of a | |
/// [TextField]. The [EditableText] widget is rarely used directly unless | |
/// you are implementing an entirely different design language, such as | |
/// Cupertino. | |
/// * Learn how to use a [TextEditingController] in one of our | |
/// [cookbook recipe](https://flutter.dev/docs/cookbook/forms/text-field-changes#2-use-a-texteditingcontroller)s. | |
class TextFieldCustom extends StatefulWidget { | |
/// Creates a Material Design text field. | |
/// | |
/// If [decoration] is non-null (which is the default), the text field requires | |
/// one of its ancestors to be a [Material] widget. | |
/// | |
/// To remove the decoration entirely (including the extra padding introduced | |
/// by the decoration to save space for the labels), set the [decoration] to | |
/// null. | |
/// | |
/// The [maxLines] property can be set to null to remove the restriction on | |
/// the number of lines. By default, it is one, meaning this is a single-line | |
/// text field. [maxLines] must not be zero. | |
/// | |
/// The [maxLength] property is set to null by default, which means the | |
/// number of characters allowed in the text field is not restricted. If | |
/// [maxLength] is set a character counter will be displayed below the | |
/// field showing how many characters have been entered. If the value is | |
/// set to a positive integer it will also display the maximum allowed | |
/// number of characters to be entered. If the value is set to | |
/// [TextField.noMaxLength] then only the current length is displayed. | |
/// | |
/// After [maxLength] characters have been input, additional input | |
/// is ignored, unless [maxLengthEnforced] is set to false. The text field | |
/// enforces the length with a [LengthLimitingTextInputFormatter], which is | |
/// evaluated after the supplied [inputFormatters], if any. The [maxLength] | |
/// value must be either null or greater than zero. | |
/// | |
/// If [maxLengthEnforced] is set to false, then more than [maxLength] | |
/// characters may be entered, and the error counter and divider will | |
/// switch to the [decoration.errorStyle] when the limit is exceeded. | |
/// | |
/// The text cursor is not shown if [showCursor] is false or if [showCursor] | |
/// is null (the default) and [readOnly] is true. | |
/// | |
/// The [textAlign], [autofocus], [obscureText], [readOnly], [autocorrect], | |
/// [maxLengthEnforced], [scrollPadding], [maxLines], and [maxLength] | |
/// arguments must not be null. | |
/// | |
/// See also: | |
/// | |
/// * [maxLength], which discusses the precise meaning of "number of | |
/// characters" and how it may differ from the intuitive meaning. | |
const TextFieldCustom({ | |
Key key, | |
this.controller, | |
this.focusNode, | |
this.decoration = const InputDecoration(), | |
TextInputType keyboardType, | |
this.textInputAction, | |
this.textCapitalization = TextCapitalization.none, | |
this.style, | |
this.strutStyle, | |
this.textAlign = TextAlign.start, | |
this.textAlignVertical, | |
this.hintText, | |
this.textDirection, | |
this.readOnly = false, | |
ToolbarOptions toolbarOptions, | |
this.showCursor, | |
this.autofocus = false, | |
this.obscureText = false, | |
this.autocorrect = true, | |
this.maxLines = 1, | |
this.minLines, | |
this.expands = false, | |
this.maxLength, | |
this.maxLengthEnforced = true, | |
this.onChanged, | |
this.onEditingComplete, | |
this.onSubmitted, | |
this.inputFormatters, | |
this.enabled, | |
this.cursorWidth = 2.0, | |
this.cursorRadius, | |
this.cursorColor, | |
this.keyboardAppearance, | |
this.scrollPadding = const EdgeInsets.all(20.0), | |
this.dragStartBehavior = DragStartBehavior.start, | |
this.enableInteractiveSelection = true, | |
this.onTap, | |
this.buildCounter, | |
this.scrollController, | |
this.scrollPhysics, | |
}) : assert(textAlign != null), | |
assert(readOnly != null), | |
assert(autofocus != null), | |
assert(obscureText != null), | |
assert(autocorrect != null), | |
assert(enableInteractiveSelection != null), | |
assert(maxLengthEnforced != null), | |
assert(scrollPadding != null), | |
assert(dragStartBehavior != null), | |
assert(maxLines == null || maxLines > 0), | |
assert(minLines == null || minLines > 0), | |
assert( | |
(maxLines == null) || (minLines == null) || (maxLines >= minLines), | |
'minLines can\'t be greater than maxLines', | |
), | |
assert(expands != null), | |
assert( | |
!expands || (maxLines == null && minLines == null), | |
'minLines and maxLines must be null when expands is true.', | |
), | |
assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0), | |
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline), | |
toolbarOptions = toolbarOptions ?? obscureText ? | |
const ToolbarOptions( | |
selectAll: true, | |
paste: true, | |
) : | |
const ToolbarOptions( | |
copy: true, | |
cut: true, | |
selectAll: true, | |
paste: true, | |
), | |
super(key: key); | |
/// Controls the text being edited. | |
/// | |
/// If null, this widget will create its own [TextEditingController]. | |
final TextEditingController controller; | |
/// Defines the keyboard focus for this widget. | |
/// | |
/// The [focusNode] is a long-lived object that's typically managed by a | |
/// [StatefulWidget] parent. See [FocusNode] for more information. | |
/// | |
/// To give the keyboard focus to this widget, provide a [focusNode] and then | |
/// use the current [FocusScope] to request the focus: | |
/// | |
/// ```dart | |
/// FocusScope.of(context).requestFocus(myFocusNode); | |
/// ``` | |
/// | |
/// This happens automatically when the widget is tapped. | |
/// | |
/// To be notified when the widget gains or loses the focus, add a listener | |
/// to the [focusNode]: | |
/// | |
/// ```dart | |
/// focusNode.addListener(() { print(myFocusNode.hasFocus); }); | |
/// ``` | |
/// | |
/// If null, this widget will create its own [FocusNode]. | |
/// | |
/// ## Keyboard | |
/// | |
/// Requesting the focus will typically cause the keyboard to be shown | |
/// if it's not showing already. | |
/// | |
/// On Android, the user can hide the keyboard - without changing the focus - | |
/// with the system back button. They can restore the keyboard's visibility | |
/// by tapping on a text field. The user might hide the keyboard and | |
/// switch to a physical keyboard, or they might just need to get it | |
/// out of the way for a moment, to expose something it's | |
/// obscuring. In this case requesting the focus again will not | |
/// cause the focus to change, and will not make the keyboard visible. | |
/// | |
/// This widget builds an [EditableText] and will ensure that the keyboard is | |
/// showing when it is tapped by calling [EditableTextState.requestKeyboard()]. | |
final FocusNode focusNode; | |
/// The decoration to show around the text field. | |
/// | |
/// By default, draws a horizontal line under the text field but can be | |
/// configured to show an icon, label, hint text, and error text. | |
/// | |
/// Specify null to remove the decoration entirely (including the | |
/// extra padding introduced by the decoration to save space for the labels). | |
final InputDecoration decoration; | |
/// {@macro flutter.widgets.editableText.keyboardType} | |
final TextInputType keyboardType; | |
/// The type of action button to use for the keyboard. | |
/// | |
/// Defaults to [TextInputAction.newline] if [keyboardType] is | |
/// [TextInputType.multiline] and [TextInputAction.done] otherwise. | |
final TextInputAction textInputAction; | |
/// {@macro flutter.widgets.editableText.textCapitalization} | |
final TextCapitalization textCapitalization; | |
/// The style to use for the text being edited. | |
/// | |
/// This text style is also used as the base style for the [decoration]. | |
/// | |
/// If null, defaults to the `subhead` text style from the current [Theme]. | |
final TextStyle style; | |
/// {@macro flutter.widgets.editableText.strutStyle} | |
final StrutStyle strutStyle; | |
/// {@macro flutter.widgets.editableText.textAlign} | |
final TextAlign textAlign; | |
/// {@macro flutter.material.inputDecorator.textAlignVertical} | |
final TextAlignVertical textAlignVertical; | |
final Text hintText; | |
/// {@macro flutter.widgets.editableText.textDirection} | |
final TextDirection textDirection; | |
/// {@macro flutter.widgets.editableText.autofocus} | |
final bool autofocus; | |
/// {@macro flutter.widgets.editableText.obscureText} | |
final bool obscureText; | |
/// {@macro flutter.widgets.editableText.autocorrect} | |
final bool autocorrect; | |
/// {@macro flutter.widgets.editableText.maxLines} | |
final int maxLines; | |
/// {@macro flutter.widgets.editableText.minLines} | |
final int minLines; | |
/// {@macro flutter.widgets.editableText.expands} | |
final bool expands; | |
/// {@macro flutter.widgets.editableText.readOnly} | |
final bool readOnly; | |
/// Configuration of toolbar options. | |
/// | |
/// If not set, select all and paste will default to be enabled. Copy and cut | |
/// will be disabled if [obscureText] is true. If [readOnly] is true, | |
/// paste and cut will be disabled regardless. | |
final ToolbarOptions toolbarOptions; | |
/// {@macro flutter.widgets.editableText.showCursor} | |
final bool showCursor; | |
/// If [maxLength] is set to this value, only the "current input length" | |
/// part of the character counter is shown. | |
static const int noMaxLength = -1; | |
/// The maximum number of characters (Unicode scalar values) to allow in the | |
/// text field. | |
/// | |
/// If set, a character counter will be displayed below the | |
/// field showing how many characters have been entered. If set to a number | |
/// greater than 0, it will also display the maximum number allowed. If set | |
/// to [TextField.noMaxLength] then only the current character count is displayed. | |
/// | |
/// After [maxLength] characters have been input, additional input | |
/// is ignored, unless [maxLengthEnforced] is set to false. The text field | |
/// enforces the length with a [LengthLimitingTextInputFormatter], which is | |
/// evaluated after the supplied [inputFormatters], if any. | |
/// | |
/// This value must be either null, [TextField.noMaxLength], or greater than 0. | |
/// If null (the default) then there is no limit to the number of characters | |
/// that can be entered. If set to [TextField.noMaxLength], then no limit will | |
/// be enforced, but the number of characters entered will still be displayed. | |
/// | |
/// Whitespace characters (e.g. newline, space, tab) are included in the | |
/// character count. | |
/// | |
/// If [maxLengthEnforced] is set to false, then more than [maxLength] | |
/// characters may be entered, but the error counter and divider will | |
/// switch to the [decoration.errorStyle] when the limit is exceeded. | |
/// | |
/// ## Limitations | |
/// | |
/// The text field does not currently count Unicode grapheme clusters (i.e. | |
/// characters visible to the user), it counts Unicode scalar values, which | |
/// leaves out a number of useful possible characters (like many emoji and | |
/// composed characters), so this will be inaccurate in the presence of those | |
/// characters. If you expect to encounter these kinds of characters, be | |
/// generous in the maxLength used. | |
/// | |
/// For instance, the character "ö" can be represented as '\u{006F}\u{0308}', | |
/// which is the letter "o" followed by a composed diaeresis "¨", or it can | |
/// be represented as '\u{00F6}', which is the Unicode scalar value "LATIN | |
/// SMALL LETTER O WITH DIAERESIS". In the first case, the text field will | |
/// count two characters, and the second case will be counted as one | |
/// character, even though the user can see no difference in the input. | |
/// | |
/// Similarly, some emoji are represented by multiple scalar values. The | |
/// Unicode "THUMBS UP SIGN + MEDIUM SKIN TONE MODIFIER", "👍🏽", should be | |
/// counted as a single character, but because it is a combination of two | |
/// Unicode scalar values, '\u{1F44D}\u{1F3FD}', it is counted as two | |
/// characters. | |
/// | |
/// See also: | |
/// | |
/// * [LengthLimitingTextInputFormatter] for more information on how it | |
/// counts characters, and how it may differ from the intuitive meaning. | |
final int maxLength; | |
/// If true, prevents the field from allowing more than [maxLength] | |
/// characters. | |
/// | |
/// If [maxLength] is set, [maxLengthEnforced] indicates whether or not to | |
/// enforce the limit, or merely provide a character counter and warning when | |
/// [maxLength] is exceeded. | |
final bool maxLengthEnforced; | |
/// {@macro flutter.widgets.editableText.onChanged} | |
/// | |
/// See also: | |
/// | |
/// * [inputFormatters], which are called before [onChanged] | |
/// runs and can validate and change ("format") the input value. | |
/// * [onEditingComplete], [onSubmitted], [onSelectionChanged]: | |
/// which are more specialized input change notifications. | |
final ValueChanged<String> onChanged; | |
/// {@macro flutter.widgets.editableText.onEditingComplete} | |
final VoidCallback onEditingComplete; | |
/// {@macro flutter.widgets.editableText.onSubmitted} | |
/// | |
/// See also: | |
/// | |
/// * [EditableText.onSubmitted] for an example of how to handle moving to | |
/// the next/previous field when using [TextInputAction.next] and | |
/// [TextInputAction.previous] for [textInputAction]. | |
final ValueChanged<String> onSubmitted; | |
/// {@macro flutter.widgets.editableText.inputFormatters} | |
final List<TextInputFormatter> inputFormatters; | |
/// If false the text field is "disabled": it ignores taps and its | |
/// [decoration] is rendered in grey. | |
/// | |
/// If non-null this property overrides the [decoration]'s | |
/// [Decoration.enabled] property. | |
final bool enabled; | |
/// {@macro flutter.widgets.editableText.cursorWidth} | |
final double cursorWidth; | |
/// {@macro flutter.widgets.editableText.cursorRadius} | |
final Radius cursorRadius; | |
/// The color to use when painting the cursor. | |
/// | |
/// Defaults to [ThemeData.cursorColor] or [CupertinoTheme.primaryColor] | |
/// depending on [ThemeData.platform]. | |
final Color cursorColor; | |
/// The appearance of the keyboard. | |
/// | |
/// This setting is only honored on iOS devices. | |
/// | |
/// If unset, defaults to the brightness of [ThemeData.primaryColorBrightness]. | |
final Brightness keyboardAppearance; | |
/// {@macro flutter.widgets.editableText.scrollPadding} | |
final EdgeInsets scrollPadding; | |
/// {@macro flutter.widgets.editableText.enableInteractiveSelection} | |
final bool enableInteractiveSelection; | |
/// {@macro flutter.widgets.scrollable.dragStartBehavior} | |
final DragStartBehavior dragStartBehavior; | |
/// {@macro flutter.rendering.editable.selectionEnabled} | |
bool get selectionEnabled => enableInteractiveSelection; | |
/// {@template flutter.material.textfield.onTap} | |
/// Called for each distinct tap except for every second tap of a double tap. | |
/// | |
/// The text field builds a [GestureDetector] to handle input events like tap, | |
/// to trigger focus requests, to move the caret, adjust the selection, etc. | |
/// Handling some of those events by wrapping the text field with a competing | |
/// GestureDetector is problematic. | |
/// | |
/// To unconditionally handle taps, without interfering with the text field's | |
/// internal gesture detector, provide this callback. | |
/// | |
/// If the text field is created with [enabled] false, taps will not be | |
/// recognized. | |
/// | |
/// To be notified when the text field gains or loses the focus, provide a | |
/// [focusNode] and add a listener to that. | |
/// | |
/// To listen to arbitrary pointer events without competing with the | |
/// text field's internal gesture detector, use a [Listener]. | |
/// {@endtemplate} | |
final GestureTapCallback onTap; | |
/// Callback that generates a custom [InputDecorator.counter] widget. | |
/// | |
/// See [InputCounterWidgetBuilder] for an explanation of the passed in | |
/// arguments. The returned widget will be placed below the line in place of | |
/// the default widget built when [counterText] is specified. | |
/// | |
/// The returned widget will be wrapped in a [Semantics] widget for | |
/// accessibility, but it also needs to be accessible itself. For example, | |
/// if returning a Text widget, set the [semanticsLabel] property. | |
/// | |
/// {@tool sample} | |
/// ```dart | |
/// Widget counter( | |
/// BuildContext context, | |
/// { | |
/// int currentLength, | |
/// int maxLength, | |
/// bool isFocused, | |
/// } | |
/// ) { | |
/// return Text( | |
/// '$currentLength of $maxLength characters', | |
/// semanticsLabel: 'character count', | |
/// ); | |
/// } | |
/// ``` | |
/// {@end-tool} | |
final InputCounterWidgetBuilder buildCounter; | |
/// {@macro flutter.widgets.edtiableText.scrollPhysics} | |
final ScrollPhysics scrollPhysics; | |
/// {@macro flutter.widgets.editableText.scrollController} | |
final ScrollController scrollController; | |
@override | |
_TextFieldState createState() => _TextFieldState(); | |
@override | |
void debugFillProperties(DiagnosticPropertiesBuilder properties) { | |
super.debugFillProperties(properties); | |
properties.add(DiagnosticsProperty<TextEditingController>('controller', controller, defaultValue: null)); | |
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode, defaultValue: null)); | |
properties.add(DiagnosticsProperty<bool>('enabled', enabled, defaultValue: null)); | |
properties.add(DiagnosticsProperty<InputDecoration>('decoration', decoration, defaultValue: const InputDecoration())); | |
properties.add(DiagnosticsProperty<TextInputType>('keyboardType', keyboardType, defaultValue: TextInputType.text)); | |
properties.add(DiagnosticsProperty<TextStyle>('style', style, defaultValue: null)); | |
properties.add(DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false)); | |
properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false)); | |
properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: true)); | |
properties.add(IntProperty('maxLines', maxLines, defaultValue: 1)); | |
properties.add(IntProperty('minLines', minLines, defaultValue: null)); | |
properties.add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false)); | |
properties.add(IntProperty('maxLength', maxLength, defaultValue: null)); | |
properties.add(FlagProperty('maxLengthEnforced', value: maxLengthEnforced, defaultValue: true, ifFalse: 'maxLength not enforced')); | |
properties.add(EnumProperty<TextInputAction>('textInputAction', textInputAction, defaultValue: null)); | |
properties.add(EnumProperty<TextCapitalization>('textCapitalization', textCapitalization, defaultValue: TextCapitalization.none)); | |
properties.add(EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: TextAlign.start)); | |
properties.add(DiagnosticsProperty<TextAlignVertical>('textAlignVertical', textAlignVertical, defaultValue: null)); | |
properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null)); | |
properties.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0)); | |
properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius, defaultValue: null)); | |
properties.add(ColorProperty('cursorColor', cursorColor, defaultValue: null)); | |
properties.add(DiagnosticsProperty<Brightness>('keyboardAppearance', keyboardAppearance, defaultValue: null)); | |
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>('scrollPadding', scrollPadding, defaultValue: const EdgeInsets.all(20.0))); | |
properties.add(FlagProperty('selectionEnabled', value: selectionEnabled, defaultValue: true, ifFalse: 'selection disabled')); | |
properties.add(DiagnosticsProperty<ScrollController>('scrollController', scrollController, defaultValue: null)); | |
properties.add(DiagnosticsProperty<ScrollPhysics>('scrollPhysics', scrollPhysics, defaultValue: null)); | |
} | |
} | |
class _TextFieldState extends State<TextFieldCustom> with AutomaticKeepAliveClientMixin implements TextSelectionGestureDetectorBuilderDelegate { | |
Set<InteractiveInkFeature> _splashes; | |
InteractiveInkFeature _currentSplash; | |
TextEditingController _controller; | |
TextEditingController get _effectiveController => widget.controller ?? _controller; | |
FocusNode _focusNode; | |
FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode()); | |
bool _isHovering = false; | |
bool get needsCounter => widget.maxLength != null | |
&& widget.decoration != null | |
&& widget.decoration.counterText == null; | |
bool _showSelectionHandles = false; | |
_TextFieldSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder; | |
// API for TextSelectionGestureDetectorBuilderDelegate. | |
@override | |
bool forcePressEnabled; | |
@override | |
final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>(); | |
@override | |
bool get selectionEnabled => widget.selectionEnabled; | |
// End of API for TextSelectionGestureDetectorBuilderDelegate. | |
InputDecoration _getEffectiveDecoration() { | |
final MaterialLocalizations localizations = MaterialLocalizations.of(context); | |
final ThemeData themeData = Theme.of(context); | |
final InputDecoration effectiveDecoration = (widget.decoration ?? const InputDecoration()) | |
.applyDefaults(themeData.inputDecorationTheme) | |
.copyWith( | |
enabled: widget.enabled, | |
hintMaxLines: widget.decoration?.hintMaxLines ?? widget.maxLines, | |
); | |
// No need to build anything if counter or counterText were given directly. | |
if (effectiveDecoration.counter != null || effectiveDecoration.counterText != null) | |
return effectiveDecoration; | |
// If buildCounter was provided, use it to generate a counter widget. | |
Widget counter; | |
final int currentLength = _effectiveController.value.text.runes.length; | |
if (effectiveDecoration.counter == null | |
&& effectiveDecoration.counterText == null | |
&& widget.buildCounter != null) { | |
final bool isFocused = _effectiveFocusNode.hasFocus; | |
counter = Semantics( | |
container: true, | |
liveRegion: isFocused, | |
child: widget.buildCounter( | |
context, | |
currentLength: currentLength, | |
maxLength: widget.maxLength, | |
isFocused: isFocused, | |
), | |
); | |
return effectiveDecoration.copyWith(counter: counter); | |
} | |
if (widget.maxLength == null) | |
return effectiveDecoration; // No counter widget | |
String counterText = '$currentLength'; | |
String semanticCounterText = ''; | |
// Handle a real maxLength (positive number) | |
if (widget.maxLength > 0) { | |
// Show the maxLength in the counter | |
counterText += '/${widget.maxLength}'; | |
final int remaining = (widget.maxLength - currentLength).clamp(0, widget.maxLength); | |
semanticCounterText = localizations.remainingTextFieldCharacterCount(remaining); | |
// Handle length exceeds maxLength | |
if (_effectiveController.value.text.runes.length > widget.maxLength) { | |
return effectiveDecoration.copyWith( | |
errorText: effectiveDecoration.errorText ?? '', | |
counterStyle: effectiveDecoration.errorStyle | |
?? themeData.textTheme.caption.copyWith(color: themeData.errorColor), | |
counterText: counterText, | |
semanticCounterText: semanticCounterText, | |
); | |
} | |
} | |
return effectiveDecoration.copyWith( | |
counterText: counterText, | |
semanticCounterText: semanticCounterText, | |
); | |
} | |
@override | |
void initState() { | |
super.initState(); | |
_selectionGestureDetectorBuilder = _TextFieldSelectionGestureDetectorBuilder(state: this); | |
if (widget.controller == null) | |
_controller = TextEditingController(); | |
} | |
@override | |
void didUpdateWidget(TextFieldCustom oldWidget) { | |
super.didUpdateWidget(oldWidget); | |
if (widget.controller == null && oldWidget.controller != null) | |
_controller = TextEditingController.fromValue(oldWidget.controller.value); | |
else if (widget.controller != null && oldWidget.controller == null) | |
_controller = null; | |
final bool isEnabled = widget.enabled ?? widget.decoration?.enabled ?? true; | |
final bool wasEnabled = oldWidget.enabled ?? oldWidget.decoration?.enabled ?? true; | |
if (wasEnabled && !isEnabled) { | |
_effectiveFocusNode.unfocus(); | |
} | |
if (_effectiveFocusNode.hasFocus && widget.readOnly != oldWidget.readOnly) { | |
if(_effectiveController.selection.isCollapsed) { | |
_showSelectionHandles = !widget.readOnly; | |
} | |
} | |
} | |
@override | |
void dispose() { | |
_focusNode?.dispose(); | |
super.dispose(); | |
} | |
EditableTextState get _editableText => editableTextKey.currentState; | |
void _requestKeyboard() { | |
_editableText?.requestKeyboard(); | |
} | |
bool _shouldShowSelectionHandles(SelectionChangedCause cause) { | |
// When the text field is activated by something that doesn't trigger the | |
// selection overlay, we shouldn't show the handles either. | |
if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar) | |
return false; | |
if (cause == SelectionChangedCause.keyboard) | |
return false; | |
if (widget.readOnly && _effectiveController.selection.isCollapsed) | |
return false; | |
if (cause == SelectionChangedCause.longPress) | |
return true; | |
if (_effectiveController.text.isNotEmpty) | |
return true; | |
return false; | |
} | |
void _handleSelectionChanged(TextSelection selection, SelectionChangedCause cause) { | |
final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause); | |
if (willShowSelectionHandles != _showSelectionHandles) { | |
setState(() { | |
_showSelectionHandles = willShowSelectionHandles; | |
}); | |
} | |
switch (Theme.of(context).platform) { | |
case TargetPlatform.iOS: | |
if (cause == SelectionChangedCause.longPress) { | |
_editableText?.bringIntoView(selection.base); | |
} | |
return; | |
case TargetPlatform.android: | |
case TargetPlatform.fuchsia: | |
// Do nothing. | |
} | |
} | |
/// Toggle the toolbar when a selection handle is tapped. | |
void _handleSelectionHandleTapped() { | |
if (_effectiveController.selection.isCollapsed) { | |
_editableText.toggleToolbar(); | |
} | |
} | |
InteractiveInkFeature _createInkFeature(Offset globalPosition) { | |
final MaterialInkController inkController = Material.of(context); | |
final ThemeData themeData = Theme.of(context); | |
final BuildContext editableContext = editableTextKey.currentContext; | |
final RenderBox referenceBox = InputDecorator.containerOf(editableContext) ?? editableContext.findRenderObject(); | |
final Offset position = referenceBox.globalToLocal(globalPosition); | |
final Color color = themeData.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 = themeData.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, | |
textDirection: Directionality.of(context), | |
); | |
return splash; | |
} | |
void _startSplash(Offset globalPosition) { | |
if (_effectiveFocusNode.hasFocus) | |
return; | |
final InteractiveInkFeature splash = _createInkFeature(globalPosition); | |
_splashes ??= HashSet<InteractiveInkFeature>(); | |
_splashes.add(splash); | |
_currentSplash = splash; | |
updateKeepAlive(); | |
} | |
void _confirmCurrentSplash() { | |
_currentSplash?.confirm(); | |
_currentSplash = null; | |
} | |
void _cancelCurrentSplash() { | |
_currentSplash?.cancel(); | |
} | |
@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(); | |
} | |
void _handleMouseEnter(PointerEnterEvent event) => _handleHover(true); | |
void _handleMouseExit(PointerExitEvent event) => _handleHover(false); | |
void _handleHover(bool hovering) { | |
if (hovering != _isHovering) { | |
setState(() { | |
return _isHovering = hovering; | |
}); | |
} | |
} | |
@override | |
Widget build(BuildContext context) { | |
super.build(context); // See AutomaticKeepAliveClientMixin. | |
assert(debugCheckHasMaterial(context)); | |
// TODO(jonahwilliams): uncomment out this check once we have migrated tests. | |
// assert(debugCheckHasMaterialLocalizations(context)); | |
assert(debugCheckHasDirectionality(context)); | |
assert( | |
!(widget.style != null && widget.style.inherit == false && | |
(widget.style.fontSize == null || widget.style.textBaseline == null)), | |
'inherit false style must supply fontSize and textBaseline', | |
); | |
final ThemeData themeData = Theme.of(context); | |
final TextStyle style = themeData.textTheme.subhead.merge(widget.style); | |
final Brightness keyboardAppearance = widget.keyboardAppearance ?? themeData.primaryColorBrightness; | |
final TextEditingController controller = _effectiveController; | |
final FocusNode focusNode = _effectiveFocusNode; | |
final List<TextInputFormatter> formatters = widget.inputFormatters ?? <TextInputFormatter>[]; | |
if (widget.maxLength != null && widget.maxLengthEnforced) | |
formatters.add(LengthLimitingTextInputFormatter(widget.maxLength)); | |
TextSelectionControls textSelectionControls; | |
bool paintCursorAboveText; | |
bool cursorOpacityAnimates; | |
Offset cursorOffset; | |
Color cursorColor = widget.cursorColor; | |
Radius cursorRadius = widget.cursorRadius; | |
switch (themeData.platform) { | |
case TargetPlatform.iOS: | |
forcePressEnabled = true; | |
textSelectionControls = cupertinoTextSelectionControls; | |
paintCursorAboveText = true; | |
cursorOpacityAnimates = true; | |
cursorColor ??= CupertinoTheme.of(context).primaryColor; | |
cursorRadius ??= const Radius.circular(2.0); | |
cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0); | |
break; | |
case TargetPlatform.android: | |
case TargetPlatform.fuchsia: | |
forcePressEnabled = false; | |
textSelectionControls = materialTextSelectionControls; | |
paintCursorAboveText = false; | |
cursorOpacityAnimates = false; | |
cursorColor ??= themeData.cursorColor; | |
break; | |
} | |
Widget child = RepaintBoundary( | |
child: EditableText( | |
key: editableTextKey, | |
readOnly: widget.readOnly, | |
toolbarOptions: widget.toolbarOptions, | |
showCursor: widget.showCursor, | |
showSelectionHandles: _showSelectionHandles, | |
controller: controller, | |
focusNode: focusNode, | |
keyboardType: widget.keyboardType, | |
textInputAction: widget.textInputAction, | |
textCapitalization: widget.textCapitalization, | |
style: style, | |
strutStyle: widget.strutStyle, | |
textAlign: widget.textAlign, | |
textDirection: widget.textDirection, | |
autofocus: widget.autofocus, | |
obscureText: widget.obscureText, | |
autocorrect: widget.autocorrect, | |
maxLines: widget.maxLines, | |
minLines: widget.minLines, | |
expands: widget.expands, | |
selectionColor: themeData.textSelectionColor, | |
selectionControls: widget.selectionEnabled ? textSelectionControls : null, | |
onChanged: widget.onChanged, | |
onSelectionChanged: _handleSelectionChanged, | |
onEditingComplete: widget.onEditingComplete, | |
onSubmitted: widget.onSubmitted, | |
onSelectionHandleTapped: _handleSelectionHandleTapped, | |
inputFormatters: formatters, | |
rendererIgnoresPointer: true, | |
cursorWidth: widget.cursorWidth, | |
cursorRadius: cursorRadius, | |
cursorColor: cursorColor, | |
cursorOpacityAnimates: cursorOpacityAnimates, | |
cursorOffset: cursorOffset, | |
paintCursorAboveText: paintCursorAboveText, | |
backgroundCursorColor: CupertinoColors.inactiveGray, | |
scrollPadding: widget.scrollPadding, | |
keyboardAppearance: keyboardAppearance, | |
enableInteractiveSelection: widget.enableInteractiveSelection, | |
dragStartBehavior: widget.dragStartBehavior, | |
scrollController: widget.scrollController, | |
scrollPhysics: widget.scrollPhysics, | |
), | |
); | |
if (widget.decoration != null) { | |
child = AnimatedBuilder( | |
animation: Listenable.merge(<Listenable>[ focusNode, controller ]), | |
builder: (BuildContext context, Widget child) { | |
return InputDecoratorCustom( | |
decoration: _getEffectiveDecoration(), | |
baseStyle: widget.style, | |
textAlign: widget.textAlign, | |
textAlignVertical: widget.textAlignVertical, | |
hintText: widget.hintText, | |
isHovering: _isHovering, | |
isFocused: focusNode.hasFocus, | |
isEmpty: controller.value.text.isEmpty, | |
expands: widget.expands, | |
child: child, | |
); | |
}, | |
child: child, | |
); | |
} | |
return Semantics( | |
onTap: () { | |
if (!_effectiveController.selection.isValid) | |
_effectiveController.selection = TextSelection.collapsed(offset: _effectiveController.text.length); | |
_requestKeyboard(); | |
}, | |
child: MouseRegion( | |
onEnter: _handleMouseEnter, | |
onExit: _handleMouseExit, | |
child: IgnorePointer( | |
ignoring: !(widget.enabled ?? widget.decoration?.enabled ?? true), | |
child: _selectionGestureDetectorBuilder.buildGestureDetector( | |
behavior: HitTestBehavior.translucent, | |
child: child, | |
), | |
), | |
), | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
These two gists saved a lot of hardworks of mine today. Thanks for sharing.