🗓️

【Flutter】table_calendarの使い方、デザイン例

2024/03/09に公開

Flutterアプリでカレンダーを作成するパッケージtable_calendarの使い方、デザインの例を紹介する。

https://pub.dev/packages/table_calendar

本記事で使用しているバージョンは以下。

table_calendar: 3.1.0

インストール

pubspec.yamlを更新する。

dependencies:
  table_calendar: # インストールするバージョン

基本の使い方


基本のTableCalendar

デフォルトでは月単位のカレンダーが表示される。

この状態でできることは表示月の切り替えのみ。左右スワイプまたはアローマークのタップで表示月を切り替えできる。
日付をタップしてもマークの位置は変わらない。また「2 weeks」ボタンをタップしてもカレンダーが2週間ごと表示にはならない。

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

class CalendarPage extends StatelessWidget {
  const CalendarPage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        // カレンダーを作成
        child: TableCalendar(
          // カレンダーの開始日
          firstDay: DateTime.utc(2000, 1, 1),
          // カレンダーの終了日
          lastDay: DateTime.utc(2030, 12, 31),
          // この日付を含む範囲が表示される
          focusedDay: DateTime.now(),
        ),
      ),
    );
  }
}

機能の追加する

プロパティを設定することでTableCalendarに様々な機能を追加することができる。

日付選択


日付をタップして選択した状態

デフォルトでは日付をタップしてもカーソル位置は変わらない。
カーソル位置はcurrentDayで指定可能。その際onDaySelectedに日付をタップした際のハンドラを登録する必要がある。
またウィジェットはStatefulWidgetに変更するなど状態を可変にする必要がある。

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

  
  State<SampleCalendar> createState() => _SampleCalendarPageState();
}

class _SampleCalendarPageState extends State<SampleCalendar> {
  // カレンダーが表示される日付
  DateTime _focusedDay = DateTime.now();
  // カレンダー上でマークが表示される日付
  DateTime _currentDay = DateTime.now();

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: TableCalendar(
          firstDay: DateTime.utc(2000, 1, 1),
          lastDay: DateTime.utc(2030, 12, 31),
          focusedDay: _focusedDay,
          currentDay: _currentDay, // マークが表示される日付
          onDaySelected: ((selectedDay, focusedDay) {
            setState(() {
              _currentDay = selectedDay; // タップした際にマーク位置を更新
              _focusedDay = selectedDay; // タップした際にカレンダーの表示位置を更新
            });
          }),
        ),
      ),
    );
  }
}

特定の日付だけ選択可能にする

特定の日付だけを選択可能にしたい場合、enabledDayPredicateを設定する。
enabledDayPredicateに対してtrueを返却する日付だけ選択可能になる。

例えば2024年4月1日のみを選択可能にする場合、以下のように記載する。

enabledDayPredicate: (day) {
  return day == DateTime.utc(2024, 4, 1);
},

またこの機能を用いると、日にちが偶数の日付のみ選択可能にもできたりする。


偶数の日付だけを選択可能

final enableDays = List.generate(3,
  (index) => (index + 1) % 2 == 0 ? DateTime.utc(2024, 3, index + 1) : null);

// ...
TableCalendar(
  enabledDayPredicate: (day) {
    return enableDays.contains(day);
  },
  // ...
)

複数日付を選択

デフォルトでは日付は一つしか選択できない。
複数日付を選択する場合はselectedDayPredicateを指定する。


日付を複数選択した状態

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

  
  State<SampleCalendar> createState() => _SampleCalendarPageState();
}

class _SampleCalendarPageState extends State<SampleCalendar> {
  DateTime _focusedDay = DateTime.now();
  DateTime _currentDay = DateTime.now();
  // 選択した日付一覧
  final selectedDays = [];

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: TableCalendar(
          selectedDayPredicate: (day) => selectedDays.contains(day),
          firstDay: DateTime.utc(2000, 1, 1),
          lastDay: DateTime.utc(2030, 12, 31),
          focusedDay: _focusedDay,
          currentDay: _currentDay,
          onDaySelected: ((selectedDay, focusedDay) {
            setState(() {
              _currentDay = selectedDay;
              _focusedDay = selectedDay;

                            // タップした日付を選択した日付一覧に追加・削除
              if (selectedDays.contains(selectedDay)) {
                selectedDays.remove(selectedDay);
              } else {
                selectedDays.add(selectedDay);
              }
            });
          }),
        ),
      ),
    );
  }
}

