Skip to content

Material 3 表单组件样式规范

最后更新:2026-01-18 版本:v1.0 状态:✅ 强制执行

💡 提示:本文档是表单组件的详细实战指南。如需查看完整的 Material 3 设计系统规范(包括文本、图标、颜色等基础规范),请参考 Material 3 设计系统


📋 目录

  1. 组件类型选择
  2. TextField 样式规范
  3. 选择控件规范
  4. 表单布局规范
  5. 代码示例
  6. 常见错误

组件类型选择

决策树

输入类型?
├── 文本输入(单行)
│   └── ✅ TextField(outlined 样式)

├── 文本输入(多行)
│   └── ✅ TextField(maxLines: null, minLines: 3)

├── 单选(2-4个选项)
│   └── ✅ Radio + RadioListTile

├── 单选(> 5个选项)
│   └── ✅ DropdownButton / PopupMenuButton

├── 多选(少量选项)
│   └── ✅ Checkbox + CheckboxListTile

├── 多选(大量选项)
│   └── ✅ FilterChip / ChoiceChip

└── 开关(布尔值)
    └── ✅ Switch / SwitchListTile

组件类型对照表

组件Material 3 组件使用场景示例
文本输入TextField用户名、密码、书名、笔记"请输入书名"
多行输入TextField(maxLines: null)备注、描述、长文本"添加笔记..."
单选Radio + RadioListTileCEFR级别选择、排序方式A1/A2/B1...
下拉选择DropdownButton书籍分类、语言选择Fiction/Non-fiction
多选Checkbox + CheckboxListTile标签选择、权限设置启用发音、显示例句
芯片选择FilterChip / ChoiceChip标签、分类筛选CEFR级别芯片
开关Switch + SwitchListTile功能开关、设置项启用通知、深色模式

TextField 样式规范

1. 基础样式(Outlined)

统一使用 Outlined 样式(Material 3 推荐):

dart
// ✅ 正确:Outlined 样式
TextField(
  decoration: InputDecoration(
    labelText: '书名',
    hintText: '请输入书名',
    border: OutlineInputBorder(
      borderRadius: BorderRadius.circular(12),  // 圆角12px
    ),
  ),
)

// ❌ 错误:Filled 样式(旧设计)
TextField(
  decoration: InputDecoration(
    labelText: '书名',
    filled: true,  // 不推荐
  ),
)

2. 圆角规范

统一使用 12px 圆角(与按钮、容器保持一致):

dart
TextField(
  decoration: InputDecoration(
    border: OutlineInputBorder(
      borderRadius: BorderRadius.circular(12),  // ✅ 12px
    ),
  ),
)

3. 颜色规范

边框颜色(自动)

dart
// ✅ 正确:使用默认颜色(自动适配主题)
TextField(
  decoration: InputDecoration(
    border: OutlineInputBorder(),  // 未聚焦:outline
    // 聚焦时自动变为 primary
  ),
)

颜色映射

  • 未聚焦colorScheme.outline(灰色边框)
  • 聚焦colorScheme.primary(蓝色边框)
  • 错误colorScheme.error(红色边框)
  • 禁用colorScheme.onSurface.withOpacity(0.38)

文本颜色(自动)

dart
// ✅ 正确:自动使用主题颜色
TextField(
  // 输入文字:onSurface(自动)
  // label:onSurfaceVariant(自动)
  // hint:onSurfaceVariant.withOpacity(0.6)(自动)
)

4. 标签和提示文字

dart
// ✅ 推荐:同时使用 labelText 和 hintText
TextField(
  decoration: InputDecoration(
    labelText: '书名',        // 浮动标签
    hintText: '请输入书名',   // 占位提示
    border: OutlineInputBorder(
      borderRadius: BorderRadius.circular(12),
    ),
  ),
)

// ⚠️ 可选:仅使用 labelText(简洁)
TextField(
  decoration: InputDecoration(
    labelText: '用户名',
    border: OutlineInputBorder(
      borderRadius: BorderRadius.circular(12),
    ),
  ),
)

