Skip to content

[Flutter] 快速建立日曆元件的套件:table_calendar

Last Updated on 2021-10-13 by Clay

日曆元件是在許多 APP 中都會使用到的元件,比方說記帳、記事相關方面的 APP。而在使用 Flutter 進行開發時,我們可以不用重頭開始造輪子,而是呼叫前輩大神們所開發的日曆套件。

今天我要介紹的是,是在 Flutter pub 上也幾乎是最受歡迎的套件:table_calendar


準備工作

首先,自然得在 pubspec.yaml 文件底下寫入想要使用的套件。

dependencies:
  flutter:
    sdk: flutter

  table_calendar: ^2.3.3


並使用以下指令取得套件。

flutter pub get

範例程式碼介紹

以下我一步步介紹如何使用程式碼呼叫此套件建立日曆元件。這個範例程式碼稍微有點長,若是想要直接試跑,可以直接看下一小節的完整程式碼。


匯入套件

首先,自然得匯入所需要的套件。

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

import 'package:intl/date_symbol_data_local.dart';
import 'package:table_calendar/table_calendar.dart';



(Optional)設定假日

// Holiday will display red word
final Map<DateTime, List> _holidays = {
  DateTime(2021, 3, 1): ["228"],
};


table_calendar 中,我們可以設定所需要的假日。在這裡可以設定很多項。接口則會放在後續建立日曆物件時。


撰寫 HomePage 頁面

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


// MyApp
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Hide the top status bar
    SystemChrome.setEnabledSystemUIOverlays([]);
    return MaterialApp(
      home: HomePage(title: "Table Calender"),
      debugShowCheckedModeBanner: false,
    );
  }
}


// HomePage
class HomePage extends StatefulWidget {
  final String title;
  HomePage({Key key, this.title}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();
}


這部分是 Flutter APP 的起手式: main() 進入點、HomePage 進入時的介面等等。

接著下一步才是撰寫 _HomePageState 中所有互動的功能。


_HomePageState 中初始狀態

class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
  Map<DateTime, List> _events;
  List _selectedEvents;
  AnimationController _animationController;
  CalendarController _calendarController;

  @override
  void initState() {
    super.initState();

    // Today
    final _selectedDay = DateTime.now();

    // Events
    _events = {
      _selectedDay.add(Duration(days: 1)): [
        'Sleep all day',
      ],
      _selectedDay.add(Duration(days: 2)): [
        'Play PS4 Game',
        'Exercising',
        'Coding',
        'Sleeping',
        'Watch a movie',
        'Take a walk',
        'Surf on the internet'
      ],
    };

    _selectedEvents = _events[_selectedDay] ?? [];
    _calendarController = CalendarController();
    _animationController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 400),
    );

    _animationController.forward();
  }


  • _selectedEvents 為選擇事件的列表
  • _animationController 則是控制動畫
  • _calendarController 顧名思義便是控制日曆元件的物件

下面 _events 則是我隨意建立的活動事件。我想像我是製作一個像行事曆一樣的 APP,一天裡面則是紀錄著我有可能要從事的活動。


重寫一些函式(仍在 _HomePageState 底下)

  @override
  void dispose() {
    _animationController.dispose();
    _calendarController.dispose();
    super.dispose();
  }

  void _onDaySelected(DateTime day, List events, List holidays) {
    print("CALLBACK: _onDaySelected");
    setState(() {
      _selectedEvents = events;
    });
  }

  void _onVisibleDaysChanged(DateTime first, DateTime last, CalendarFormat format) {
    print("CALLBACK: _onVisibleDaysChanged");
  }

  void _onCalendarCreated(DateTime first, DateTime last, CalendarFormat format) {
    print("CALLBACK: _onCalendarCreated");
  }


這裡我們重寫了一些函式,主要是之後讓我們在跟日曆元件互動時能看到 LOG 的訊息。這部分是最初 table_calendar 的說明文件上就有寫著的。這邊我也就直接保留,沒有修改。


介面排列的形式(仍在 _HomePageState 底下)

  // Basic interface
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Column(
        mainAxisSize: MainAxisSize.max,
        children: [
          _buildTableCalendar(),
          Expanded(child: _buildEventList()),
        ],
      ),
    );
  }


這裡最重要的便是 Column 元件底下的佈局。既然是包在 Column 元件,那代表我的佈局是從上到下排列。

接著我們再來實作 _buildTableCalendar() 以及 _buildEventList() 這兩個方法。


_buildEventList() (仍在 _HomePageState 底下)

// Event List
  Widget _buildEventList() {
    return ListView(
      children: _selectedEvents.map(
              (event) => Container(
            decoration: BoxDecoration(
              color: Colors.lightBlueAccent[100],
              border: Border.all(width: 0.8),
              borderRadius: BorderRadius.circular(12.0),
            ),
            margin: const EdgeInsets.symmetric(
              horizontal: 2.0,
              vertical: 1.0,
            ),
            child: ListTile(
              title: Text(event.toString()),
              onTap: () => print("${event} tapped!"),
            ),
          )
      ).toList(),
    );
  }


這裡看得出來是以 ListView 元件製作,並將我最初設定的 _events 事件帶入。其他的便是一些顏色長寬之類的參數設定。


_buildTableCalendar() (仍在 _HomePageState 底下)

