Flutter日历组件插件calendar_components的使用

Flutter日历组件插件calendar_components的使用

开始使用

首先,在你的pubspec.yaml文件中添加以下依赖:

dependencies:
  calendar_components: ^0.1.0

然后运行flutter pub get以安装该包。

可用组件

该插件提供了几个简单的组件,帮助你构建自己的日历。这些组件允许你自己处理设计和状态管理。下面是一些主要组件的介绍及示例代码。

日历头部组件 CalendarComponentHeader

用于展示日历顶部的头部信息。通常包含一周七天的名称。

CalendarComponentHeader(
  itemBuilder: (day) => Text(day.name),
)

基础日历网格 CalendarComponentDayGrid

用于展示基本的日历网格。根据是否需要处理溢出日期,可以选择不同的构造函数。

处理溢出日期

final currentMonth = DateTime.now();

CalendarComponentDayGrid.overflow(
  currentMonth: currentMonth,
  itemBuilder: (context, date, index) {
    return Text('${date.day}');
  },
)

不处理溢出日期

CalendarComponentDayGrid.noOverflow(
  currentMonth: currentMonth,
  startDate: currentMonth.copyWith(day: 1),
  endDate: currentMonth.lastDayOfCurrentMonth(),
  itemBuilder: (context, date, index) {
    return Text('${date.day}');
  },
);

单选或多选日历网格

允许用户选择一个或多个日期。这些是无状态小部件,你需要自己管理选择的状态。

单选日历网格

final currentMonth = DateTime.now();

CalendarComponentSingleSelectableDayGrid.overflow(
  selectedDate: ...,
  currentMonth: currentMonth,
  itemBuilder: (context, date, isSelected, index) {
    return GestureDetector(
      onTap: () => setState(() {
        // 更新选择状态
      }),
      child: Text('${date.day}'),
    );
  },
)

多选日历网格

CalendarComponentMultipleSelectableDayGrid.noOverflow(
  selectedDates: [...],
  currentMonth: currentMonth,
  startDate: currentMonth.copyWith(day: 1),
  endDate: currentMonth.lastDayOfCurrentMonth(),
  itemBuilder: (context, date, isSelected, index) {
    return GestureDetector(
      onTap: () => setState(() {
        // 更新选择状态
      }),
      child: Text('${date.day}'),
    );
  },
);

区间选择日历网格 CalendarComponentRangedSelectableDayGrid

允许用户选择一个日期区间。这也是一个无状态小部件,你需要自己管理选择的日期范围。

CalendarComponentRangedSelectableDayGrid.overflow(
  selectedStartDate: ...,
  selectedEndDate: ...,
  currentMonth: DateTime.now(),
  itemBuilder: (context, date, selectedState, index) {
    return GestureDetector(
      onTap: () => setState(() {
        // 更新选择状态
      }),
      child: Text('${date.day}'),
    );
  },
)

完整示例代码

以下是一个完整的示例代码,展示了如何使用这些组件来创建一个日历应用。

import 'package:calendar_components/calendar_components.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';

void main() {
  runApp(const MyApp());
}

extension on GregorianCalendarDay {
  String get shorterName {
    switch (this) {
      case GregorianCalendarDay.sunday:
        return 'Sun';
      case GregorianCalendarDay.monday:
        return 'Mon';
      case GregorianCalendarDay.tuesday:
        return 'Tue';
      case GregorianCalendarDay.wednesday:
        return 'Wed';
      case GregorianCalendarDay.thursday:
        return 'Thu';
      case GregorianCalendarDay.friday:
        return 'Fri';
      case GregorianCalendarDay.saturday:
        return 'Sat';
    }
  }
}