5. 辅助文字和错误提示

dart
// ✅ 正确:辅助文字 + 错误提示
TextField(
  decoration: InputDecoration(
    labelText: '密码',
    hintText: '至少8位字符',
    helperText: '密码长度至少8位',        // 辅助文字
    errorText: _errorText,                // 错误提示
    border: OutlineInputBorder(
      borderRadius: BorderRadius.circular(12),
    ),
  ),
)

// 动态错误提示
String? _validatePassword(String? value) {
  if (value == null || value.isEmpty) {
    return '请输入密码';
  }
  if (value.length < 8) {
    return '密码长度至少8位';
  }
  return null;
}

6. 前缀和后缀图标

dart
// ✅ 推荐:使用 prefixIcon 和 suffixIcon
TextField(
  decoration: InputDecoration(
    labelText: '搜索',
    hintText: '输入关键词...',
    prefixIcon: Icon(Icons.search),     // 前缀图标
    suffixIcon: IconButton(             // 后缀按钮
      icon: Icon(Icons.clear),
      onPressed: _clearText,
    ),
    border: OutlineInputBorder(
      borderRadius: BorderRadius.circular(12),
    ),
  ),
)

7. 多行输入

dart
// ✅ 多行文本输入
TextField(
  maxLines: null,    // 无限行
  minLines: 3,       // 最少3行
  decoration: InputDecoration(
    labelText: '笔记',
    hintText: '添加笔记...',
    alignLabelWithHint: true,  // 标签与输入对齐
    border: OutlineInputBorder(
      borderRadius: BorderRadius.circular(12),
    ),
  ),
)

选择控件规范

1. Radio(单选按钮)

基础用法

dart
// ✅ 推荐:使用 RadioListTile
RadioListTile<String>(
  title: Text('A1 - 初学者'),
  value: 'A1',
  groupValue: _selectedCEFR,
  onChanged: (value) => setState(() => _selectedCEFR = value),
  activeColor: Theme.of(context).colorScheme.primary,  // 选中颜色
)

自定义样式

dart
// 水平排列的 Radio 组
Row(
  children: [
    Radio<String>(
      value: 'A1',
      groupValue: _selectedCEFR,
      onChanged: (value) => setState(() => _selectedCEFR = value),
    ),
    Text('A1'),
    SizedBox(width: 16),
    Radio<String>(
      value: 'A2',
      groupValue: _selectedCEFR,
      onChanged: (value) => setState(() => _selectedCEFR = value),
    ),
    Text('A2'),
  ],
)

2. Checkbox(多选框)

基础用法

dart
// ✅ 推荐:使用 CheckboxListTile
CheckboxListTile(
  title: Text('启用发音'),
  subtitle: Text('点击单词时自动播放发音'),
  value: _enablePronunciation,
  onChanged: (value) => setState(() => _enablePronunciation = value ?? false),
  activeColor: Theme.of(context).colorScheme.primary,
)

三态复选框

dart
// 三态复选框(true/false/null)
CheckboxListTile(
  title: Text('全选'),
  tristate: true,  // 启用三态
  value: _allSelected,  // true/false/null
  onChanged: (value) => setState(() => _allSelected = value),
)

3. Switch(开关)

基础用法

dart
// ✅ 推荐:使用 SwitchListTile
SwitchListTile(
  title: Text('深色模式'),
  subtitle: Text('使用深色主题'),
  value: _darkMode,
  onChanged: (value) => setState(() => _darkMode = value),
  activeColor: Theme.of(context).colorScheme.primary,
)

自定义样式

dart
// 紧凑样式(标题 + 开关)
Row(
  mainAxisAlignment: MainAxisAlignment.spaceBetween,
  children: [
    Text('启用通知'),
    Switch(
      value: _notificationsEnabled,
      onChanged: (value) => setState(() => _notificationsEnabled = value),
      activeColor: Theme.of(context).colorScheme.primary,
    ),
  ],
)

