在这里插入图片描述

Flutter for OpenHarmony 实战之基础组件:第八篇 TextField 输入框与表单

前言

从登录注册到搜索商品,输入框无处不在。

相比于展示类组件,TextField 明显复杂得多。你需要管理光标、处理软键盘弹起、实时监听内容变化、校验格式合法性…

本文你将学到

  • 控制器 (Controller) 的生命周期管理
  • 实现“点击眼睛显示/隐藏密码”
  • Form 表单的一键校验技巧
  • 解决“键盘遮挡输入框”的经典方案
  • 实战:封装现代化的登录输入组件

一、TextField 基础用法

在这里插入图片描述

1.1 最简单的输入框

TextField(
  decoration: InputDecoration(
    labelText: '用户名',
    hintText: '请输入手机号/邮箱',
    prefixIcon: Icon(Icons.person),
    border: OutlineInputBorder(), // 边框样式
  ),
  onChanged: (text) {
    print('当前输入: $text');
  },
)

1.2 获取与控制输入 (TextEditingController)

想要获取输入框的内容,或者设置默认值,必须使用 TextEditingController

class LoginDemo extends StatefulWidget {
  const LoginDemo({super.key});

  
  State<LoginDemo> createState() => _LoginDemoState();
}

class _LoginDemoState extends State<LoginDemo> {
  // 1. 创建控制器
  final TextEditingController _usernameController = TextEditingController();

  
  void initState() {
    super.initState();
    // 2. 设置默认值 (可选)
    _usernameController.text = "admin";
  }

  
  void dispose() {
    // 3. ⭐️ 务必销毁,防止内存泄漏
    _usernameController.dispose();
    super.dispose();
  }

  void _login() {
    // 4. 获取内容
    final username = _usernameController.text;
    print('登录用户名: $username');
  }

  
  Widget build(BuildContext context) {
    return TextField(
      controller: _usernameController, // 绑定
    );
  }
}

1.3 密码框与显示/隐藏

通过 obscureText 属性控制是否隐藏内容(变为星号或圆点)。

bool _isObscure = true;

TextField(
  obscureText: _isObscure, // 控制密码显示
  decoration: InputDecoration(
    labelText: '密码',
    // 后缀图标:眼睛按钮
    suffixIcon: IconButton(
      icon: Icon(_isObscure ? Icons.visibility : Icons.visibility_off),
      onPressed: () {
        setState(() {
          _isObscure = !_isObscure;
        });
      },
    ),
  ),
)

二、Form 表单与校验

如果一个页面有多个输入框(如注册页),一个个去判断太麻烦了。Flutter 提供了 FormTextFormField 来批量管理。

在这里插入图片描述

2.1 核心组件

  • Form: 容器,通过 GlobalKey 管理状态。
  • TextFormField: 也就是 TextField 的加强版,增加了 validatoronSaved 回调。

2.2 实战:带校验的注册表单

class RegisterForm extends StatefulWidget {
  const RegisterForm({super.key});

  
  State<RegisterForm> createState() => _RegisterFormState();
}

class _RegisterFormState extends State<RegisterForm> {
  // 1. 创建 GlobalKey
  final _formKey = GlobalKey<FormState>();
  
  String _email = '';
  String _password = '';

  void _submit() {
    // 2. 调用 validate() 触发所有子项校验
    if (_formKey.currentState!.validate()) {
      // 3. 校验通过,保存数据
      _formKey.currentState!.save();
      print('注册信息: $_email, $_password');
      // TODO: 发起注册请求
    }
  }

  
  Widget build(BuildContext context) {
    return Form(
      key: _formKey, // 绑定 Key
      child: Column(
        children: [
          TextFormField(
            decoration: const InputDecoration(labelText: '邮箱'),
            // 校验逻辑
            validator: (value) {
              if (value == null || value.isEmpty) {
                return '邮箱不能为空';
              }
              if (!value.contains('@')) {
                return '请输入有效的邮箱地址';
              }
              return null; // 返回 null 表示通过
            },
            onSaved: (val) => _email = val!,
          ),
          
          const SizedBox(height: 16),
          
          TextFormField(
            decoration: const InputDecoration(labelText: '密码'),
            validator: (value) {
              if (value == null || value.length < 6) {
                return '密码长度不能少于6位';
              }
              return null;
            },
            onSaved: (val) => _password = val!,
            obscureText: true,
          ),
          
          const SizedBox(height: 32),
          
          ElevatedButton(
            onPressed: _submit,
            child: const Text('立即注册'),
          ),
        ],
      ),
    );
  }
}

三、常见问题解决方案

3.1 键盘遮挡输入框

在这里插入图片描述

当在底部输入时,软键盘弹起会盖住输入框。

方案 A: Scaffold(resizeToAvoidBottomInset: true) (默认开启)。这会让 Body 自动变矮。
方案 B: 将页面包裹在 SingleChildScrollViewListView 中。当键盘弹起,页面可滚动。

import 'package:flutter/material.dart';

class KeyboardOcclusionPage extends StatefulWidget {
  const KeyboardOcclusionPage({super.key});

  
  State<KeyboardOcclusionPage> createState() => _KeyboardOcclusionPageState();
}

