在这里插入图片描述

概述

table_calendar是Flutter中最流行的日历组件库,它提供了功能完整的日历显示和日期选择功能。在视力保护提醒应用中,我们使用table_calendar来展示月度统计数据和日期选择。本文将详细讲解如何使用table_calendar创建日历组件,包括日期选择、事件标记、自定义样式等功能。

table_calendar的核心功能

table_calendar主要提供以下功能:

  1. 日历显示 - 展示完整的月度日历
  2. 日期选择 - 支持单个或多个日期选择
  3. 事件标记 - 在日期上标记事件
  4. 自定义样式 - 支持丰富的样式自定义选项

这些功能结合在一起,为应用提供了一个完整的日历解决方案。

项目依赖配置

在pubspec.yaml中,我们已经配置了所需的依赖:

dependencies:
  flutter:
    sdk: flutter
  table_calendar: ^3.0.9
  flutter_screenutil: ^5.9.0

table_calendar库提供了日历功能,flutter_screenutil用于屏幕适配。这些依赖都是为了支持鸿蒙系统的Flutter开发。

基础日历实现

基础日历实现包括日期选择和月份导航。首先我们需要创建一个StatefulWidget来管理日历的状态,包括当前焦点日期和选中的日期。通过在initState中初始化这些变量,我们可以确保日历在首次加载时显示当前日期。这种状态管理方式使得日历能够响应用户的交互操作。

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

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

class _CalendarPageState extends State<CalendarPage> {
  late DateTime _focusedDay;
  late DateTime _selectedDay;

  
  void initState() {
    super.initState();
    _focusedDay = DateTime.now();
    _selectedDay = DateTime.now();
  }

这段代码定义了CalendarPage这个StatefulWidget类。通过继承StatefulWidget,我们可以创建一个有状态的组件,能够在用户交互时更新UI。_focusedDay表示当前显示的月份,_selectedDay表示用户选中的日期。在initState方法中,我们将这两个变量都初始化为当前日期,这样日历首次加载时就会显示今天的日期。late关键字表示这些变量会在后续被初始化,不需要在声明时赋值。

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('日历'),
        backgroundColor: const Color(0xFF2196F3),
        foregroundColor: Colors.white,
      ),
      body: SingleChildScrollView(
        child: Column(
          children: [
            _buildCalendar(),
            SizedBox(height: 16.h),
            _buildSelectedDateInfo(),
            SizedBox(height: 20.h),
          ],
        ),
      ),
    );
  }

build方法是Widget的核心方法,返回该Widget的UI结构。Scaffold提供了Material Design的基础框架,包括AppBar和body。AppBar设置了蓝色背景和白色文字,提供了顶部导航栏。SingleChildScrollView使页面内容可以滚动,这在日历下方有其他内容时非常有用。Column布局用于垂直排列日历组件和日期信息显示区域。通过调用_buildCalendar()和_buildSelectedDateInfo()方法,我们将日历和日期信息分离为独立的组件,提高了代码的可读性和复用性。

  Widget _buildCalendar() {
    return Container(
      margin: EdgeInsets.all(16.w),
      padding: EdgeInsets.all(16.w),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12.r),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.1),
            blurRadius: 8,
            offset: const Offset(0, 2),
          ),
        ],
      ),
      child: TableCalendar(

_buildCalendar方法构建了日历的容器和样式。Container提供了白色背景、圆角和阴影效果,使日历看起来像一个独立的卡片。margin设置外边距,padding设置内边距,这样日历与屏幕边缘和内部内容都有适当的间距。BoxDecoration定义了容器的装饰效果,包括白色背景、12个单位的圆角和阴影。阴影使用黑色半透明颜色,blurRadius为8使阴影看起来柔和。TableCalendar是table_calendar库的核心组件,它提供了完整的日历功能。

        firstDay: DateTime.utc(2024, 1, 1),
        lastDay: DateTime.utc(2025, 12, 31),
        focusedDay: _focusedDay,
        selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
        onDaySelected: (selectedDay, focusedDay) {
          setState(() {
            _selectedDay = selectedDay;
            _focusedDay = focusedDay;
          });
        },
        onPageChanged: (focusedDay) {
          _focusedDay = focusedDay;
        },

TableCalendar的参数配置了日历的基本功能。firstDay和lastDay定义了日历的可选日期范围,这里设置为2024年1月1日到2025年12月31日。focusedDay表示当前显示的月份,通过_focusedDay变量控制。selectedDayPredicate是一个回调函数,用于判断某个日期是否被选中,这里使用isSameDay方法比较日期。onDaySelected回调在用户点击日期时触发,通过setState更新_selectedDay和_focusedDay,这会触发Widget的重新构建。onPageChanged回调在用户切换月份时触发,更新_focusedDay以显示新的月份。

        calendarStyle: CalendarStyle(
          selectedDecoration: BoxDecoration(
            color: const Color(0xFF2196F3),
            shape: BoxShape.circle,
          ),
          todayDecoration: BoxDecoration(
            color: const Color(0xFF2196F3).withOpacity(0.3),
            shape: BoxShape.circle,
          ),
          weekendTextStyle: TextStyle(
            fontSize: 14.sp,
            color: Colors.red,
          ),
          defaultTextStyle: TextStyle(fontSize: 14.sp),
        ),

calendarStyle定义了日期单元格的样式。selectedDecoration设置选中日期的背景为蓝色圆形,这样用户可以清楚地看到选中的日期。todayDecoration设置今天的背景为半透明蓝色,使其与选中日期区分开来。weekendTextStyle将周末文字设置为红色,这是日历的常见做法,可以帮助用户快速识别周末。defaultTextStyle设置普通日期的文字样式,使用14个单位的字体大小。这些样式配置共同构成了日历的视觉效果。

        headerStyle: HeaderStyle(
          formatButtonVisible: false,
          titleCentered: true,
          titleTextStyle: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold),
          leftChevronIcon: Icon(Icons.chevron_left, size: 20.sp),
          rightChevronIcon: Icon(Icons.chevron_right, size: 20.sp),
        ),
        daysOfWeekStyle: DaysOfWeekStyle(
          weekdayStyle: TextStyle(fontSize: 12.sp, fontWeight: FontWeight.bold),
          weekendStyle: TextStyle(fontSize: 12.sp, fontWeight: FontWeight.bold, color: Colors.red),
        ),
      ),
    );
  }