extension on DateTime {
  bool isSameMonthAs(DateTime other) {
    return year == other.year && month == other.month;
  }
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      localizationsDelegates: [
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: [
        Locale('en', 'GB'),
      ],
      home: MyHomePage(),
    );
  }
}

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

  [@override](/user/override)
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  DateTime? _selectedStartDate;
  DateTime? _selectedEndDate;

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: LayoutBuilder(builder: (context, constraints) {
          const horizontalPadding = 18.0;
          final itemExtent = (constraints.maxWidth - 2 * horizontalPadding) /
              DateTime.daysPerWeek;

          final startDate = DateTime.now();

          return Column(
            children: [
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: horizontalPadding),
                child: CalendarComponentHeader(
                  itemBuilder: (context, day) => Container(
                    padding: const EdgeInsets.symmetric(vertical: 10),
                    width: itemExtent,
                    child: Text(
                      day.shorterName,
                      textAlign: TextAlign.center,
                      style: const TextStyle(color: Color(0xff717171)),
                    ),
                  ),
                ),
              ),
              Expanded(
                child: ListView.separated(
                  itemBuilder: (context, index) {
                    final month = DateTime(startDate.year, startDate.month + index);

                    return PartialCalendarDayGrid(
                      itemExtent: itemExtent,
                      horizontalPadding: horizontalPadding,
                      currentMonth: month,
                      startDate: month.copyWith(
                          day: month.isSameMonthAs(startDate) ? 18 : 1),
                      endDate: month.lastDateOfCurrentMonth(),
                      selectedStartDate: _selectedStartDate,
                      selectedEndDate: _selectedEndDate,
                      isDateAvailable: (date) => ![
                        for (var i = 18; i <= 24; i++)
                          startDate.copyWith(day: i).toMidnight()
                      ].contains(date),
                      onDateTapped: _onDateTapped,
                    );
                  },
                  separatorBuilder: (_, __) => const SizedBox(height: 16),
                  itemCount: 12,
                ),
              ),
            ],
          );
        }),
      ),
    );
  }

  void _onDateTapped(DateTime date) {
    setState(() {
      final selectedStartDate = _selectedStartDate;
      final selectedEndDate = _selectedEndDate;

      if (selectedStartDate == null) {
        _selectedStartDate = date;
        return;
      }

      if (selectedEndDate == null) {
        // 允许取消选择同一日期
        if (date.isAtSameMomentAs(selectedStartDate)) {
          _selectedStartDate = null;
          _selectedEndDate = null;
          return;
        } else {
          _selectedEndDate = date;
          return;
        }
      }

      if (date.isAtSameMomentAs(selectedStartDate)) {
        _selectedStartDate = date;
        _selectedEndDate = null;
        return;
      }

      if (date.isAtSameMomentAs(selectedEndDate)) {
        _selectedStartDate = date;
        _selectedEndDate = null;
        return;
      }

      _selectedEndDate = null;
      _selectedStartDate = date;
      return;
    });
  }
}

class PartialCalendarDayGrid extends StatelessWidget {
  const PartialCalendarDayGrid({
    super.key,
    double? horizontalPadding,
    required this.itemExtent,
    required this.currentMonth,
    required this.startDate,
    required this.selectedStartDate,
    required this.selectedEndDate,
    required this.endDate,
    this.isDateAvailable,
    this.onDateTapped,
  }) : horizontalPadding = horizontalPadding ?? 0;

  final double horizontalPadding;
  final double itemExtent;
  final DateTime currentMonth;
  final DateTime startDate;
  final DateTime endDate;
  final DateTime? selectedStartDate;
  final DateTime? selectedEndDate;
  final bool Function(DateTime)? isDateAvailable;
  final ValueChanged<DateTime>? onDateTapped;

  static final months = [
    'January',
    'February',
    'March',
    'April',
    'May',
    'June',
    'July',
    'August',
    'September',
    'October',
    'November',
    'December',
  ];

  [@override](/user/override)
  Widget build(BuildContext context) {
    final month = months[currentMonth.month - 1];
    final year = currentMonth.year;

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Padding(
          padding: EdgeInsets.only(top: 16, bottom: 12, left: horizontalPadding),
          child: Text(
            '$month $year',
            style: const TextStyle(
              fontWeight: FontWeight.w600,
              fontSize: 16,
            ),
          ),
        ),
        Padding(
          padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
          child: CalendarComponentRangedSelectableDayGrid.noOverflow(
            currentMonth: currentMonth,
            startDate: startDate,
            endDate: endDate,
            selectedStartDate: selectedStartDate,
            selectedEndDate: selectedEndDate,
            itemBuilder: _itemBuilder,
          ),
        ),
      ],
    );
  }

  Widget _itemBuilder(
    BuildContext context,
    DateTime date,
    SelectedDateRangeState? selectedState,
    int index,
  ) {
    final isDateAvailable = this.isDateAvailable?.call(date) ?? true;
    final onDateTapped = isDateAvailable ? this.onDateTapped : null;

    return PartialCalendarDayGridItem(
      date,
      currentMonth: currentMonth,
      startDate: startDate,
      endDate: endDate,
      itemExtent: itemExtent,
      itemIndex: index,
      selectedState: selectedState,
      onDateTapped: onDateTapped,
    );
  }
}

class PartialCalendarDayGridItem extends StatelessWidget {
  const PartialCalendarDayGridItem(
    this.date, {
    super.key,
    required this.currentMonth,
    required this.startDate,
    required this.endDate,
    required this.itemExtent,
    required this.itemIndex,
    required this.selectedState,
    this.onDateTapped,
  });

  final DateTime date;
  final DateTime currentMonth;
  final DateTime startDate;
  final DateTime endDate;
  final double itemExtent;
  final int itemIndex;
  final SelectedDateRangeState? selectedState;
  final ValueChanged<DateTime>? onDateTapped;