class _KeyboardOcclusionPageState extends State<KeyboardOcclusionPage> {
  bool _useFix = false; // 是否开启修复方案

  
  Widget build(BuildContext context) {
    return Scaffold(
      // 核心差异点 1: 反例时关闭自动避让,模拟遮挡效果
      // resizeToAvoidBottomInset 默认为 true。
      // 设为 false 时,键盘弹起不仅不会把页面顶上去,还会直接盖住底部内容。
      resizeToAvoidBottomInset: _useFix,
      appBar: AppBar(
        title: const Text('场景1:键盘遮挡问题'),
        backgroundColor: _useFix ? Colors.green[100] : Colors.red[100],
        actions: [
          Row(
            children: [
              const Text('启用修复'),
              Switch(
                value: _useFix,
                onChanged: (v) {
                  FocusScope.of(context).unfocus(); // 切换时先收起键盘
                  setState(() => _useFix = v);
                },
              ),
            ],
          ),
          const SizedBox(width: 12),
        ],
      ),
      // 核心差异点 2: 正例使用 SingleChildScrollView
      body: _useFix ? _buildScrollView() : _buildFixedColumn(),
    );
  }

  // 反例:固定布局,无滚动,且 resizeToAvoidBottomInset = false
  Widget _buildFixedColumn() {
    return Container(
      width: double.infinity,
      color: Colors.red[50],
      padding: const EdgeInsets.all(16),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.end, // 把内容顶到底部,确保键盘能遮挡它
        children: [
          const Spacer(),
          const Icon(Icons.warning_amber_rounded,
              size: 80, color: Colors.orange),
          const SizedBox(height: 16),
          const Text(
            '【反面教材】\n\n1. resizeToAvoidBottomInset = false\n2. 没有 ScrollView\n\n👉 点击下方输入框,键盘将弹起并直接无情地【遮挡】住它。\n你看不到正在输入什么。',
            textAlign: TextAlign.center,
            style: TextStyle(
                fontSize: 16, fontWeight: FontWeight.bold, color: Colors.red),
          ),
          const SizedBox(height: 50),
          const TextField(
            decoration: InputDecoration(
              filled: true,
              fillColor: Colors.white,
              labelText: '我是底部的输入框 (一点就废)',
              border: OutlineInputBorder(),
            ),
          ),
          const SizedBox(height: 30), // 留点底部边距
        ],
      ),
    );
  }

  // 正例:滚动视图 + 自动避让
  Widget _buildScrollView() {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16),
      child: Column(
        children: [
          // 模拟很多内容撑开高度
          ...List.generate(
              3,
              (i) => Container(
                    height: 150,
                    margin: const EdgeInsets.only(bottom: 16),
                    color: Colors.blue[100 * (i % 3 + 1)],
                    alignment: Alignment.center,
                    child: Text('占位内容 $i'),
                  )),
          const SizedBox(height: 20),
          const Text(
            '【正面教材】\n\n1. resizeToAvoidBottomInset = true (默认)\n2. 包裹 SingleChildScrollView\n\n👉 点击下方输入框,页面会自动顶起,\n且你可以自由滑动查看所有内容。',
            textAlign: TextAlign.center,
            style: TextStyle(
                color: Colors.green, fontWeight: FontWeight.bold, fontSize: 16),
          ),
          const SizedBox(height: 20),
          const TextField(
            decoration: InputDecoration(
              filled: true,
              fillColor: Colors.white,
              labelText: '安全输入框 (自动避让)',
              border: OutlineInputBorder(),
            ),
          ),
          const SizedBox(height: 300), // 故意加长底部,测试滚动
        ],
      ),
    );
  }
}

3.2 点击空白处收起键盘在这里插入图片描述

这在 iOS 和鸿蒙上是符合用户直觉的体验。

import 'package:flutter/material.dart';

class KeyboardDismissPage extends StatefulWidget {
  const KeyboardDismissPage({super.key});

  
  State<KeyboardDismissPage> createState() => _KeyboardDismissPageState();
}

