[Flutter/dart]カレンダーを自作する


概要


スマホアプリでカレンダーを使う機会は多いですよね。カレンダーを使用できるライブラリは多いのですが、自分がやりたいことができない、ということも多いでしょう。特に、日付と一緒に他のwidgetを表示することがやりにくいものが多い印象です。

そんな場合は自分でカレンダーを作ってしまいましょう。



方法


指定の月に含まれる日にちを取得


まずはある月に含まれる日にちを取得しましょう。1日から始めて、月が変わるまで1日づつaddしていきます。(もちろん西向く士は30日で、、、という場合分けをしてもいいですが、閏年とかも考慮すると結構面倒です。)


List<List<DateTime>> _datesInMonth(int year, int month) {
  List<List<DateTime>> _dates = [];
  List<DateTime> _datesInWeek=[];
  DateTime _date = DateTime(year, month, 1);

  while (_date.month == month) { //1

    _datesInWeek.add(_date);
    if (_date.weekday==DateTime.saturday){
      _dates.add(_datesInWeek);
      _datesInWeek=[]; //2 土曜日で区切る
    }
    _date = _date.add(Duration(days: 1)); 
  }

  if (_datesInWeek.length>0){
    _dates.add(_datesInWeek);
  }

  return _dates;
}

「1週間に含まれる日にちのリスト」のリストを返す関数です。

その月の1日から始め、月が変わるまで日にちを追加していきます(1)。

土曜日を追加したら「1週間のリスト」は完成です(2)。



「1週間のリスト」をカレンダーのwidgetに


次に、各「1週間のリスト」をカレンダーの形式のwidgetに変換します。

TableRow _weekWidget(List<DateTime> _dates){

  List<Widget> widgets=[];
  int startWeekDay=_dates[0].weekday;
  bool _isFirstWeek =(_dates[0].day==1);
  DateTime _additionalDate;

  if (startWeekDay==DateTime.sunday){ //日曜はじまり

    widgets=_dates.map((DateTime d) => 
                 _dayWidget(d, _isFirstWeek)).toList();

    _additionalDate=_dates[_dates.length-1].add(Duration(days: 1));
    while(widgets.length<DateTime.daysPerWeek){
      //月末は1週間が7日になるまで翌月の日を追加
      widgets.add(_dayWidget(_additionalDate, _isFirstWeek));
      _additionalDate=_additionalDate.add(Duration(days: 1));
    }
  }
  else{ //日曜以外から始まり(=第1週)

    _additionalDate=_dates[0].subtract(Duration(days: 1));
    List<DateTime> _additionalDates=[];

    //前の月
    while(_additionalDates.length<DateTime.daysPerWeek-_dates.length){
      _additionalDates.add(_additionalDate);
      _additionalDate=_additionalDate.subtract(Duration(days: 1));
    }

    int _len=_additionalDates.length;
    for (int i=0; i<_len; i++){
      widgets.add(_dayWidget(_additionalDates[_len-i-1], 
             _isFirstWeek));
    }

    widgets.addAll(_dates.map((DateTime d)
                   =>_dayWidget(d, _isFirstWeek)));
  }

  return TableRow(
      children: widgets
  );
}

1週間のリストに含まれる各日をwidgetにしていきます(_dayWidget 詳細は後述)。

ただし、月頭と月末は、1週間のリストに7日未満しか含まれていない可能性があるので、前月や翌月を加えます。



日にちをwidgetに


各日をwidgetにするメソッドは以下の通りです。

Widget _dayWidget(DateTime date, bool addWeekDayStr){

    const double PAD_DEFAULT=5;
    double pad_left= date.weekday==DateTime.sunday? 0: PAD_DEFAULT;
    double pad_right=date.weekday==DateTime.saturday?0: PAD_DEFAULT;
    List<Widget> _dayColumn=[];
    DateTime _now=DateTime.now();
    bool _isToday=((date.year==_now.year) && (date.month==_now.month) 
             && (date.day==_now.day));

    if (addWeekDayStr) {
      _dayColumn.add(Text(WEEKDAYS[date.weekday])); //1 曜日
      _dayColumn.add(SizedBox(height: 8,));
    }

    _dayColumn.addAll([
      Container(
        padding: const EdgeInsets.all(2),
        alignment: Alignment.center,
        decoration: _isToday? BoxDecoration(color: Colors.red ,shape: 
               BoxShape.circle): null, //2 今日は背景を赤く
        child: Text(
          date.day.toString(),
          style:
            TextStyle(
              fontSize: 16,
              color: _isToday? Colors.white :dateColor(date), 
              //3 土日は色を変える
          ),
        ),
      ),
      SizedBox(height: 4,)
    ]);

    //4 お好みのwidgetを追加
    _dayColumn.add(Icon(Icons.brightness_1, color: Colors.green,));

    return Container(
        padding: EdgeInsets.only(left: pad_left, right: pad_right, 
                      bottom: 5),
        child: Column(
          children: _dayColumn,
        ),
      );
  }

