點燈坊

失くすものさえない今が強くなるチャンスよ

TextField 實現驗證碼輸入2

Sam Xiao's Avatar 2024-12-02

TextField 可實現驗證碼輸入,包括顯示 數字鍵盤,與一次只能輸入 一個數字。本範例使用 FocusTraversalGroup

Version

Flutter 3.24.5

Flutter

code01

  • Android 與 iOS 都成功使用 TextField 實現輸入驗證碼

TextField

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

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

  
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  final List<String> _codes = List.generate(
    6,
    (_) => '',
  );
  final FocusNode _firstFocusNode = FocusNode();

  
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback(
      (_) => _firstFocusNode.requestFocus(),
    );
  }

  
  Widget build(BuildContext context) {
    var appBar = AppBar(
      title: const Text(
        'Verification Code',
      ),
    );

    var codes = FocusTraversalGroup(
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: List.generate(
          6,
          (index) {
            return Container(
              margin: const EdgeInsets.symmetric(
                horizontal: 4,
              ),
              width: 40,
              child: TextField(
                keyboardType: TextInputType.number,
                maxLength: 1,
                textAlign: TextAlign.center,
                focusNode: index == 0 ? _firstFocusNode : null,
                inputFormatters: [
                  FilteringTextInputFormatter.digitsOnly,
                ],
                decoration: const InputDecoration(
                  counterText: '',
                  border: OutlineInputBorder(),
                ),
                onChanged: (value) {
                  if (value.isEmpty) {
                    return;
                  }

                  _codes[index] = value;

                  if (index < 5) {
                    FocusScope.of(context).nextFocus();
                  } else {
                    FocusScope.of(context).unfocus();
                  }
                },
                onTapOutside: (event) {
                  FocusScope.of(context).unfocus();
                },
              ),
            );
          },
        ),
      ),
    );

    var body = Center(
      child: codes,
    );

    return Scaffold(
      appBar: appBar,
      body: body,
    );
  }

  
  void dispose() {
    _firstFocusNode.dispose();
    super.dispose();
  }
}

Line 12

final List<String> _codes = List.generate(
  6,
  (_) => '',
);
  • _codes:以 array 儲存所輸入的驗證碼,初始值皆為 empty string
  • (_) => ''List.generate() 的 callback 的型別原本為 (int index) => {},因為 index 沒用到,所以必須以 _ 代替

Line 16

final FocusNode _firstFocusNode = FocusNode();
  • 定義第一個 TextField 所需的 FocusNode

Line 18


void initState() {
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback(
    (_) => _firstFocusNode.requestFocus(),
  );
}
  • WidgetsBinding.instance.addPostFrameCallback():在 Widget Tree render 完時觸發,對第一個 TextField 進行 focus

Line 34

var codes = FocusTraversalGroup(
  child: Row(
    mainAxisSize: MainAxisSize.min,
    children: List.generate(
      6,
      (index) {
        return Container(
          margin: const EdgeInsets.symmetric(
            horizontal: 4,
          ),
          width: 40,
          child: TextField(
            keyboardType: TextInputType.number,
            maxLength: 1,
            textAlign: TextAlign.center,
            focusNode: index == 0 ? _firstFocusNode : null,
            inputFormatters: [
              FilteringTextInputFormatter.digitsOnly,
            ],
            decoration: const InputDecoration(
              counterText: '',
              border: OutlineInputBorder(),
            ),
            onChanged: (value) {
              if (value.isEmpty) {
                return;
              }

              _codes[index] = value;

              if (index < 5) {
                FocusScope.of(context).nextFocus();
              } else {
                FocusScope.of(context).unfocus();
              }
            },
            onTapOutside: (event) {
              FocusScope.of(context).unfocus();
            },
          ),
        );
      },
    ),
  ),
);
  • FocusTraversalGroup:將驗證碼輸入的多個 TextField 定為一個 group,如此 focus 將可依序跳轉
  • keyboardType: TextInputType.number:自動顯示 數字鍵盤
  • maxLength: 1:一次只能輸入一個字,適合輸入驗證碼
  • textAlign: TextAlign.center:輸入的驗證碼水平置中
  • focusNode: index == 0 ? _firstFocusNode : null:第一個 TextField 將取得 focus
  • inputFormatters:設定 TextField 能輸入的限制
    • FilteringTextInputFormatter.digitsOnly:只能輸入數字
  • decoration
    • counterText: '':當搭配 maxLength 時,counterText 會顯示 當前字數 / 最大字數 的統計,但用於驗證碼輸入時則不需要,因此設定為 empty string
    • border: OutlineInputBorder():驗證輸入為圓框

Line 57

onChanged: (value) {
  if (value.isEmpty) {
    return;
  }

  _codes[index] = value;

  if (index < 5) {
    FocusScope.of(context).nextFocus();
  } else {
    FocusScope.of(context).unfocus();
  }
},
  • 當輸入時,會將值寫入 _codes array
  • 當不是 最後一個 驗證碼時,會自動跳到下一個驗證碼
  • 當輸入完最一個驗證碼時,自動隱藏 數字鍵盤

會在 輸入後 才取得 index,而不是 輸入前 取得 index

Line 70

onTapOutside: (event) {
  FocusScope.of(context).unfocus();
},
  • 離開 TextField 自動隱藏 數字鍵盤

Conclusion

  • FocusTraversalGroup 讓驗證碼輸入跳轉 focus 更加容易,只需簡單 nextFocus() 即可
  • initState() 內配合WidgetsBinding.instance.addPostFrameCallback() 可視為 Vue 的 mounted(),專門用來對 Widget Tree render 完要執行的初始化操作,如 設置焦點請求數據啟動畫面動畫