7
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

FlutterでiOSスタイルの生年入力ドラムロールを実装する

Posted at

これは何?

Flutterで「誕生年」を選択するためのドラムロールを実装したので、
忘れないようにメモをします。

完成形

ユーザーが何かしらのボタンをタップした際に表示される、お馴染みのドラムロールです。

最初は「上下にスクロール」が選択されているのがミソです。

いくつか他のアプリを確認したところ、
細かく気遣っているアプリとそうでないアプリがあるようでした。

そもそもなぜ「生年」?

多いのは生年月日だと思いますが、アプリのコア機能とは直接関係のない個人情報をユーザーに登録することを要求している場合、Apple様にリジェクトされてしまうことがあります。
Guideline 5.1.1 - Legal - Privacy - Data Collection and Storage

開発チームとしてはアプリのコア機能と関係していると考えていたのですが、
Apple様がそう判断するなら従うしかありません(怒)

ということで、「誕生年」のみの取得となりました。

完成コード

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

typedef Fn = Function({required DateTime date});

Future<void> showCupertinoYearPicker({
  required double sheetHeight,
  required double itemHeight,
  required double textSize,
  required Fn handler,
  required BuildContext context,
}) async {
  final int currentYear = DateTime.now().year - 16; // 何歳から登録できるかの制限値 適宜修正する(この例では16歳)
  final int startYear = currentYear - 110; // 125歳まで登録できることを想定
  final int totalYears = currentYear - startYear + 1;
  const int scrollHintIndex = 17; // 「上下にスクロール」の初期位置 適宜修正する

  await showCupertinoModalPopup(
    context: context,
    builder: (context) {
      return Container(
        height: sheetHeight,
        color: Colors.white,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.end,
          children: [
            Padding(
              padding: const EdgeInsets.only(right: 20),
              child: TextButton(
                child: Text(
                  '閉じる',
                  style: TextStyle(
                    color: Colors.blue,
                    fontSize: textSize,
                  ),
                ),
                onPressed: () => Navigator.of(context).pop(),
              ),
            ),
            Expanded(
              child: CupertinoPicker(
                diameterRatio: 1.1,
                itemExtent: 35.0,
                onSelectedItemChanged: (int index) {
                  if (index != scrollHintIndex) {
                    handler(
                      date: DateTime(
                        currentYear - index + (index > scrollHintIndex ? 1 : 0),
                      ),
                    );
                  }
                },
                children: List<Widget>.generate(totalYears, (index) {
                  if (index == scrollHintIndex) {
                    return const Text(
                      '上下にスクロール',
                      style: TextStyle(color: CupertinoColors.black),
                    );
                  }
                  return Text(
                    '${currentYear - index + (index > scrollHintIndex ? 1 : 0)}',
                    style: const TextStyle(color: CupertinoColors.black),
                  );
                }),
                scrollController: FixedExtentScrollController(
                  initialItem: scrollHintIndex,
                ),
              ),
            ),
          ],
        ),
      );
    },
  );
}

解説

showCupertinoModalPopupを主に利用しています。

各プロパティ

  • diameterRatio: ピッカーの直径の比率。
  • itemExtent: 各アイテムの高さ(固定値35.0)。
  • onSelectedItemChanged: アイテムが選択されたときのコールバック。scrollHintIndex(「上下にスクロール」)が選択された場合を除き、選択された年に基づいてhandler関数が呼び出されます。
  • children: ピッカーの中の各アイテム。年のリストとscrollHintIndexにはヒントテキスト(「上下にスクロール」)が表示されます。

ハンドラー

typedef Fn = Function({required DateTime date});

DateTimeを引数にとる新しい関数の型を定義しています。
目的としては、ユーザーが選択した生年を、呼び出す側のViewModelへ伝えたいために、
その関数(ハンドラー)を受け取っている、といった感じになります。

required Fn handler,
onSelectedItemChanged: (int index) {
                  if (index != scrollHintIndex) {
                    handler(
                      date: DateTime(
                        currentYear - index + (index > scrollHintIndex ? 1 : 0),
                      ),
                    );
                  }
                },

後述でも記載しますが、「上下にスクロール」が選択されているときは実行されません。

handlerはViewModel層で自由にプロジェクトの目的に合わせて定義してください。
必要なければ、不要です。

初期選択位置

                scrollController: FixedExtentScrollController(
                  initialItem: scrollHintIndex,
                ),

FixedExtentScrollControllerを使用してピッカーの初期選択位置を設定しています。なお、

currentYear - index + (index > scrollHintIndex ? 1 : 0)

でお分かりの通り、「上下にスクロール」の初期文字分調整しなくてはいけないので、
ListのアイテムのindexがscrollHintIndexによってずれてしまう場合は調整してください。
※今回は上から下に行くほど誕生年が遡る形式で、scrollHintIndexより下の場合影響を受けるため、その分の位置を調整しています。

終わりに

ChatGPTに記事作成を手伝ってもらっています。
もし誤りがあった場合、ご指摘いただければ幸いです。

7
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?