是什么
pinput 全称是 PIN input。它提供了一个漂亮且高度可定制的 分格输入框,常用于输入固定长度的验证码或 PIN 码。相比 TextField 自己拆格子,pinput 开箱即用。
主要特性
- 固定长度输入(默认 4 位,可自定义 4/6 位甚至更多)。
- 自定义样式:边框、颜色、形状、大小、间距等。
- 自动聚焦:打开页面时光标自动进入输入状态。
- 粘贴功能:支持从剪贴板粘贴验证码。
- 动画效果:输入时格子样式可以动态变化(例如高亮)。
- 验证器:类似
TextFormField,可以设置validator。 - 事件回调:
onChanged、onCompleted、onSubmitted等。
入门案例
安装
flutter pub add pinput
代码
import 'package:flutter/material.dart';
import 'package:pinput/pinput.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text("Pinput 示例")),
body: const Center(child: PinInputDemo()),
),
);
}
}
class PinInputDemo extends StatelessWidget {
const PinInputDemo({super.key});
@override
Widget build(BuildContext context) {
return Pinput(
length: 6, // 验证码长度
autofocus: true,
showCursor: true,
onCompleted: (pin) {
print("输入完成: $pin");
},
onChanged: (value) {
print("当前输入: $value");
},
);
}
}
- length:PIN/验证码长度(默认 4)
- autofocus:是否自动聚焦输入
- showCursor:是否显示光标
- onCompleted:输入完成时回调
- onChanged:输入变化时回调
日志如下:
I/flutter (15560): 当前输入: 1
I/flutter (15560): 当前输入: 12
I/flutter (15560): 当前输入: 123
I/flutter (15560): 当前输入: 1234
I/flutter (15560): 当前输入: 12345
I/flutter (15560): 当前输入: 123456
I/flutter (15560): 输入完成: 123456
Pinput 常用属性
基础配置
| 属性 | 类型 | 作用 |
|---|---|---|
length |
int |
PIN / 验证码的位数,默认 4。 |
controller |
TextEditingController? |
管理输入内容,支持读写、清空等。 |
focusNode |
FocusNode? |
控制焦点,适合和表单一起用。 |
autofocus |
bool |
页面加载后是否自动聚焦输入框。 |
keyboardType |
TextInputType |
键盘类型,常用 TextInputType.number。 |
obscureText |
String? |
是否隐藏输入(比如显示 * 代替密码)。 |
样式相关
| 属性 | 类型 | 作用 |
|---|---|---|
defaultPinTheme |
PinTheme |
默认格子的样式。 |
focusedPinTheme |
PinTheme |
聚焦时的样式。 |
submittedPinTheme |
PinTheme |
输入完成时的样式。 |
errorPinTheme |
PinTheme |
校验失败时的样式。 |
separatorBuilder |
WidgetBuilder? |
自定义每个格子之间的分隔符(默认间距)。 |
PinTheme 里可设置:宽高、字体、边框、圆角、背景色等。
功能行为
| 属性 | 类型 | 作用 |
|---|---|---|
showCursor |
bool |
是否显示光标。 |
enableSuggestions |
bool |
是否启用输入建议(默认关闭)。 |
enableIMEPersonalizedLearning |
bool |
是否允许输入法学习(安全场景可关闭)。 |
inputFormatters |
List<TextInputFormatter> |
限制输入内容(比如只允许数字)。 |
readOnly |
bool |
是否只读。 |
enabled |
bool |
是否可用。 |
表单 & 校验
| 属性 | 类型 | 作用 |
|---|---|---|
validator |
FormFieldValidator<String>? |
表单验证函数,返回错误信息或 null。 |
onChanged |
ValueChanged<String>? |
输入变化时触发。 |
onCompleted |
ValueChanged<String>? |
输入完成(达到 length 位数)时触发。 |
onSubmitted |
ValueChanged<String>? |
按键盘确认键时触发。 |
- 当用户**输入框已经填满(即长度达到
length属性设置的数字)**时, - 并且按下了键盘上的“确认/完成/回车”键(即提交输入)时,
- 就会触发
onSubmitted回调。
注意:
- 只是每输入一个数字不会触发
onSubmitted,那是onChanged的工作。- 只有完成输入后,按下键盘上的 done/enter/submit 才会触发
onSubmitted。- 如果输入已经填满所有位数(达到
length),但没有点击键盘上的「完成 / 回车 / 提交」键,那么onSubmitted是不会触发的。
动画 & 粘贴
| 属性 | 类型 | 作用 |
|---|---|---|
androidSmsAutofillMethod |
枚举 | 在 Android 自动填充验证码。 |
listenForMultipleSmsOnAndroid |
bool |
是否监听多条短信验证码。 |
hapticFeedbackType |
枚举 | 输入时的震动反馈。 |
toolbarEnabled |
bool |
是否允许复制/粘贴菜单。 |
pinputAutovalidateMode |
AutovalidateMode |
验证模式 |
enum PinputAutovalidateMode {
/// 不会自动触发校验
disabled,
/// 仅在 onCompleted / onSubmitted 被调用后触发校验
onSubmit,
}
disabled(默认)- 不会自动调用
validator - 只有你手动调用
formKey.currentState!.validate()时,才会校验
- 不会自动调用
onSubmit- 当 用户输入满位数(触发
onCompleted) 或者 调用onSubmitted时,自动执行validator - 适合验证码场景:
- 用户输入完 6 位 → 自动检查是否符合规则
- 用户点提交按钮 → 校验
- 当 用户输入满位数(触发
案例
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:pinput/pinput.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text("登录验证码 Demo")),
body: const LoginPage(),
),
);
}
}
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _phoneController = TextEditingController();
final _codeController = TextEditingController();
int _seconds = 0;
Timer? _timer;
/// 启动倒计时
void _startTimer() {
setState(() => _seconds = 5);
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (_seconds <= 1) {
timer.cancel();
}
setState(() => _seconds--);
});
}
@override
void dispose() {
_phoneController.dispose();
_codeController.dispose();
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final defaultPinTheme = PinTheme(
width: 50,
height: 50,
textStyle: const TextStyle(fontSize: 20, color: Colors.black),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8),
),
);
return Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 手机号输入
TextField(
controller: _phoneController,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
labelText: "手机号",
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
// 发送验证码按钮
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: _seconds == 0
? () {
if (_phoneController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("请输入手机号")),
);
return;
}
// TODO: 在这里调用接口发送验证码
_startTimer();
}
: null,
child: Text(_seconds == 0 ? "发送验证码" : "重新发送 ($_seconds)"),
),
),
],
),
const SizedBox(height: 24),
// 验证码输入框
Pinput(
length: 6, // 6位验证码
controller: _codeController, // 绑定控制器
defaultPinTheme: defaultPinTheme, // 默认样式
showCursor: true, // 显示光标
onCompleted: (pin) { // 输入完成回调
print("验证码输入完成: $pin");
},
),
const SizedBox(height: 24),
// 登录按钮
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
final phone = _phoneController.text.trim();
// _codeController.text" 获取验证码
final code = _codeController.text.trim();
if (phone.isEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text("请输入手机号")));
return;
}
if (code.length != 6) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text("请输入 6 位验证码")));
return;
}
// TODO: 在这里调用登录接口
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("手机号: $phone, 验证码: $code")),
);
},
child: const Text("登录"),
),
),
],
),
);
}
}
自定义样式
案例
import 'package:flutter/material.dart';
import 'package:pinput/pinput.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(home: CustomPinputPage());
}
}
class CustomPinputPage extends StatelessWidget {
const CustomPinputPage({super.key});
@override
Widget build(BuildContext context) {
// 默认样式
final defaultPinTheme = PinTheme(
width: 56,
height: 56,
textStyle: const TextStyle(
fontSize: 20,
color: Colors.black,
fontWeight: FontWeight.w600,
),
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade400),
),
);
// 聚焦状态样式
final focusedPinTheme = defaultPinTheme.copyWith(
decoration: defaultPinTheme.decoration!.copyWith(
border: Border.all(color: Colors.blue, width: 2),
color: Colors.white,
),
);
// 已输入的格子样式
final submittedPinTheme = defaultPinTheme.copyWith(
decoration: defaultPinTheme.decoration!.copyWith(
color: Colors.green.shade100,
),
);
return Scaffold(
appBar: AppBar(title: const Text("Pinput 自定义样式 Demo")),
body: Center(
child: Pinput(
length: 6,
defaultPinTheme: defaultPinTheme,
focusedPinTheme: focusedPinTheme,
submittedPinTheme: submittedPinTheme,
showCursor: true,
onCompleted: (pin) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text("验证码输入完成: $pin")));
},
),
),
);
}
}
三个属性
Pinput 的一个强大之处就是 可以完全自定义输入框的样式。核心是三个属性:
defaultPinTheme→ 普通状态下的样式focusedPinTheme→ 输入框被选中、正在输入时的样式submittedPinTheme→ 输入完成(已有内容)的样式
PinTheme
PinTheme 是一个样式配置类,控制单个 PIN 输入格子的宽高、边框、背景、文字样式等。主要属性:
width/height→ 每个格子的宽高textStyle→ 输入内容的文字样式decoration→ 背景和边框装饰(BoxDecoration)
copyWith 的作用
PinTheme 是个不可变对象(immutable),如果你想改里面的某个属性(比如边框颜色),不能直接改原来的,要用 copyWith 复制出一个新对象。比如:
final focusedPinTheme = defaultPinTheme.copyWith(
width: 60,
height: 60,
decoration: defaultPinTheme.decoration!.copyWith(
border: Border.all(color: Colors.blue, width: 2),
color: Colors.white,
),
);
- 先复制一份
defaultPinTheme - 然后把
width和height改成新的值 - 再把
decoration的边框颜色换成蓝色,背景改成白色 - 这样写的好处是可以避免许多重复的代码。
注意:基于原有对象,生成一个新的对象(不可变),并且只修改你指定的属性,其他属性保持不变。
常见支持 copyWith 的类
在 Flutter 里,大部分和样式/主题相关的类都支持 copyWith 方法,这是 Flutter 的一种设计习惯。
| 类 | 说明 | 示例 |
|---|---|---|
TextStyle |
字体样式(大小、颜色、加粗等) | style.copyWith(color: Colors.red) |
ThemeData |
全局主题(颜色、字体、按钮样式等) | Theme.of(context).copyWith(primaryColor: Colors.blue) |
TextTheme |
一组文本样式 | theme.textTheme.copyWith(bodyLarge: newStyle) |
BoxDecoration |
背景装饰(颜色、圆角、边框) | decoration.copyWith(color: Colors.green) |
InputDecoration |
输入框样式(hintText、边框等) | decoration.copyWith(hintText: "新提示") |
ButtonStyle |
按钮样式 | ElevatedButton.styleFrom(...).copyWith(...) |
PinTheme(第三方) |
Pinput 的格子样式 |
defaultPinTheme.copyWith(width: 60) |