4. DropdownButton(下拉选择)

dart
// ✅ 下拉选择框
DropdownButtonFormField<String>(
  decoration: InputDecoration(
    labelText: '书籍分类',
    border: OutlineInputBorder(
      borderRadius: BorderRadius.circular(12),
    ),
  ),
  value: _selectedCategory,
  items: [
    DropdownMenuItem(value: 'fiction', child: Text('Fiction')),
    DropdownMenuItem(value: 'non-fiction', child: Text('Non-fiction')),
    DropdownMenuItem(value: 'textbook', child: Text('Textbook')),
  ],
  onChanged: (value) => setState(() => _selectedCategory = value),
)

5. Chip(芯片选择)

FilterChip(多选)

dart
// ✅ FilterChip 用于多选标签
Wrap(
  spacing: 8,
  children: [
    FilterChip(
      label: Text('A1'),
      selected: _selectedLevels.contains('A1'),
      onSelected: (selected) {
        setState(() {
          if (selected) {
            _selectedLevels.add('A1');
          } else {
            _selectedLevels.remove('A1');
          }
        });
      },
    ),
    FilterChip(
      label: Text('A2'),
      selected: _selectedLevels.contains('A2'),
      onSelected: (selected) {
        setState(() {
          if (selected) {
            _selectedLevels.add('A2');
          } else {
            _selectedLevels.remove('A2');
          }
        });
      },
    ),
  ],
)

ChoiceChip(单选)

dart
// ✅ ChoiceChip 用于单选标签
Wrap(
  spacing: 8,
  children: [
    ChoiceChip(
      label: Text('全部'),
      selected: _selectedFilter == 'all',
      onSelected: (selected) {
        if (selected) setState(() => _selectedFilter = 'all');
      },
    ),
    ChoiceChip(
      label: Text('学习中'),
      selected: _selectedFilter == 'learning',
      onSelected: (selected) {
        if (selected) setState(() => _selectedFilter = 'learning');
      },
    ),
  ],
)

表单布局规范

1. 垂直间距

场景间距
表单项之间16px
分组标题与表单项8px
表单与提交按钮24px
dart
Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    TextField(...),
    SizedBox(height: 16),  // 表单项间距
    TextField(...),
    SizedBox(height: 16),
    TextField(...),
    SizedBox(height: 24),  // 表单与按钮间距
    FilledButton(...),
  ],
)

2. 表单宽度

全宽表单(移动端)

dart
Container(
  padding: EdgeInsets.all(16),
  child: Column(
    children: [
      TextField(
        decoration: InputDecoration(
          labelText: '书名',
          border: OutlineInputBorder(
            borderRadius: BorderRadius.circular(12),
          ),
        ),
      ),
      SizedBox(height: 16),
      SizedBox(
        width: double.infinity,  // 全宽按钮
        height: 48,
        child: FilledButton(
          onPressed: _submit,
          child: Text('提交'),
        ),
      ),
    ],
  ),
)

3. 分组布局

dart
// ✅ 使用 SectionLabel 分组
Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    SectionLabel('基本信息'),
    SizedBox(height: 8),
    TextField(
      decoration: InputDecoration(
        labelText: '书名',
        border: OutlineInputBorder(
          borderRadius: BorderRadius.circular(12),
        ),
      ),
    ),
    SizedBox(height: 16),
    TextField(
      decoration: InputDecoration(
        labelText: '作者',
        border: OutlineInputBorder(
          borderRadius: BorderRadius.circular(12),
        ),
      ),
    ),
    SizedBox(height: 24),

    SectionLabel('高级设置'),
    SizedBox(height: 8),
    SwitchListTile(...),
    CheckboxListTile(...),
  ],
)

代码示例

示例1:完整登录表单

dart
class LoginForm extends StatefulWidget {
  @override
  _LoginFormState createState() => _LoginFormState();
}