その日が月の第1週の場合、addWeekDayStrがtrueになり、曜日が日付の上に追加されます(1)。

今日は日付の背景を赤くします(2)。

日付の文字色は平日黒、土曜青、日曜赤にします(3)。

日付の下にお好みのwidgetを追加できます(4)。


曜日に応じて日付の色を返すヘルパメソッドはこちら

static Color dateColor(DateTime date){

  switch(date.weekday){

    case DateTime.sunday:
      return Colors.red;

    case DateTime.saturday:
      return Colors.blue;

    default:
      return Colors.black;
  }
}

表示する月を変更


表示する月の変更は以下の通りです。

void _changeRange(int direction){

    _selectedDay = DateTime(_selectedDay.year, _selectedDay.month + 
                 direction, 1);

    setState(() {});
  }

全体


以上を用いて以下のようにカレンダーを作成します。

Widget _calendarWidget(DateTime startDay){

  int _year=startDay.year;
  int _month=startDay.month;
  List<List<DateTime>> _drawnDates;
  const int MOVE_LEFT=-1;
  const int MOVE_RIGHT=1;

  _drawnDates=_datesInMonth(_selectedDay.year, _selectedDay.month);

  return
    Column(
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            GestureDetector(
              child: Container(
                padding: const EdgeInsets.only(right: 16),
                color: Colors.transparent,
                child: Text(
                  "<",
                  style: TextStyle(
                    fontSize: 18
                  ),
                ),
              ),
              onTap: ()=>_changeRange(MOVE_LEFT),
            ),
            Text(
              "${_year}${_month}月",
              style: TextStyle(
                  fontSize: 18
              ),
            ),
            GestureDetector(
              child: Container(
                padding: const EdgeInsets.only(left: 16),
                color: Colors.transparent,
                child: Text(
                  ">",
                  style: TextStyle(
                      fontSize: 18
                  ),
                ),
              ),
              onTap: ()=>_changeRange(MOVE_RIGHT),
            ),
          ],
        ),
        SizedBox(height: 16,),
        Table(
          children: _drawnDates.map((List<DateTime> _days) 
                     =>_weekWidget(_days)).toList(),
        ),
      ]
  );
}

以下のようなカレンダーができます。



最新記事

すべて表示

現象 flutter_local_notificationを使って通知を作成。通知タップ時の動作も登録。 通知が来た時にアプリが立ち上がっている(foregroundでもbackgroundでも)と、期待通りの動作となる。 しかし、通知が来た時にアプリが終了していると、通知をタップしても登録した動作とならない。 原因と解決策 アプリが終了すると登録した動作は消えてしまうらしい(参考)。 Note:

概要 Firebase Messagingでメッセージを送信し、通知がタップされたら何らかの動作を実行する(例:特定のページに移動する)という場合は多いかと思います。 通知タップ時にアプリがforeground状態(つまりアプリを開いている)ならば簡単なのですが、アプリがbackground状態にある場合は少し工夫が必要です。 方法 FirebaseMessaging.onBackgroundMe

現象 FirebaseMessaging.onBackgroundMessage()でタイトルのエラーが発生。 原因と解決策 上記メソッドの引数に渡すデリゲートはトップレベル関数でなければならない。自分の場合はインスタンスメソッドを渡していたのでエラーとなった。 渡すメソッドをトップレベル関数にするとエラーは解消した。 @main.dart void main() async{ //略 F

靴を大切にしよう!靴管理アプリ SHOES_KEEP

納品:iPhone6.5①.png

靴の履いた回数、お手入れ回数を管理するアプリです。

google-play-badge.png
Download_on_the_App_Store_Badge_JP_RGB_blk_100317.png

テーマ日記:テーマを決めてジャンルごとに記録

訂正①2040×1152.jpg

ジャンルごとにテーマ、サブテーマをつけて投稿、記録できる日記アプリです。

google-play-badge.png