headerStyle定义了日历头部的样式。formatButtonVisible设置为false隐藏格式按钮,使头部看起来更简洁。titleCentered设置为true使月份标题居中显示。titleTextStyle定义了月份标题的文字样式,使用16个单位的粗体字体。leftChevronIcon和rightChevronIcon定义了左右导航按钮的图标,使用chevron_left和chevron_right图标。daysOfWeekStyle定义了周标题行的样式,weekdayStyle设置工作日的样式,weekendStyle设置周末的样式并使用红色文字。这些样式配置使日历的导航和周标题更加清晰易读。

  Widget _buildSelectedDateInfo() {
    return Container(
      margin: EdgeInsets.symmetric(horizontal: 16.w),
      padding: EdgeInsets.all(16.w),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12.r),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.1),
            blurRadius: 8,
            offset: const Offset(0, 2),
          ),
        ],
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            '选中日期',
            style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold),
          ),
          SizedBox(height: 12.h),
          Text(
            '${_selectedDay.year}${_selectedDay.month}${_selectedDay.day}日',
            style: TextStyle(fontSize: 14.sp, color: const Color(0xFF2196F3)),
          ),
          SizedBox(height: 8.h),
          Text(
            '星期${_getWeekday(_selectedDay.weekday)}',
            style: TextStyle(fontSize: 12.sp, color: Colors.grey),
          ),
        ],
      ),
    );
  }

  String _getWeekday(int weekday) {
    const weekdays = ['一', '二', '三', '四', '五', '六', '日'];
    return weekdays[weekday - 1];
  }
}

_buildSelectedDateInfo方法显示用户选中的日期信息。Container提供了与日历相同的样式,包括白色背景、圆角和阴影,使两个卡片看起来一致。Column布局用于垂直排列标题和日期信息。第一个Text显示"选中日期"标题,使用粗体。第二个Text显示完整的日期,格式为"年月日",使用蓝色文字。第三个Text显示星期几,使用灰色文字。_getWeekday方法将weekday数字(1-7)转换为中文星期表示(一-日),这样用户可以看到更易读的星期信息。

事件标记

在日历上标记事件。事件标记功能允许我们在日历上显示特定日期的事件,这对于提醒应用非常重要。我们使用一个Map数据结构来存储每个日期对应的事件列表,这样可以轻松地查询和管理事件数据。通过eventLoader参数,table_calendar能够自动加载并显示这些事件。

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

  
  State<EventCalendarPage> createState() => _EventCalendarPageState();
}