範囲選択


範囲を選択した状態

デフォルトでは日付を一つしか選択できないが、rangeSelectionModeでモード指定を行うことで、範囲選択することが可能になる。
rangeSelectionModeに渡すプロパティとその説明は以下。

プロパティ名 説明
RangeSelectionMode.disabled (デフォルト)常時範囲選択は無効
RangeSelectionMode.toggledOff 現在範囲選択は無効で、日付を長押しすることで範囲選択を有効化
RangeSelectionMode.toggledOn 現在範囲選択は無効で、日付を長押しすることで範囲選択を無効化
RangeSelectionMode.enforced 常時範囲選択は有効
class CalendarPage extends StatefulWidget {
  const CalendarPage({super.key});

  
  State<CalendarPage> createState() => _CalendarPageState();
}

class _CalendarPageState extends State<CalendarPage> {
  DateTime _focusedDay = DateTime.now();
    // 範囲の開始日
  DateTime? _rangeStartDay = DateTime.now();
    // 範囲の終了日
  DateTime? _rangeEndDay = DateTime.now();

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: 
        TableCalendar(
          firstDay: DateTime.utc(2000, 1, 1),
          lastDay: DateTime.utc(2030, 12, 31),
          focusedDay: _focusedDay,
          // 範囲が開始する日付
          rangeStartDay: _rangeStartDay,
                   // 範囲が終了する日付
          rangeEndDay: _rangeEndDay,
                    // モードを指定して範囲を選択可能に
          rangeSelectionMode: RangeSelectionMode.enforced,
                    // 日付をタップした際のイベント
          onRangeSelected: (start, end, focusedDay) {
            setState(() {
              // 開始日を更新
              _rangeStartDay = start;
                           // 終了日を更新
              _rangeEndDay = end;
                          // 表示する日付を更新
              _focusedDay = focusedDay;
            });
          },
        ),
      ),
    );
  }
}

フォーマット(1ヶ月表示、2週間表示、1週間表)切り替え


1ヶ月表示


2週間表示


1週間表示

onFormatChangedを定義することでカレンダーのフォーマットを変更する機能を実装できる。
画面上でフォーマットを変更するには、画像に写っているカレンダーのヘッダーにあるボタンをタップする。

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

  
  State<CalendarPage> createState() => _CalendarPageState();
}

class _CalendarPageState extends State<CalendarPage> {
  DateTime _focusedDay = DateTime.now();
  // 選択中のフォーマット
  CalendarFormat _calendarFormat = CalendarFormat.month;

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: TableCalendar(
          locale: 'ja_JP',
          firstDay: DateTime.utc(2000, 1, 1),
          lastDay: DateTime.utc(2030, 12, 31),
          focusedDay: _focusedDay,
          // 選択中のフォーマット
          calendarFormat: _calendarFormat,
          // フォーマット変更ハンドラ
          onFormatChanged: (format) => setState(() {
            _calendarFormat = format;
          }),
        ),
      ),
    );
  }
}

スワイプ操作の制御

TableCalendarは以下のスワイプ操作をすることができる。

  • 水平方向(左右)スワイプ:カレンダー範囲切り替え
  • 垂直方向(上下)スワイプ:カレンダーフォーマット切り替え

availableGesturesを指定することで上記のスワイプ操作を制御できる。

プロパティ 説明
AvailableGestures.all 全てのジェスチャーが有効。
AvailableGestures.none すべてのジェスチャーを無効化。
AvailableGestures.verticalSwipe 垂直スワイプが有効。
AvailableGestures.horizontalSwipe 水平スワイプが有効。

ロケール対応

カレンダーに表示される曜日や月の表記はデフォルト時英語。
localeを指定することで、これらの表示をロケール対応できる。


ロケール先を日本に指定した場合

ロケール対応するにはintlパッケージをインストールする。

dependencies:
  flutter:
    sdk: flutter
  table_calendar: 3.1.0
  intl:

runApp()の前にinitializeDateFormatting()を行う必要がある。

