You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中TextFormField输入时错误消息垂直截断问题

输入框错误消息垂直截断问题解决

问题背景

我封装了一个基于TextFormField的CustomTextFormField组件,在用户资料页面使用时遇到问题:输入框为空时错误消息显示正常,但用户开始输入时,错误消息会被垂直截断,效果如下:

输入时错误消息截断

自定义输入框组件代码

import 'package:flutter/material.dart';

class CustomTextFormField extends StatelessWidget {
  final TextEditingController controller;
  final String hintText;
  final String label;
  final bool obscureText;
  final FormFieldValidator<String>? validator;
  final ValueChanged<String>? onChanged;

  const CustomTextFormField({
    Key? key,
    required this.controller,
    required this.hintText,
    this.obscureText = false,
    required this.label,
    this.validator,
    this.onChanged,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return TextFormField(
      autovalidateMode: AutovalidateMode.onUserInteraction,
      controller: controller,
      obscureText: obscureText,
      validator: validator,
      onChanged: onChanged,
      decoration: InputDecoration(
        filled: true,
        fillColor: Theme.of(context).colorScheme.surfaceVariant,
        contentPadding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
        labelText: label,
        labelStyle: Theme.of(context).textTheme.labelSmall,
        hintText: hintText,
        hintStyle: Theme.of(context).textTheme.labelMedium,
        border: OutlineInputBorder(
          borderSide: BorderSide(width: 0.50, color: Theme.of(context).colorScheme.primary),
          gapPadding: 0,
          borderRadius: const BorderRadius.all(
            Radius.circular(10),
          ),
        ),
      ),
    );
  }
}

组件使用场景(用户资料页面)

class UserProfileScreen extends GetView<UserController> {
  UserProfileScreen({Key? key}) : super(key: key);

  final _formKey = GlobalKey<FormState>();
  final _usernameFormKey = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('User Profile'),
      ),
      drawer: CustomDrawer(),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Obx(() {
            UserModel user = controller.user.value!;
            return Column(
              mainAxisAlignment: MainAxisAlignment.start,
              children: <Widget>[
                const CircleAvatar(
                  radius: 50,
                  backgroundColor: Colors.grey,
                  child: Icon(
                    Icons.person,
                    size: 50,
                  ),
                ),
                const SizedBox(height: 20.0),
                Text(
                  user.username,
                  style: const TextStyle(
                    fontSize: 20.0,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(height: 10.0),
                ElevatedButton(
                  onPressed: () {
                    showDialog(
                      context: context,
                      builder: (_) => AlertDialog(
                        title: const Text('New Username'),
                        content: Form(
                          key: _usernameFormKey,
                          child: CustomTextFormField(
                            controller: controller.usernameController,
                            label: 'Username',
                            hintText: 'Enter your username',
                            onChanged: (value) {
                              user = user.copyWith(username: value);
                              controller.user.value = user;
                            },
                            validator: FormValidators.validateUsername,
                          ),
                        ),
                        actions: [
                          TextButton(
                            child: const Text('Cancel'),
                            onPressed: () => Navigator.of(context).pop(),
                          ),
                          TextButton(
                            child: const Text('Save'),
                            onPressed: () {
                              if (_usernameFormKey.currentState!.validate()) {
                                controller.updateUser(user);
                                Navigator.of(context).pop();
                              }
                            },
                          ),
                        ],
                      ),
                    );
                  },
                  child: const Text("Change Username"),
                ),
                // 省略剩余代码
              ],
            );
          }),
        ),
      ),
    );
  }
}

应用主题配置

// AppTheme类
import 'package:flutter/material.dart';
import 'package:mastermind_together/src/ui/theme/color_scheme.dart';
import 'package:mastermind_together/src/ui/theme/text_styles.dart';

class AppTheme {
  AppTheme._();

  static ThemeData lightTheme = ThemeData(
    colorScheme: lightThemeColors,
    useMaterial3: true,
    textTheme: textTheme,
    buttonTheme: ButtonThemeData(
      buttonColor: lightThemeColors.primary,
      textTheme: ButtonTextTheme.primary,
    ),
    canvasColor: lightThemeColors.surface,
  );