class _LoginFormState extends State<LoginForm> {
  final _formKey = GlobalKey<FormState>();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();
  bool _obscurePassword = true;

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            // 邮箱输入
            TextFormField(
              controller: _emailController,
              decoration: InputDecoration(
                labelText: '邮箱',
                hintText: 'example@email.com',
                prefixIcon: Icon(Icons.email),
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(12),
                ),
              ),
              keyboardType: TextInputType.emailAddress,
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return '请输入邮箱';
                }
                if (!value.contains('@')) {
                  return '请输入有效的邮箱地址';
                }
                return null;
              },
            ),
            SizedBox(height: 16),

            // 密码输入
            TextFormField(
              controller: _passwordController,
              obscureText: _obscurePassword,
              decoration: InputDecoration(
                labelText: '密码',
                hintText: '至少8位字符',
                prefixIcon: Icon(Icons.lock),
                suffixIcon: IconButton(
                  icon: Icon(
                    _obscurePassword ? Icons.visibility : Icons.visibility_off,
                  ),
                  onPressed: () {
                    setState(() => _obscurePassword = !_obscurePassword);
                  },
                ),
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(12),
                ),
              ),
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return '请输入密码';
                }
                if (value.length < 8) {
                  return '密码长度至少8位';
                }
                return null;
              },
            ),
            SizedBox(height: 24),

            // 提交按钮
            SizedBox(
              height: 48,
              child: FilledButton(
                onPressed: () {
                  if (_formKey.currentState!.validate()) {
                    _submit();
                  }
                },
                style: FilledButton.styleFrom(
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(12),
                  ),
                ),
                child: Text('登录'),
              ),
            ),
          ],
        ),
      ),
    );
  }

  void _submit() {
    // 提交逻辑
  }

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }
}

示例2:书籍创建表单

dart
class CreateBookForm extends StatefulWidget {
  @override
  _CreateBookFormState createState() => _CreateBookFormState();
}

class _CreateBookFormState extends State<CreateBookForm> {
  final _titleController = TextEditingController();
  final _authorController = TextEditingController();
  String _selectedCategory = 'fiction';

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // 基本信息
          SectionLabel('基本信息'),
          SizedBox(height: 8),

          TextField(
            controller: _titleController,
            decoration: InputDecoration(
              labelText: '书名',
              hintText: '请输入书名',
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(12),
              ),
            ),
          ),
          SizedBox(height: 16),

          TextField(
            controller: _authorController,
            decoration: InputDecoration(
              labelText: '作者',
              hintText: '请输入作者名',
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(12),
              ),
            ),
          ),
          SizedBox(height: 16),

          // 分类选择
          DropdownButtonFormField<String>(
            decoration: InputDecoration(
              labelText: '分类',
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(12),
              ),
            ),
            value: _selectedCategory,
            items: [
              DropdownMenuItem(value: 'fiction', child: Text('Fiction')),
              DropdownMenuItem(value: 'non-fiction', child: Text('Non-fiction')),
              DropdownMenuItem(value: 'textbook', child: Text('Textbook')),
            ],
            onChanged: (value) {
              setState(() => _selectedCategory = value!);
            },
          ),
          SizedBox(height: 24),

          // 保存按钮
          SizedBox(
            width: double.infinity,
            height: 48,
            child: FilledButton(
              onPressed: _save,
              style: FilledButton.styleFrom(
                shape: RoundedRectangleBorder(
                  borderRadius: BorderRadius.circular(12),
                ),
              ),
              child: Text('保存'),
            ),
          ),
        ],
      ),
    );
  }

  void _save() {
    // 保存逻辑
  }

  @override
  void dispose() {
    _titleController.dispose();
    _authorController.dispose();
    super.dispose();
  }
}

常见错误

❌ 错误1:使用 Filled 样式