// Simple TableCalendar configuration (using Style)
  Widget _buildTableCalendar() {
    return TableCalendar(
      calendarController: _calendarController,
      events: _events,
      holidays: _holidays,
      startingDayOfWeek: StartingDayOfWeek.sunday,

      // Calendar
      calendarStyle: CalendarStyle(
        selectedColor: Colors.lightBlueAccent[400],
        todayColor: Colors.lightBlueAccent[100],
        markersColor: Colors.lightBlue[600],
        outsideDaysVisible: false,
      ),

      // Header
      headerStyle: HeaderStyle(
        formatButtonTextStyle:
        TextStyle().copyWith(
          color: Colors.white,
          fontSize: 15.0,
        ),
        formatButtonDecoration: BoxDecoration(
          color: Colors.grey[600],
          borderRadius: BorderRadius.circular(8.0),
        ),
      ),

      // Operating
      onDaySelected: _onDaySelected,
      onVisibleDaysChanged: _onVisibleDaysChanged,
      onCalendarCreated: _onCalendarCreated,
    );
  }


這部分是日曆元件最需要研究的部分。目前我也尚未完全弄清楚每個接口的使用方法。不過就我看到的一些網路上的範例來說,能改的地方還真不少。

主要就是建立 TableCalendar 這個元件。

執行結果請參考下方完整程式碼。即便加上空格也在兩百行以內,其實算是精簡的範例程式碼了。


完整程式碼

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

import 'package:intl/date_symbol_data_local.dart';
import 'package:table_calendar/table_calendar.dart';


// Holiday will display red word
final Map<DateTime, List> _holidays = {
  DateTime(2021, 3, 1): ["228"],
};


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


// MyApp
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Hide the top status bar
    SystemChrome.setEnabledSystemUIOverlays([]);
    return MaterialApp(
      home: HomePage(title: "Table Calender"),
      debugShowCheckedModeBanner: false,
    );
  }
}


// HomePage
class HomePage extends StatefulWidget {
  final String title;
  HomePage({Key key, this.title}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();
}


class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
  Map<DateTime, List> _events;
  List _selectedEvents;
  AnimationController _animationController;
  CalendarController _calendarController;

  @override
  void initState() {
    super.initState();

    // Today
    final _selectedDay = DateTime.now();

    // Events
    _events = {
      _selectedDay.add(Duration(days: 1)): [
        'Sleep all day',
      ],
      _selectedDay.add(Duration(days: 2)): [
        'Play PS4 Game',
        'Exercising',
        'Coding',
        'Sleeping',
        'Watch a movie',
        'Take a walk',
        'Surf on the internet'
      ],
    };

    _selectedEvents = _events[_selectedDay] ?? [];
    _calendarController = CalendarController();
    _animationController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 400),
    );

    _animationController.forward();
  }

  @override
  void dispose() {
    _animationController.dispose();
    _calendarController.dispose();
    super.dispose();
  }

  void _onDaySelected(DateTime day, List events, List holidays) {
    print("CALLBACK: _onDaySelected");
    setState(() {
      _selectedEvents = events;
    });
  }

  void _onVisibleDaysChanged(DateTime first, DateTime last, CalendarFormat format) {
    print("CALLBACK: _onVisibleDaysChanged");
  }

  void _onCalendarCreated(DateTime first, DateTime last, CalendarFormat format) {
    print("CALLBACK: _onCalendarCreated");
  }


  // Basic interface
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Column(
        mainAxisSize: MainAxisSize.max,
        children: [
          _buildTableCalendar(),
          Expanded(child: _buildEventList()),
        ],
      ),
    );
  }

  // Simple TableCalendar configuration (using Style)
  Widget _buildTableCalendar() {
    return TableCalendar(
      calendarController: _calendarController,
      events: _events,
      holidays: _holidays,
      startingDayOfWeek: StartingDayOfWeek.sunday,

      // Calendar
      calendarStyle: CalendarStyle(
        selectedColor: Colors.lightBlueAccent[400],
        todayColor: Colors.lightBlueAccent[100],
        markersColor: Colors.lightBlue[600],
        outsideDaysVisible: false,
      ),

      // Header
      headerStyle: HeaderStyle(
        formatButtonTextStyle:
        TextStyle().copyWith(
          color: Colors.white,
          fontSize: 15.0,
        ),
        formatButtonDecoration: BoxDecoration(
          color: Colors.grey[600],
          borderRadius: BorderRadius.circular(8.0),
        ),
      ),

      // Operating
      onDaySelected: _onDaySelected,
      onVisibleDaysChanged: _onVisibleDaysChanged,
      onCalendarCreated: _onCalendarCreated,
    );
  }

  // Event List
  Widget _buildEventList() {
    return ListView(
      children: _selectedEvents.map(
          (event) => Container(
            decoration: BoxDecoration(
              color: Colors.lightBlueAccent[100],
              border: Border.all(width: 0.8),
              borderRadius: BorderRadius.circular(12.0),
            ),
            margin: const EdgeInsets.symmetric(
              horizontal: 2.0,
              vertical: 1.0,
            ),
            child: ListTile(
              title: Text(event.toString()),
              onTap: () => print("${event} tapped!"),
            ),
          )
      ).toList(),
    );
  }
}


Output:


References


Read More

Leave a Reply