我需要完全自定义输入字段的呈现方式,尤其是错误消息的呈现方式。我需要将它们显示在字段下方,而无需对该字段进行任何其他修改。
现在,当我按照文档中的说明设置helperText: ' '
时,将调整字段的大小(默认)或将其固定,但是字段高度变得很大,以保留错误消息的空间。
我当然可以在我的字段下方添加一个Text
小部件来解决错误(这是我实际上所做的),但是我不能使用表单验证,因为它依赖于输入的内置错误管理小部件以显示错误消息...因此这将需要重新创建我自己的表单验证。
我一直在考虑的一件事是阻止表单验证显示错误消息,但我找不到解决方法。
有什么主意吗?
编辑:下面的一些代码:
class CustomTextFormField extends StatefulWidget {
final FormFieldSetter<String> onSaved;
final ValueChanged<String> onFieldSubmitted;
final TextInputAction textInputAction;
final String initialValue;
final bool autofocus;
final bool obscureText;
final String label;
final bool withDivider;
final FocusNode focusNode;
final bool required;
CustomTextFormField(
{Key key,
this.onSaved,
this.onFieldSubmitted,
this.textInputAction,
this.initialValue,
this.autofocus = false,
this.obscureText = false,
this.label,
this.withDivider = false,
this.focusNode,
this.required})
: super(key: key);
CustomTextFormFieldState createState() => CustomTextFormFieldState();
}
class CustomTextFormFieldState extends State<CustomTextFormField> with CustomFormFieldState {
bool _isMasked = false;
String _showObscureIcon;
FocusNode _focusNode;
TextEditingController _controller;
String _errorText;
/// True if this field has any validation errors.
bool get hasError => _errorText != null;
@override
void initState() {
// Add a listener to the focusNode to detect when we loose focus
_focusNode = widget.focusNode != null ? widget.focusNode : FocusNode();
_focusNode.addListener(lostFocusListener);
// Create the TextEditingController for the field with optional initial value
_controller = TextEditingController(text: widget.initialValue);
_controller.addListener(() {
CustomForm.of(context).fieldDidChange(); // CustomForm is basically a simplified and slightly adapted version of the built-in Form
});
// Set obscure properties
if (widget.obscureText) {
_isMasked = true;
_showObscureIcon = 'assets/img/icon_displaypassword.png';
}
super.initState();
}
/// Displays the field label
Widget _inputLabel() {
return Container(
child: Text(widget.label,
style: hasError ? TextStyle(color: themeErrorColor) : null),
margin: const EdgeInsets.only(bottom: 8.0),
);
}
/// Displays the field
Widget _inputField() {
return Expanded(
child: TextFormField(
decoration: InputDecoration(
enabledBorder: themeInputBorder,
focusedBorder: themeInputBorder,
contentPadding: EdgeInsets.all(14.0),
),
textInputAction: widget.textInputAction,
autofocus: widget.autofocus,
onSaved: widget.onSaved,
onFieldSubmitted: widget.onFieldSubmitted,
obscureText: _isMasked,
focusNode: _focusNode,
controller: _controller,
));
}
/// Displays the obscured toggle button
Widget _obscureButton() {
return IconButton(
icon: Image.asset(_showObscureIcon),
onPressed: _toggleObscureText,
);
}
/// Displays the decorated field
Widget _decoratedField() {
// Row that contains the input field
List<Widget> rowChildren = <Widget>[_inputField()];
// If the field must be oscured, we add the toggle button to the row
if (widget.obscureText) {
rowChildren.add(_obscureButton());
}
return Container(
decoration: hasError ? themeInputErrorDecoration : themeInputDecoration, // themeInputErrorDecoration and themeInputDecoration are LinearGradient defined elsewhere
padding: const EdgeInsets.all(1.0),
child: Container(
decoration: BoxDecoration(
color: Colors.white, borderRadius: BorderRadius.circular(2.0)),
child: Row(
children: rowChildren,
),
));
}
/// Displays the error message if any
Widget _errorLabel() {
return Offstage(
offstage: !hasError,
child: Container(
margin: EdgeInsets.only(top: 10.0),
child: Text(_errorText == null ? '' : _errorText,
style: TextStyle(color: themeErrorColor))));
}
/// Toggles beetween obscured/clear text and action icon
void _toggleObscureText() {
setState(() {
_isMasked = !_isMasked;
_showObscureIcon = _isMasked
? 'assets/img/icon_displaypassword.png'
: 'assets/img/icon_dontdisplaypassword.png';
});
}
/// Callback called when the focus has changed. Validates the input text
void lostFocusListener() {
if (!_focusNode.hasFocus) {
if (!touched) {
touched = true;
}
}
}
/// Saves the field
void save() {
widget.onSaved(_controller.text);
}
/// Resets the field to its initial value.
void reset() {
setState(() {
_controller.text = widget.initialValue;
_errorText = null;
});
}
/// Validates the field and set the [_errorText]. Returns true if there
/// were no errors.
bool validate() {
// If the field has not been touched yet, the field is validated
if(!touched) {
return true;
}
setState(() {
_validate();
});
return !hasError;
}
void _validate() {
// TODO: write a real validator
if (_controller.text.isEmpty && widget.required) {
_errorText = 'Field is required';
} else {
_errorText = null;
}
}
@override
Widget build(BuildContext context) {
register(context);
// Widgets for the column
List<Widget> children = <Widget>[
_inputLabel(),
_decoratedField(),
_errorLabel()
];
// Adds a divider if neeed to the column
if (widget.withDivider) {
children.add(Divider(height: 40.0));
}
// Renders the column
return Column(
crossAxisAlignment: CrossAxisAlignment.start, children: children);
}
@override
void dispose() {
// Clean up the controller when the widget is disposed.
_controller.dispose();
super.dispose();
}
@override
void deactivate() {
unregister(context);
super.deactivate();
}
}
abstract class CustomFormFieldState {
String errorText;
/// True if this field has any validation errors.
bool get hasError => errorText != null;
bool touched = false;
void register(BuildContext context) {
CustomForm.of(context)?.register(this);
}
void unregister(BuildContext context) {
CustomForm.of(context)?.unregister(this);
}
void save();
void reset();
bool validate();
}