dart
// ❌ 错误:Filled 样式(Material 2 旧样式)
TextField(
  decoration: InputDecoration(
    labelText: '书名',
    filled: true,
    fillColor: Colors.grey[200],
  ),
)

// ✅ 正确:Outlined 样式(Material 3 推荐)
TextField(
  decoration: InputDecoration(
    labelText: '书名',
    border: OutlineInputBorder(
      borderRadius: BorderRadius.circular(12),
    ),
  ),
)

❌ 错误2:圆角不统一

dart
// ❌ 错误:圆角不一致
TextField(
  decoration: InputDecoration(
    border: OutlineInputBorder(
      borderRadius: BorderRadius.circular(8),  // 8px
    ),
  ),
)

// ✅ 正确:统一使用12px圆角
TextField(
  decoration: InputDecoration(
    border: OutlineInputBorder(
      borderRadius: BorderRadius.circular(12),  // 12px
    ),
  ),
)

❌ 错误3:硬编码颜色

dart
// ❌ 错误:硬编码颜色
TextField(
  decoration: InputDecoration(
    labelStyle: TextStyle(color: Colors.blue),
    focusedBorder: OutlineInputBorder(
      borderSide: BorderSide(color: Colors.blue),
    ),
  ),
)

// ✅ 正确:使用主题颜色(自动)
TextField(
  decoration: InputDecoration(
    // labelStyle 和 focusedBorder 颜色自动使用 colorScheme.primary
    border: OutlineInputBorder(
      borderRadius: BorderRadius.circular(12),
    ),
  ),
)

❌ 错误4:表单项间距不一致

dart
// ❌ 错误:间距混乱
Column(
  children: [
    TextField(...),
    SizedBox(height: 10),  // 不一致
    TextField(...),
    SizedBox(height: 20),  // 不一致
    TextField(...),
  ],
)

// ✅ 正确:统一16px间距
Column(
  children: [
    TextField(...),
    SizedBox(height: 16),  // 统一
    TextField(...),
    SizedBox(height: 16),  // 统一
    TextField(...),
  ],
)

❌ 错误5:未使用 Form 验证

dart
// ❌ 错误:手动验证,代码冗长
TextField(
  controller: _controller,
  onChanged: (value) {
    setState(() {
      if (value.isEmpty) {
        _errorText = '请输入内容';
      } else {
        _errorText = null;
      }
    });
  },
  decoration: InputDecoration(
    errorText: _errorText,
  ),
)

// ✅ 正确:使用 Form + TextFormField
Form(
  key: _formKey,
  child: TextFormField(
    decoration: InputDecoration(
      border: OutlineInputBorder(
        borderRadius: BorderRadius.circular(12),
      ),
    ),
    validator: (value) {
      if (value == null || value.isEmpty) {
        return '请输入内容';
      }
      return null;
    },
  ),
)

快速参考

常用表单组件模板

dart
// TextField 模板
TextField(
  decoration: InputDecoration(
    labelText: '标签',
    hintText: '提示文字',
    border: OutlineInputBorder(
      borderRadius: BorderRadius.circular(12),
    ),
  ),
)

// TextFormField 模板(带验证)
TextFormField(
  decoration: InputDecoration(
    labelText: '标签',
    border: OutlineInputBorder(
      borderRadius: BorderRadius.circular(12),
    ),
  ),
  validator: (value) {
    if (value == null || value.isEmpty) {
      return '请输入内容';
    }
    return null;
  },
)

// SwitchListTile 模板
SwitchListTile(
  title: Text('标题'),
  subtitle: Text('描述'),
  value: _value,
  onChanged: (value) => setState(() => _value = value),
  activeColor: Theme.of(context).colorScheme.primary,
)

// CheckboxListTile 模板
CheckboxListTile(
  title: Text('标题'),
  value: _value,
  onChanged: (value) => setState(() => _value = value ?? false),
  activeColor: Theme.of(context).colorScheme.primary,
)

相关文档


维护者:Reading Vocab Helper Team 反馈:发现问题请提Issue