import 'package:intl/date_symbol_data_local.dart';

void main() {
  initializeDateFormatting().then((_) => runApp(MyApp()));
}

最後にlocaleプロパティにロケールを指定する。

TableCalendar(
  // ロケールを日本に指定
  locale: "ja_JP",
  //...
)

デザイン例

プロパティを設定することでデザインをカスタムすることができる。

ヘッダーを非表示にする


表示状態


非表示状態

デフォルトではヘッダーが表示されている。headerVisiblefalseに指定することで、ヘッダーを非表示にできる。

曜日を非表示にする


表示状態


非表示状態

デフォルトでは曜日が表示されている。daysOfWeekVisiblefalseに指定することで、曜日を非表示にできる。

開始曜日を変更する


日曜日開始


水曜日開始

デフォルトでは週は日曜日から始まる。startingDayOfWeekの値を指定することで、週の開始曜日を変更できる。

// 水曜日に指定
startingDayOfWeek: StartingDayOfWeek.wednesday,

日付の表示を変更する

デフォルトでは日付は日にちが表示される。

日付のデザインを変更するにはcalendarBuildersを設定する。

calendarBuilders: CalendarBuilders(
  // デザインを指定
)

CalendarBuilders()には複数のプロパティがあり、それぞれ次の部分を設定できる。

ビルダー 説明
prioritizedBuilder 有効日、今日、選択日、範囲の始点/終点、外部の日付、非アクティブ日付などを優先的にビルドする。
todayBuilder 今日の日付にカスタムウィジェットをビルドする。
selectedBuilder 選択された日付にカスタムウィジェットをビルドする。
rangeStartBuilder 範囲の始点にカスタムウィジェットをビルドする。
rangeEndBuilder 範囲の終点にカスタムウィジェットをビルドする。
withinRangeBuilder 範囲内の日付にカスタムウィジェットをビルドする。
outsideBuilder 範囲外の日付にカスタムウィジェットをビルドする。
disabledBuilder 無効な日付にカスタムウィジェットをビルドする。
holidayBuilder 祝日にカスタムウィジェットをビルドする。
defaultBuilder デフォルトのビルダーで、通常の日付にカスタムウィジェットをビルドする。
rangeHighlightBuilder 範囲のハイライトにカスタムウィジェットをビルドする。
singleMarkerBuilder 単一日に対するマーカーをビルドする。
markerBuilder 日付ごとの複数のマーカーをビルドする。
dowBuilder 曜日(日曜日から土曜日まで)にカスタムウィジェットをビルドする。
headerTitleBuilder カレンダーのヘッダーに表示されるタイトル(月と年)にカスタムウィジェットをビルドする。
weekNumberBuilder 週番号にカスタムウィジェットをビルドする。

例えば今日の日付の表示を変更するにはtodayBuilderを指定する。

TableCalendar(
  firstDay: DateTime.utc(2000, 1, 1),
  lastDay: DateTime.utc(2030, 12, 31),
  focusedDay: DateTime.now(),
  calendarBuilders: CalendarBuilders(
    // 今日の日付のデザインを指定
      todayBuilder: (context, todayDay, focusDay) =>
          const Center(child: Icon(Icons.star))),
),

日にちは第二引数から参照できる。

TableCalendar(
  firstDay: DateTime.utc(2000, 1, 1),
  lastDay: DateTime.utc(2030, 12, 31),
  focusedDay: DateTime.now(),
  calendarBuilders: CalendarBuilders(
      // 今日の日付のデザインを指定
      todayBuilder: (context, todayDay, focusDay) => Center(
          child: Container(
              height: double.infinity,
              width: double.infinity,
              color: Colors.blue,
              child: Center(
                  child: Text(
                todayDay.day.toString(),
                style: TextStyle(
                    color: Colors.white,
                    fontWeight: FontWeight.w900,
                    fontSize: 24),
              ))))),
),

サイズを小さくする

TableCalendarを小さく表示するには少しコツがいる。
今回は下画像の様にカレンダーのサイズを、画面サイズの半分に小さくしてみる。


小さく表示したTableCalendar

ヘッダーは非表示。また今日の日付は青いマークが表示される様に指定した。