  static TextTheme textTheme = const TextTheme(
    displayLarge: h1,
    displayMedium: h2,
    bodyLarge: bodyMedium,
    bodyMedium: body,
    bodySmall: labelSmall,
    labelLarge: btnText,
    labelMedium: placeholderBodyMedium,
    labelSmall: labelText,
    headlineMedium: h3,
    titleMedium: bodyMedium,
  );
}

// 文本样式定义
import 'package:flutter/material.dart';
import 'package:mastermind_together/src/ui/theme/color_scheme.dart';

const String fontFamily = 'Inter';
const Color textColor = darkerPrimaryColor;
const Color textPlaceholderColor = placeholderColor;
const Color btnTextColor = Colors.white; //TODO move to color_scheme
const Color linkColor = Colors.blue;

const TextStyle h1 = TextStyle(
  color: textColor,
  fontSize: 64,
  fontFamily: fontFamily,
  fontWeight: FontWeight.w700,
);

const TextStyle h2 = TextStyle(
  color: textColor,
  fontSize: 42,
  fontFamily: fontFamily,
  fontWeight: FontWeight.w400,
);

const TextStyle h3 = TextStyle(
  color: textColor,
  fontSize: 26,
  fontFamily: fontFamily,
  fontWeight: FontWeight.w700,
);
const TextStyle body = TextStyle(
  color: textColor,
  fontSize: 16,
  fontFamily: fontFamily,
  fontWeight: FontWeight.w400,
);

const TextStyle bodyMediumLink = TextStyle(
  color: textColor,
  fontSize: 16,
  fontFamily: fontFamily,
  fontWeight: FontWeight.w500,
  decoration: TextDecoration.underline,
);

const TextStyle linkStyle = TextStyle(
  color: linkColor,
  fontSize: 16,
  fontFamily: fontFamily,
  fontWeight: FontWeight.w700,
  decoration: TextDecoration.underline,
);

const TextStyle bodyMedium = TextStyle(
  color: textColor,
  fontSize: 16,
  fontFamily: fontFamily,
  fontWeight: FontWeight.w500,
);

const TextStyle placeholderBodyMedium = TextStyle(
  color: textPlaceholderColor,
  fontSize: 16,
  fontFamily: fontFamily,
  fontWeight: FontWeight.w400,
  height: 1.6,
);

const TextStyle labelText = TextStyle(
  color: textColor,
  fontSize: 14,
  fontFamily: fontFamily,
  fontWeight: FontWeight.w500,
  // height: 2.0,
);

const TextStyle cardTitle = TextStyle(
  color: textColor,
  fontSize: 26,
  fontFamily: fontFamily,
  fontWeight: FontWeight.w600,
);

const TextStyle labelSmall = TextStyle(
  color: textColor,
  fontSize: 10,
  fontFamily: fontFamily,
  fontWeight: FontWeight.w400,
  height: 0.4,
);

const TextStyle btnText = TextStyle(
  color: btnTextColor,
  fontSize: 16,
  fontFamily: fontFamily,
  fontWeight: FontWeight.w700,
);

问题原因

问题出在主题配置的labelSmall样式中,你设置了height: 0.4,这个值过小,导致错误消息文本的行高被强制压缩。当输入框有内容时,错误消息的垂直空间被挤压,最终出现截断现象。

解决方法

方案1:修改主题中的行高

调整labelSmall的height属性为合理值(比如1.2),或者直接移除该属性使用默认行高:

const TextStyle labelSmall = TextStyle(
  color: textColor,
  fontSize: 10,
  fontFamily: fontFamily,
  fontWeight: FontWeight.w400,
  height: 1.2, // 修改为合理行高,或删除此行
);

方案2:在输入框中显式指定错误样式

在CustomTextFormField的InputDecoration中单独设置错误文本样式,覆盖主题的影响:

decoration: InputDecoration(
  // 保留原有配置
  errorStyle: TextStyle(
    height: 1.2,
    fontSize: 10,
    fontFamily: fontFamily,
    color: Colors.red, // 可选:指定错误文本颜色
  ),
),

两种方案都能保证错误消息拥有足够的垂直显示空间,避免截断。


内容的提问来源于stack exchange,提问作者Răzvan Puiu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 10:44:54