  [@override](/user/override)
  Widget build(BuildContext context) {
    if (!date.isSameMonthAs(currentMonth) ||
        date.isBefore(startDate) ||
        date.isAfter(endDate)) {
      return SizedBox(width: itemExtent, height: itemExtent);
    }

    final selectedDecoration = BoxDecoration(
      color: Colors.black,
      borderRadius: BorderRadius.circular(itemExtent / 2),
    );

    const inBetweenColor = Color(0xffe8e8e8);
    const disabledColor = Color(0xffd8d8d8);

    final inBetweenDecoration = BoxDecoration(
      color: inBetweenColor,
      gradient: () {
        final isLeftMostDate = itemIndex % DateTime.daysPerWeek == 0;
        final isRightMostDate = (itemIndex + 1) % DateTime.daysPerWeek == 0;
        final isDateFirstOfTheMonth =
            date.isAtSameMomentAs(currentMonth.copyWith(day: 1));
        final isDateLastOfTheMonth =
            date.isAtSameMomentAs(currentMonth.lastDateOfCurrentMonth());

        if (!isLeftMostDate &&
            !isRightMostDate &&
            !isDateFirstOfTheMonth &&
            !isDateLastOfTheMonth) {
          return null;
        }

        return LinearGradient(
          colors: [
            inBetweenColor,
            Theme.of(context).scaffoldBackgroundColor,
          ],
          stops: const [0.65, 1],
          begin: isLeftMostDate || isDateFirstOfTheMonth
              ? Alignment.centerRight
              : Alignment.centerLeft,
          end: isRightMostDate || isDateLastOfTheMonth
              ? Alignment.centerRight
              : Alignment.centerLeft,
        );
      }(),
    );

    const margin = EdgeInsets.all(4);
    final inBetweenMargin = margin.copyWith(left: 0, right: 0);
    final connectedEndMargin = margin.copyWith(
      left: selectedState == SelectedDateRangeState.startDateConnected
          ? itemExtent / 2
          : 0,
      right: selectedState == SelectedDateRangeState.endDateConnected
          ? itemExtent / 2
          : 0,
    );

    final isValidDateRange = selectedState?.isValidDateRange ?? false;
    final isSelected = selectedState?.isSelected ?? false;

    return GestureDetector(
      behavior: HitTestBehavior.opaque,
      onTap: () => onDateTapped?.call(date),
      child: SizedBox(
        width: itemExtent,
        height: itemExtent,
        child: Stack(
          children: [
            if (isValidDateRange)
              Container(
                decoration: inBetweenDecoration,
                margin: selectedState != null
                    ? connectedEndMargin
                    : inBetweenMargin,
              ),
            Container(
              margin: margin,
              decoration: isSelected ? selectedDecoration : null,
              child: Center(
                child: Text(
                  date.day.toString(),
                  style: TextStyle(
                    color: onDateTapped == null
                        ? disabledColor
                        : isSelected
                            ? Colors.white
                            : Colors.black,
                    fontWeight: FontWeight.w700,
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

更多关于Flutter日历组件插件calendar_components的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter日历组件插件calendar_components的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


在 Flutter 中,calendar_components 是一个用于构建日历相关 UI 的插件。它提供了丰富的组件和功能,可以帮助开发者快速实现日历视图、事件管理、日程安排等功能。以下是如何使用 calendar_components 插件的基本步骤。

1. 添加依赖

首先,你需要在 pubspec.yaml 文件中添加 calendar_components 插件的依赖:

dependencies:
  flutter:
    sdk: flutter
  calendar_components: ^0.0.1  # 请使用最新版本

然后,运行 flutter pub get 来获取依赖。

2. 导入包

在你的 Dart 文件中,导入 calendar_components 包:

import 'package:calendar_components/calendar_components.dart';

3. 使用日历组件

calendar_components 提供了多种日历相关的组件,例如 CalendarViewEventView 等。以下是一个简单的示例,展示如何使用 CalendarView

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

class CalendarScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Calendar Example'),
      ),
      body: CalendarView(
        initialDate: DateTime.now(),
        onDateSelected: (DateTime date) {
          print("Selected date: $date");
        },
        events: [
          Event(
            title: 'Meeting',
            date: DateTime.now(),
            description: 'Team meeting at 10 AM',
          ),
          Event(
            title: 'Lunch',
            date: DateTime.now().add(Duration(days: 1)),
            description: 'Lunch with John',
          ),
        ],
      ),
    );
  }
}

4. 自定义日历样式

你可以通过 CalendarView 的各种属性来自定义日历的外观和行为。例如,可以设置日历的初始日期、事件列表、日期选择回调等。

CalendarView(
  initialDate: DateTime.now(),
  onDateSelected: (DateTime date) {
    print("Selected date: $date");
  },
  events: [
    Event(
      title: 'Meeting',
      date: DateTime.now(),
      description: 'Team meeting at 10 AM',
    ),
    Event(
      title: 'Lunch',
      date: DateTime.now().add(Duration(days: 1)),
      description: 'Lunch with John',
    ),
  ],
  calendarStyle: CalendarStyle(
    selectedColor: Colors.blue,
    todayColor: Colors.green,
    eventMarkerColor: Colors.red,
  ),
)

5. 处理事件

你可以通过 Event 类来创建和管理事件。每个事件可以包含标题、日期、描述等信息。你可以在日历中显示这些事件,并在用户选择某个日期时触发相应的回调。

Event(
  title: 'Meeting',
  date: DateTime.now(),
  description: 'Team meeting at 10 AM',
)

6. 运行项目

完成上述步骤后,你可以运行你的 Flutter 项目,查看日历组件的效果。

flutter run
回到顶部