📱

【Flutter】電話番号入力用のテキストフィールドの作り方

に公開

要約

電話番号入力用のテキストフィールドは、「許可する文字種類の制限」「表示用フォーマット」「カーソル位置の維持」の3点を実装する必要があります。ライブラリを探せば実現できるものもすでに存在するかもしれません。
この記事では、TextInputFormatterを継承した、PhoneInputFormatterを使い、日本の携帯番号(最大11桁)を「XXX XXXX XXXX」風にスペース自動挿入する実装を紹介します。

対象

  • Flutter で電話番号入力フィールドを自前で作り込みたい方
  • TextInputFormatterを理解して、カーソル位置やスペース挿入を安定させたい方

実装上の課題

  • キーボードタイプを TextInputType.phone にしても文字種の完全制御はできない。コピペされたことを考慮する必要がある。
  • スペースの自動挿入を実現させたい時、カーソル位置の制御をする必要がある。

解決アプローチ

実装上の課題を解決するため、以下参考コードと合わせて解説していきます。

TextInputFormatterを自作し、入力可能な文字とカーソル位置を制御

入力された文字列を確認し、入力可能文字の制御とカーソル位置の制御をする自作のTextInputFormatter(PhoneInputFormatter)をinputFormattersに指定します。

            TextField(
              inputFormatters: [PhoneInputFormatter()],
              decoration: InputDecoration(
                hintText: '入力補完(数字のみで「*** **** ****」のようにスペース入力補完)',
              ),
              keyboardType: TextInputType.phone,
            ),

PhoneInputFormatterの実装は以下です。
コメントにて、実装の説明を入れています。

/// 電話番号フォーマットのためのカスタムFormatter
class PhoneInputFormatter extends TextInputFormatter {
  static const _maxDigits = 11;
  static final _nonDigit = RegExp(r'\D');
  static final _digit = RegExp(r'\d');

  
  TextEditingValue formatEditUpdate(
    TextEditingValue oldValue,
    TextEditingValue newValue,
  ) {
    if (newValue.isComposingRangeValid) {
      return newValue;
    }

    // 表示用文字列の生成
    // 入力後文字列を数字のみにする(「070 1234 5678」→「07012345678」)
    var digits = newValue.text.replaceAll(_nonDigit, '');
    String formatted;

    if (digits.length > _maxDigits) {
      // digits文字列の長さが11を超えていれば、先頭から11文字でdigitsを更新する
      digits = digits.substring(0, _maxDigits);
    }
    if (digits.length <= 3) {
      // digits文字列の長さが3文字以下であれば、formattedにdigitsをそのまま代入(formatted='070')
      formatted = digits;
    } else if (digits.length <= 7) {
      // digits文字列の長さが4-7文字以下であれば、formattedに「{3文字}{半角スペース}{最大4文字}」で代入(formatted='070 1234')
      formatted = '${digits.substring(0, 3)} ${digits.substring(3)}';
    } else {
      // digits文字列の長さが8-11文字であれば、formattedに「{3文字}{半角スペース}{4文字}{半角スペース}{最大4文字}」で代入(formatted='070 1234 5678')
      formatted =
          '${digits.substring(0, 3)} ${digits.substring(3, 7)} ${digits.substring(7)}';
    }

    // カーソル位置の取得
    // 入力後文字列のカーソル位置取得(「070 12|3」→ 6、「070 12345|」→ 9)
    final rawCursor = newValue.selection.baseOffset;
    // 入力後文字列のカーソルより左にある数を数える(「070 12|3」→ 4、「070 12345|」→ 8)
    final digitCountBeforeCursor = _digit
        .allMatches(newValue.text.substring(0, rawCursor))
        .length;
    // formatted文字列に対するカーソル位置を取得
    final newCursor = _offsetForDigitIndex(formatted, digitCountBeforeCursor);

    return TextEditingValue(
      text: formatted,
      selection: TextSelection.collapsed(offset: newCursor),
      composing: TextRange.empty,
    );
  }

  /// formatted文字列に対するカーソル位置を取得
  /// 引数:
  ///     [formatted]:半角スペース入り文字列「070 1234 5」
  ///     [digitIndex]:入力後文字列のカーソルより左にある数の個数
  /// 処理の概要:
  ///     formatted文字列と入力後文字列のカーソル位置より左の数の個数から、
  ///     formatted文字列に対するカーソル位置(13)を求める。
  ///     例)formatted文字列「070 1234」、入力後文字列「070 1234」→ カーソル位置「070 1234|」なので8
  ///     例)formatted文字列「070 1234 5」、入力後文字列「070 12345」→ カーソル位置「070 1234 5|」なので10
  ///     例)formatted文字列「070 1234 5678」、入力後文字列「070 1234 567899999999」→ カーソル位置「070 1234 5678|」なので13
  int _offsetForDigitIndex(String formatted, int digitIndex) {
    if (digitIndex <= 0) return 0;
    int count = 0;

    for (var i = 0; i < formatted.length; i++) {
      if (_digit.hasMatch(formatted[i])) {
        // formatted文字列の各インデックス番目の文字が数字であればcountに加算
        count++;
        if (count == digitIndex) {
          // カーソルより左にある数を数え上げきったなら、カーソル位置として、index+1を返す
          return i + 1;
        }
      }
    }
    // コピペ対策として、digitIndexがformatted文字列の文字数より大きい時、formatted文字列の末尾を返す
    // 「070 1234 56」と入力済みに対して、「07012345678」をペースト。
    // formatted='070 1234 5607'なのでformatted.length=13、digitIndex=20
    return formatted.length;
  }

よくあるハマりどころ

カスタムのInputFormatterを実装していると、コピペへの対応を忘れがちです。こちらも忘れずに対応しましょう。

代替案

調べられてはないですが、入力欄含めてパッケージ化されているライブラリもあると思います。そちらを確認するのも対応する上での選択肢に入れておくと良いと思います。良いライブラリがあればコメントで教えていただけると幸いです。

まとめ or 次の一歩(要点/注意/次の一歩で3行)

今回は、InputFormatterを自作することで、快適な電話番号入力ができるようになりました。
TextInputType.phone だけでは文字種を完全制御できないため、フォーマッタを必ず併用する必要があります。国際化を見据え、今回の日本の携帯電話番号以外にも対応できるよう抽象化する必要であったり、ライブラリを導入することも検討するのも良いでしょう。

Discussion