class _KeyboardDismissPageState extends State<KeyboardDismissPage> {
  bool _enableDismiss = false;

  
  Widget build(BuildContext context) {
    // 构造内部页面内容
    Widget bodyContent = Scaffold(
      appBar: AppBar(
        title: const Text('场景2:点击收起键盘'),
        backgroundColor: _enableDismiss ? Colors.green[100] : Colors.red[100],
      ),
      body: Padding(
        padding: const EdgeInsets.all(24.0),
        child: Column(
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const Text('启用“点击空白收起”'),
                const SizedBox(width: 8),
                Switch(
                  value: _enableDismiss,
                  onChanged: (v) => setState(() => _enableDismiss = v),
                ),
              ],
            ),
            const Divider(),
            const SizedBox(height: 40),
            Text(
              _enableDismiss
                  ? '【正面教材】\n\n已包裹 GestureDetector。\n👉 现在点击下方输入框唤起键盘,\n然后点击任意【空白区域】,键盘会自动收起。'
                  : '【反面教材】\n\n未处理点击事件。\n👉 点击下方输入框唤起键盘后...\n尝试狂点空白处,键盘会纹丝不动。\n(只能无奈地去点键盘上小小的完成键)',
              textAlign: TextAlign.center,
              style: TextStyle(
                fontSize: 16,
                fontWeight: FontWeight.bold,
                color: _enableDismiss ? Colors.green : Colors.red,
              ),
            ),
            const SizedBox(height: 40),
            const TextField(
              autofocus: true,
              decoration: InputDecoration(
                hintText: '点我尝试唤起键盘...',
                border: OutlineInputBorder(),
                prefixIcon: Icon(Icons.keyboard),
              ),
            ),
          ],
        ),
      ),
    );

    // 核心差异:正例包裹 GestureDetector
    if (_enableDismiss) {
      return GestureDetector(
        behavior:
            HitTestBehavior.translucent, // 💡 只有设为 translucent,才能捕捉透明区域的点击
        onTap: () {
          debugPrint('触发全局点击,收起键盘');
          // 收起键盘的核心代码
          FocusScope.of(context).unfocus();
        },
        child: bodyContent,
      );
    } else {
      return bodyContent;
    }
  }
}

四、鸿蒙实战:封装通用输入组件

在这里插入图片描述

为了让 APP 风格统一,我们封装一个样式的 InputWidget

import 'package:flutter/material.dart';

class CustomInputPage extends StatelessWidget {
  const CustomInputPage({super.key});

  
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => FocusScope.of(context).unfocus(),
      child: Scaffold(
        backgroundColor: Colors.grey[100], // 浅灰背景突出卡片
        appBar: AppBar(title: const Text('实战:封装通用输入组件')),
        body: SingleChildScrollView(
          padding: const EdgeInsets.all(24),
          child: Column(
            children: [
              const Text('以下使用了自定义的 ModernTextField 组件',
                  style: TextStyle(color: Colors.grey)),
              const SizedBox(height: 32),
              const ModernTextField(
                hint: '请输入账号',
                icon: Icons.person,
              ),
              const SizedBox(height: 20),
              const ModernTextField(
                hint: '请输入密码',
                icon: Icons.lock,
                isPassword: true,
              ),
              const SizedBox(height: 48),
              SizedBox(
                width: double.infinity,
                height: 50,
                child: DecoratedBox(
                  decoration: BoxDecoration(
                    gradient: const LinearGradient(
                        colors: [Colors.blue, Colors.purple]),
                    borderRadius: BorderRadius.circular(12),
                    boxShadow: [
                      BoxShadow(
                        color: Colors.blue.withOpacity(0.3),
                        offset: const Offset(0, 4),
                        blurRadius: 10,
                      )
                    ],
                  ),
                  child: MaterialButton(
                    onPressed: () {},
                    child: const Text('登录',
                        style: TextStyle(
                            color: Colors.white,
                            fontSize: 16,
                            fontWeight: FontWeight.bold)),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

/// 封装的现代化输入框组件
class ModernTextField extends StatelessWidget {
  final TextEditingController? controller;
  final String hint;
  final IconData? icon;
  final bool isPassword;

  const ModernTextField({
    super.key,
    this.controller,
    required this.hint,
    this.icon,
    this.isPassword = false,
  });

  
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12),
        // 增加投影,提升立体感
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.05),
            offset: const Offset(0, 4),
            blurRadius: 10,
          ),
        ],
      ),
      child: TextField(
        controller: controller,
        obscureText: isPassword,
        decoration: InputDecoration(
          hintText: hint,
          hintStyle: TextStyle(color: Colors.grey[400]),
          prefixIcon:
              icon != null ? Icon(icon, color: Colors.blueAccent) : null,
          border: InputBorder.none, // 去掉默认下划线
          contentPadding:
              const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
        ),
      ),
    );
  }
}

五、阶段总结

至此,《Flutter for OpenHarmony 实战之基础组件》 系列的前 8 篇基础中的基础已经讲解完毕!但这仅仅是开始。

我们已经掌握了:

  1. Container: 布局的基石。
  2. Row/Column: 线性排列的艺术。
  3. Stack: 这就是层叠(Z轴)。
  4. Text: 信息的传递者。
  5. Image: 颜值即正义。
  6. ListView: 动态的长河。
  7. Button: 交互的起点。
  8. TextField: 数据的入口。

掌握了这 8 大组件,你已经能画出大部分 UI 界面了。但要构建一个完整的、有灵魂的 App,我们还需要搭建页面的骨架(Scaffold)、与用户进行对话(Dialog)、以及更复杂的导航结构(Tabs)。

下一篇预告

接下来的第九篇,我们将进入**“中级组件篇”**,解决页面的结构问题:

《Flutter for OpenHarmony 实战之基础组件:第九篇 Scaffold 与 AppBar 页面骨架》

我们将学习如何像搭积木一样,快速构建出具备导航栏、侧边栏和悬浮按钮的标准 Material 页面。


🚀 感谢你的陪伴! 愿你在鸿蒙跨平台开发的道路上越走越远,打造出令人惊艳的应用。

🌐 欢迎加入开源鸿蒙跨平台社区开源鸿蒙跨平台开发者社区

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