class _EventCalendarPageState extends State<EventCalendarPage> {
  late DateTime _focusedDay;
  late DateTime _selectedDay;
  final Map<DateTime, List<String>> _events = {
    DateTime(2024, 1, 15): ['提醒1', '提醒2'],
    DateTime(2024, 1, 20): ['提醒3'],
    DateTime(2024, 1, 25): ['提醒4', '提醒5', '提醒6'],
  };

  
  void initState() {
    super.initState();
    _focusedDay = DateTime.now();
    _selectedDay = DateTime.now();
  }

这段代码定义了EventCalendarPage这个StatefulWidget类。通过继承StatefulWidget,我们可以创建一个有状态的组件。_events是一个Map,键是DateTime对象,值是该日期的事件列表。这种数据结构使得我们可以快速查找某个日期的所有事件。在实际应用中,这个Map可以从数据库或API获取,以支持动态的事件数据。这里我们使用硬编码的数据作为示例,展示了如何在特定日期存储多个事件。

  List<String> _getEventsForDay(DateTime day) {
    return _events[day] ?? [];
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('事件日历'),
        backgroundColor: const Color(0xFF2196F3),
        foregroundColor: Colors.white,
      ),
      body: SingleChildScrollView(
        child: Column(
          children: [
            _buildEventCalendar(),
            SizedBox(height: 16.h),
            _buildEventList(),
            SizedBox(height: 20.h),
          ],
        ),
      ),
    );
  }

_getEventsForDay方法根据日期查询事件列表。这个方法使用Map的[]操作符查找指定日期的事件,如果该日期没有事件则返回空列表(??操作符)。build方法的结构与基础日历类似,使用Scaffold和Column布局。通过调用_buildEventCalendar()和_buildEventList()方法,我们将事件日历和事件列表分离为独立的组件。这种设计使代码更加模块化和易于维护。

  Widget _buildEventCalendar() {
    return Container(
      margin: EdgeInsets.all(16.w),
      padding: EdgeInsets.all(16.w),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12.r),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.1),
            blurRadius: 8,
            offset: const Offset(0, 2),
          ),
        ],
      ),
      child: TableCalendar(
        firstDay: DateTime.utc(2024, 1, 1),
        lastDay: DateTime.utc(2025, 12, 31),
        focusedDay: _focusedDay,
        selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
        onDaySelected: (selectedDay, focusedDay) {
          setState(() {
            _selectedDay = selectedDay;
            _focusedDay = focusedDay;
          });
        },
        eventLoader: _getEventsForDay,

_buildEventCalendar方法构建了事件日历的容器。Container提供了与基础日历相同的样式。TableCalendar的基本参数配置与基础日历相同,包括firstDay、lastDay、focusedDay等。关键的区别是eventLoader参数,它将_getEventsForDay方法传递给TableCalendar,使其能够自动加载并显示事件标记。当用户点击日期时,onDaySelected回调会更新_selectedDay和_focusedDay,这会触发_buildEventList方法重新构建以显示新选中日期的事件。

        calendarStyle: CalendarStyle(
          selectedDecoration: BoxDecoration(
            color: const Color(0xFF2196F3),
            shape: BoxShape.circle,
          ),
          todayDecoration: BoxDecoration(
            color: const Color(0xFF2196F3).withOpacity(0.3),
            shape: BoxShape.circle,
          ),
          markerDecoration: BoxDecoration(
            color: const Color(0xFFFFC107),
            shape: BoxShape.circle,
          ),
        ),
        headerStyle: HeaderStyle(
          formatButtonVisible: false,
          titleCentered: true,
          titleTextStyle: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold),
        ),
      ),
    );
  }

calendarStyle定义了日期单元格的样式。selectedDecoration和todayDecoration与基础日历相同。markerDecoration定义了事件标记的样式,这里使用黄色圆形标记。当某个日期有事件时,table_calendar会在该日期下方显示这个黄色标记,使用户能够快速识别有事件的日期。headerStyle定义了日历头部的样式,与基础日历相同。

  Widget _buildEventList() {
    final events = _getEventsForDay(_selectedDay);
    return Container(
      margin: EdgeInsets.symmetric(horizontal: 16.w),
      padding: EdgeInsets.all(16.w),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12.r),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.1),
            blurRadius: 8,
            offset: const Offset(0, 2),
          ),
        ],
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            '该日期的事件',
            style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold),
          ),
          SizedBox(height: 12.h),
          if (events.isEmpty)
            Text(
              '暂无事件',
              style: TextStyle(fontSize: 12.sp, color: Colors.grey),
            )
          else
            ...events.map((event) => Padding(
              padding: EdgeInsets.symmetric(vertical: 4.h),
              child: Row(
                children: [
                  Icon(Icons.event, size: 16.sp, color: const Color(0xFF2196F3)),
                  SizedBox(width: 8.w),
                  Text(
                    event,
                    style: TextStyle(fontSize: 12.sp),
                  ),
                ],
              ),
            )),
        ],
      ),
    );
  }
}

