Flutter 组件抽取:验证码输入功能(CodeInputContainer)

简介

验证码输入框,可选需要输入的验证码个数,输入达指定个数后自动回调

效果

在这里插入图片描述

范例

class _TestPageState extends State<TestPage> {
  
  void initState() {
    super.initState();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('CodeInputContainer')),
      body: Container(
        width: double.maxFinite,
        padding: const EdgeInsets.all(20),
        child: Column(
          children: [
            CodeInputContainer(
              count: 4,
              phone: '18888888888',
              onRestart: () async {
                Function() cancel = MsgUtil.loading();
                await Future.delayed(const Duration(seconds: 1));
                cancel.call();
                MsgUtil.toast('验证码发送失败');
                return false;
              },
              onResult: (code) {
                MsgUtil.toast('验证码($code)正确');
              },
            ),
            const SizedBox(height: 30),
            CodeInputContainer(
              count: 6,
              phone: '18888888888',
              onRestart: () async {
                Function() cancel = MsgUtil.loading();
                await Future.delayed(const Duration(seconds: 1));
                cancel.call();
                MsgUtil.toast('验证码发送成功');
                return true;
              },
              onResult: (code) {
                MsgUtil.toast('验证码($code)错误');
              },
            ),
          ],
        ),
      ),
    );
  }
}

说明

自动维护页面切换、APP 退到后台、熄屏等情况下,重新进入页面时的倒计时对齐问题

代码

import 'dart:async';
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

/// 验证码输入框
class CodeInputContainer extends StatefulWidget {
  final int count;
  final String phone;
  final Function(String code) onResult;
  /// 重新发起获取验证码
  /// [return] 是否发起"获取验证码"操作成功
  final Future<bool> Function() onRestart;

  const CodeInputContainer({
    super.key,
    required this.count,
    required this.phone,
    required this.onResult,
    required this.onRestart,
  });

  
  State createState() => _CodeInputContainerState();
}

class _CodeInputContainerState extends State<CodeInputContainer> with WidgetsBindingObserver {
  late final ValueNotifier<String> code = ValueNotifier('');
  late FocusNode inputFocus = FocusNode();

  bool restart = false;
  Timer? timer;
  final int seconds = 60;
  late final ValueNotifier<int> timeCount = ValueNotifier(seconds);
  DateTime? pausedTime;

  void startTimer() {
    timer = Timer.periodic(const Duration(seconds: 1), (timer) {
      timeCount.value--;
      if (timeCount.value <= 0) {
        timer.cancel();
        timeCount.value = seconds;
        code.value = '';
        setState(() {
          restart = true;
        });
      }
    });
  }

  String handlePhone(String phone) {
    if (phone.length == 11) {
      return '${phone.substring(0, 3)} ${phone.substring(3, 7)} ${phone.substring(7, 11)}';
    } else {
      return phone;
    }
  }

  
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    WidgetsBinding.instance.addPostFrameCallback((_) {
      inputFocus.requestFocus();
      startTimer();
    });
  }

  
  void didChangeAppLifecycleState(AppLifecycleState state) {
    /// 适配页面切换、熄屏时倒计时混乱问题
    if (state == AppLifecycleState.resumed) {
      if (pausedTime != null) {
        int seconds = DateTime.now().difference(pausedTime!).inSeconds;
        pausedTime = null;
        timeCount.value = max(0, timeCount.value - seconds);
        startTimer();
      }
    } else if (state == AppLifecycleState.paused) {
      timer?.cancel();
      pausedTime = DateTime.now();
    }
  }

  
  void dispose() {
    super.dispose();
    WidgetsBinding.instance.removeObserver(this);
    timer?.cancel();
  }

  
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      mainAxisSize: MainAxisSize.min,
      children: [
        const Text(
          '输入验证码',
          style: TextStyle(
            color: Colors.black,
            fontSize: 24,
            fontWeight: FontWeight.w600,
          ),
        ),
        const SizedBox(height: 8),
        Text(
          '验证码已发送至 ${handlePhone(widget.phone)}',
          style: const TextStyle(color: Colors.grey, fontSize: 13),
        ),
        const SizedBox(height: 20),
        buildCodeInput(),
        GestureDetector(
          onTap: () {
            /// 点击时弹出输入键盘
            SystemChannels.textInput.invokeMethod('TextInput.show');
            inputFocus.requestFocus();
          },
          child: buildCodeView(),
        ),
        const SizedBox(height: 15),
        if (!restart)
          ValueListenableBuilder<int>(
            valueListenable: timeCount,
            builder: (context, value, child) {
              return Text(
                '$value 秒后可重新获取',
                style: const TextStyle(color: Colors.grey, fontSize: 13),
              );
            },
          ),
        if (restart)
          GestureDetector(
            onTap: () async {
              if (await widget.onRestart.call()) {
                setState(() {
                  restart = false;
                });
                startTimer();
              }
            },
            child: const Text(
              '重新发送',
              style: TextStyle(color: Colors.red, fontSize: 13),
            ),
          ),
      ],
    );
  }

  Widget buildCodeInput() {
    return SizedBox(
      height: 0,
      width: 0,
      child: TextField(
        controller: TextEditingController(text: code.value),
        focusNode: inputFocus,
        maxLength: widget.count,
        keyboardType: TextInputType.number,
        // 禁止长按复制
        enableInteractiveSelection: false,
        decoration: const InputDecoration(
          counterText: '',
          border: OutlineInputBorder(borderSide: BorderSide.none),
        ),
        inputFormatters: [
          // 只允许输入数字
          FilteringTextInputFormatter(RegExp("^[0-9]*\$"), allow: true)
        ],
        onChanged: (v) async {
          code.value = v;
          if (v.length == widget.count) widget.onResult.call(v);
        },
      ),
    );
  }

  Widget buildCodeView() {
    return ValueListenableBuilder<String>(
      valueListenable: code,
      builder: (context, value, child) {
        return GridView.count(
          padding: EdgeInsets.zero,
          crossAxisCount: widget.count,
          scrollDirection: Axis.vertical,
          physics: const NeverScrollableScrollPhysics(),
          shrinkWrap: true,
          crossAxisSpacing: 8,
          childAspectRatio: 0.95,
          children: List.generate(widget.count, (int i) => i).map((index) {
            return Container(
              alignment: Alignment.center,
              decoration: BoxDecoration(
                color: Colors.grey.shade200,
                border: ((index < widget.count && index == value.length) ||
                        (inputFocus.hasFocus && value.isEmpty && index == 0))
                    ? Border.all(width: 1, color: Colors.red)
                    : null,
                borderRadius: BorderRadius.circular(8),
              ),
              child: (value.length > index)
                  ? Text(
                      value[index],
                      style: const TextStyle(
                        color: Colors.black,
                        fontSize: 24,
                        fontWeight: FontWeight.w500,
                      ),
                    )
                  : null,
            );
          }).toList(),
        );
      },
    );
  }
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值