是否可以将错误消息放置在TextFormField下方?

时间:2019-07-30 20:45:31

标签: flutter

我需要完全自定义输入字段的呈现方式,尤其是错误消息的呈现方式。我需要将它们显示在字段下方,而无需对该字段进行任何其他修改。

现在,当我按照文档中的说明设置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();
}

0 个答案:

没有答案