_buildEventList方法显示选中日期的所有事件。首先调用_getEventsForDay(_selectedDay)获取选中日期的事件列表。Container提供了与日历相同的样式。Column布局用于垂直排列标题和事件列表。使用if-else判断是否有事件,如果没有事件则显示"暂无事件"提示。如果有事件,使用…events.map()语法将事件列表转换为多个Row组件,每个Row显示一个事件。每个Row包含一个事件图标和事件文字,使用Padding添加垂直间距。这种设计使事件列表清晰易读。

自定义样式

table_calendar支持丰富的样式自定义。通过CalendarStyle、HeaderStyle和DaysOfWeekStyle等配置对象,我们可以精细地控制日历的外观。这些样式配置允许我们改变日期单元格的颜色、形状、文字样式等,以及头部导航栏和周标题行的样式。通过合理的样式配置,我们可以创建符合应用设计风格的日历组件。

calendarStyle: CalendarStyle(
  selectedDecoration: BoxDecoration(
    color: const Color(0xFF2196F3),
    shape: BoxShape.circle,
  ),
  todayDecoration: BoxDecoration(
    color: const Color(0xFF2196F3).withOpacity(0.3),
    shape: BoxShape.circle,
  ),
  weekendTextStyle: TextStyle(
    fontSize: 14.sp,
    color: Colors.red,
  ),
  defaultTextStyle: TextStyle(fontSize: 14.sp),
  outsideTextStyle: TextStyle(
    fontSize: 14.sp,
    color: Colors.grey[300],
  ),
),

calendarStyle定义了日期单元格的样式。selectedDecoration设置选中日期的背景为蓝色圆形,todayDecoration设置今天的背景为半透明蓝色。weekendTextStyle将周末文字设置为红色,defaultTextStyle设置普通日期的文字样式。outsideTextStyle设置当月之外日期的文字样式,通常使用灰色使其看起来不可选。这些样式配置共同构成了日历的视觉效果。

headerStyle: HeaderStyle(
  formatButtonVisible: false,
  titleCentered: true,
  titleTextStyle: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold),
  leftChevronIcon: Icon(Icons.chevron_left, size: 20.sp),
  rightChevronIcon: Icon(Icons.chevron_right, size: 20.sp),
),
daysOfWeekStyle: DaysOfWeekStyle(
  weekdayStyle: TextStyle(fontSize: 12.sp, fontWeight: FontWeight.bold),
  weekendStyle: TextStyle(fontSize: 12.sp, fontWeight: FontWeight.bold, color: Colors.red),
),

headerStyle定义了日历头部的样式。formatButtonVisible设置为false隐藏格式按钮,titleCentered设置为true使月份标题居中显示。titleTextStyle定义了月份标题的文字样式。leftChevronIcon和rightChevronIcon定义了左右导航按钮的图标。daysOfWeekStyle定义了周标题行的样式,weekdayStyle设置工作日的样式,weekendStyle设置周末的样式并使用红色文字。这些样式配置使日历的导航和周标题更加清晰易读。

屏幕适配处理

在整个日历中,我们使用flutter_screenutil库来处理屏幕适配。.w表示宽度单位,.h表示高度单位,.sp表示字体大小单位。这样可以确保在不同屏幕尺寸的设备上,日历都能正确显示。

例如,SizedBox(height: 16.h)表示高度为16个高度单位。TextStyle(fontSize: 16.sp)表示字体大小为16个字体单位。

总结

table_calendar是一个功能完整的日历组件库,它提供了日期选择、事件标记、自定义样式等功能。通过使用table_calendar,我们可以轻松地创建功能完整的日历组件。通过flutter_screenutil库,我们确保了日历在不同屏幕尺寸上的正确显示。

在视力保护提醒应用中,我们使用table_calendar来展示月度统计数据和日期选择,帮助用户更好地管理和查看提醒数据。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