SizedBox(
  // 幅を画面サイズの半分
  width: MediaQuery.of(context).size.width * 0.5,
  child: TableCalendar(
    firstDay: DateTime.utc(2000, 1, 1),
    lastDay: DateTime.utc(2030, 12, 31),
    focusedDay: _focusedDay,
    // 列の高さ30px(デフォルトは52px)
    rowHeight: 30,
    // ヘッダーを非表示
    headerVisible: false,
    // カレンダーの各要素のスタイルを指定
    calendarBuilders: CalendarBuilders(
        // 今日の日付のスタイル
        todayBuilder: (context, day, focusedDay) {
      return Container(
        width: 20,
        height: 20,
        decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(10),
            color: Colors.blue),
        child: Center(
          child: Text(
            day.day.toString(),
            style: TextStyle(
                color: Colors.white, fontWeight: FontWeight.w600),
          ),
        ),
      );
    },
        // 今日以外の範囲内日付のスタイル
        defaultBuilder: (context, day, focusedDay) {
      return Text(day.day.toString());
    },
        // 範囲外日付のスタイル
        outsideBuilder: (context, day, focusedDay) {
      return Text(
        day.day.toString(),
        // 範囲外なので文字色グレー
        style: const TextStyle(color: Colors.grey),
      );
    }),
  ),
),

単にSizedBoxでラップするだけではエラーになるので、各日付セルの高さや日付文字のサイズを調整する必要がある。

プロパティ

TableCalendarウィジェットのプロパティ一覧。

プロパティ名 役割
focusedDay (必須)フォーカスされている日付、カレンダーはこの日付が含まれる月や週を表示する
firstDay (必須)カレンダーの最初の日
lastDay (必須)カレンダーの最後の日
currentDay 現在選択中の日付、デフォルトでは丸いマークがついている
locale カレンダーが対応するロケールを指定
rangeStartDay 範囲の開始日
rangeEndDay 範囲の終了日
weekendDays 週末の日、デフォルトは土日
calendarFormat カレンダーの表示フォーマット
availableCalendarFormats 利用可能なカレンダーフォーマット
headerVisible ヘッダーの表示
daysOfWeekVisible 曜日の表示
pageJumpingEnabled ページジャンプの有効化
pageAnimationEnabled 範囲外日付タップ時の、ページ遷移アニメーション有効化
sixWeekMonthsEnforced trueで必ず6週間表示
shouldFillViewport trueで可能な限りカレンダーが広がる
weekNumbersVisible trueで週の番号を表示
rowHeight ヘッダーを除く日付の行の高さ
daysOfWeekHeight 曜日行の高さ
formatAnimationDuration フォーマット切り替えアニメーションの時間
formatAnimationCurve フォーマット切り替えアニメーションのCurveを指定
pageAnimationDuration カレンダー範囲切り替えのアニメーション時間
pageAnimationCurve カレンダー範囲切り替えのアニメーションのCurveを指定
startingDayOfWeek 週の始まりの曜日を指定
dayHitTestBehavior 日付をタップした際のヒットテストの挙動を指定
availableGestures 利用可能なジェスチャー
simpleSwipeConfig 左右スワイプの挙動を制御する
headerStyle ヘッダーのスタイルを指定
daysOfWeekStyle 曜日のスタイルを指定
calendarStyle カレンダーのスタイルを指定
calendarBuilders カレンダーのビルダー
rangeSelectionMode 範囲の選択モード
eventLoader イベントの読み込み関数
enabledDayPredicate 有効な日のプレディケート
selectedDayPredicate 選択された日のプレディケート
holidayPredicate 休日のプレディケート
onRangeSelected 範囲が選択されたときのコールバック
onDaySelected 日が選択されたときのコールバック
onDayLongPressed 日が長押しされたときのコールバック
onDisabledDayTapped 無効な日がタップされたときのコールバック
onDisabledDayLongPressed 無効な日が長押しされたときのコールバック
onHeaderTapped ヘッダーがタップされたときのコールバック
onHeaderLongPressed ヘッダーが長押しされたときのコールバック
onPageChanged ページが変更されたときのコールバック
onFormatChanged フォーマットが変更されたときのコールバック
onCalendarCreated カレンダーが作成されたときのコールバック

Discussion