如何在Flutter中建立一个自定义的日历

2,019 阅读9分钟

多年来,我们使用的日历已经发生了变化。从手写的日历到打印的日历,我们现在都有一个数字日历在手,它是非常可定制的,并在我们想要提醒的精确时刻提醒我们的事件。

我们要看看如何在Flutter中建立和定制日历小部件,以便为我们的用户提供这种体验。

尽管Flutter以日期和时间选择器的形式提供了一个日历小部件,提供了可定制的颜色、字体和用法,但它缺少一些功能。你可以用它来挑选日期和时间(或两者)并将其添加到你的应用程序中,但它需要与一个按钮和一个可以保存所选日期或时间的占位符相结合。

因此,我将从Flutter架构提供的原生日历开始,然后转到 [TableCalendar](https://pub.dev/packages/table_calendar),这是pub.dev上最受欢迎的日历小部件。也有许多其他流行的日历部件,你可以使用,但在本教程中,我们将深入介绍一个。

Flutter日历小部件(日期选择器和时间选择器)

为了更彻底地解释这个部件,我创建了一个在线会议的单屏幕应用程序。用户可以输入会议名称和链接,然后选择一个日期和时间。

Flutter Calendar Widget DemoFlutter Calendar Widget Demo Form Filled

首先,我们来看看showDatePicker 默认构造函数。

showDatePicker({
// it requires a context
  required BuildContext context,  
// when datePicker is displayed, it will show month of the current date
  required DateTime initialDate,  
// earliest possible date to be displayed (eg: 2000)
  required DateTime firstDate,
// latest allowed date to be displayed (eg: 2050)
  required DateTime lastDate,
// it represents TODAY and it will be highlighted
  DateTime? currentDate,
 // either by input or selected, defaults to calendar mode.
  DatePickerEntryMode initialEntryMode = DatePickerEntryMode.calendar or input,
// restricts user to select date from range to dates.
  SelectableDayPredicate? selectableDayPredicate,
// text that is displayed at the top of the datePicker
  String? helpText,
// text that is displayed on cancel button
  String? cancelText,
// text that is displayed on confirm button
  String? confirmText,
// use builder function to customise the datePicker  
  TransitionBuilder? Builder,
// option to display datePicker in year or day mode. Defaults to day
  DatePickerMode initialDatePickerMode = DatePickerMode.day or year,
// error message displayed when user hasn't entered date in proper format
  String? errorFormatText,
// error message displayed when date is not selectable
  String? errorInvalidText,
// hint message displayed to prompt user to enter date according to the format mentioned (eg: dd/mm/yyyy)
  String? fieldHintText,
// label message displayed for what the user is entering date for (eg: birthdate)
  String? fieldLabelText,
})

关于上述默认构造函数,你可以参考下面的图片,我指出了一些重要的属性,可以根据你的需要进行定制。

Flutter Calendar Properties

它是如何工作的?

我不打算在这里公布整个代码,而只是展示它的实现和解释。showDatePicker的其余代码可以在这里找到,供你试验。

第1步:实现一个ValueNotifier

我已经实现了一个ValueNotifier ,它将保存文本字段中的日期。

final ValueNotifier<DateTime?> dateSub = ValueNotifier(null);

第2步:创建一个datePicker 的对话框

有了ValueListenerBuilder 和一个DateTime 的实例,并在InkWell 小组件的帮助下,当我们点击textField ,一个datePicker 的对话框就会弹出来。当用户点击所需的日期时,它将显示在textField

ValueListenableBuilder<DateTime?>(
   valueListenable: dateSub,
   builder: (context, dateVal, child) {
     return InkWell(
         onTap: () async {
           DateTime? date = await showDatePicker(
               context: context,
               initialDate: DateTime.now(),
               firstDate: DateTime.now(),
               lastDate: DateTime(2050),
               currentDate: DateTime.now(),
               initialEntryMode: DatePickerEntryMode.calendar,
               initialDatePickerMode: DatePickerMode.day,
               builder: (context, child) {
                 return Theme(
                   data: Theme.of(context).copyWith(
                       colorScheme:  ColorScheme.fromSwatch(
                         primarySwatch: Colors.blueGrey,
                         accentColor: AppColors.blackCoffee,
                         backgroundColor: Colors.lightBlue,
                         cardColor: Colors.white,
                       )
                   ),
                   child: child!,
                 );
               });
           dateSub.value = date;
         },
         child: buildDateTimePicker(
             dateVal != null ? convertDate(dateVal) : ''));
   }),

buildDateTimePicker 无非是一个带有自定义边框和日历图标作为尾部图标的listTile

Widget buildDateTimePicker(String data) {
 return ListTile(
   shape: RoundedRectangleBorder(
     borderRadius: BorderRadius.circular(10.0),
     side: const BorderSide(color: AppColors.eggPlant, width: 1.5),
   ),
   title: Text(data),
   trailing: const Icon(
     Icons.calendar_today,
     color: AppColors.eggPlant,
   ),
 );
}

我们也有一个字符串方法来将日期转换成所需的格式。

String convertDate(DateTime dateTime) {
 return DateFormat('dd/MM/yyyy').format(dateTime);
}

这就是代码实现后的样子。

Flutter Calendar Date and Time Picker

现在,让我们回到我之前讨论的TableCalendar ,我们将如何实现它,以及我们将如何定制它以满足应用程序的需求。

有几种定制的可能性,讨论它们都会超出本文的范围。所以我将尽量具体,只讨论其中最重要的部分。当然,也有我个人实验过的代码实现,以及可以参考的图片。

TableCalendar

安装非常简单:你需要在你的pubspec.yaml 文件中复制并粘贴依赖性,以便从这里table_calendar

最新的版本是。

table_calendar: ^3.0.2

现在,我将把它的构造函数分成三个部分。

  1. 设置TableCalendar 小部件
  2. 为你的应用需求设计日历的样式
  3. 将事件添加到日历中

这样你就可以轻松地理解代码,也知道如何成功地实现它。

第1步:设置TableCalendar 小部件

我使用了SingleChildScrollView 作为我的父部件,然后在Column 部件内添加了一个Card 部件,以便给日历增加一点高度。然后,我在Card 小组件内添加了TableCalendar 小组件作为其子件。

SingleChildScrollView(
 child: Column(
   children: [
     Card(
       margin: const EdgeInsets.all(8.0),
       elevation: 5.0,
       shape: const RoundedRectangleBorder(
         borderRadius: BorderRadius.all(
           Radius.circular(10),
         ),
         side: BorderSide( color: AppColors.blackCoffee, width: 2.0),
       ),
       child: TableCalendar(
          // today's date
         focusedDay: _focusedCalendarDate,
         // earliest possible date
         firstDay: _initialCalendarDate,
         // latest allowed date
         lastDay: _lastCalendarDate, 
         // default view when displayed
         calendarFormat: CalendarFormat.month, 
         // default is Saturday & Sunday but can be set to any day.
         // instead of day, a number can be mentioned as well.
         weekendDays: const [DateTime.sunday, 6],
         // default is Sunday but can be changed according to locale
         startingDayOfWeek: StartingDayOfWeek.monday,
        // height between the day row and 1st date row, default is 16.0
         daysOfWeekHeight: 40.0,
         // height between the date rows, default is 52.0
         rowHeight: 60.0,

上面的代码是在设置日历,它将出现在移动屏幕上,有一些默认值,还有一些根据地区设置的定制。我在每个属性之前都添加了注释,以了解它的作用。

我知道在TableCalendar widget的类文件中已经给出了解释,但有时用更简单的术语来理解属性会更容易。我有一个习惯,就是阅读所有的东西,理解它,然后我试着为我的读者简化,这样他们就不必在实现代码之前翻阅每一行。

Flutter Custom Calendar Styling

第二步:造型TableCalendar

好的,所以在设计表格日历的时候,又有3个部分。首先是标题,在这里我们有月份的名称和一个在周视图和月视图之间切换的按钮。左右箭头在月份之间滚动。

根据应用程序的主题,你可以定制一切,使日历的外观和感觉,基本上日历的整个用户界面,与你的应用程序的用户界面相匹配。

再次将代码分成三部分。

headerStyle

// Calendar Header Styling
headerStyle: const HeaderStyle(
 titleTextStyle:
     TextStyle(color: AppColors.babyPowder, fontSize: 20.0),
 decoration: BoxDecoration(
     color: AppColors.eggPlant,
     borderRadius: BorderRadius.only(
         topLeft: Radius.circular(10),
         topRight: Radius.circular(10))),
 formatButtonTextStyle:
     TextStyle(color: AppColors.ultraRed, fontSize: 16.0),
 formatButtonDecoration: BoxDecoration(
   color: AppColors.babyPowder,
   borderRadius: BorderRadius.all(
     Radius.circular(5.0),
   ), ),
 leftChevronIcon: Icon(
   Icons.chevron_left,
   color: AppColors.babyPowder,
   size: 28,
 ),
 rightChevronIcon: Icon(
   Icons.chevron_right,
   color: AppColors.babyPowder,
   size: 28,
 ),
),

Flutter Custom Calendar Header Styling

头部以下的日子的样式

在这里,你可以为周末、工作日设置不同的颜色,如果你设置了假日,也可以为假日设置不同的颜色。

// Calendar Days Styling
daysOfWeekStyle: const DaysOfWeekStyle(
 // Weekend days color (Sat,Sun)
 weekendStyle: TextStyle(color: AppColors.ultraRed),
),

在上面的代码中,我为周末的日子添加了颜色,这是我在实现TableCalendar widget时最初设置的。

Flutter Custom Calendar Days Styling

设计日期的样式

在这里你可以为特定的周末日期或假日日期添加颜色。此外,还可以自定义当前日期和选定日期的高亮颜色。

// Calendar Dates styling
calendarStyle: const CalendarStyle(
 // Weekend dates color (Sat & Sun Column)
 weekendTextStyle: TextStyle(color: AppColors.ultraRed),
 // highlighted color for today
 todayDecoration: BoxDecoration(
   color: AppColors.eggPlant,
   shape: BoxShape.circle,
 ),
 // highlighted color for selected day
 selectedDecoration: BoxDecoration(
   color: AppColors.blackCoffee,
   shape: BoxShape.circle,
 ),
),

Flutter Dates Styling Properties

接下来的代码块来自 TableCalender 提供的官方文档。它是实现选定日期的默认方式。这段代码根据上述自定义的颜色突出了当前日期和选定日期。没有比这更好的方法了,这也是TableCalendar 的建议。

selectedDayPredicate: (currentSelectedDate) {
 // as per the documentation 'selectedDayPredicate' needs to determine current selected day.
 return (isSameDay(
     _selectedCalendarDate!, currentSelectedDate));
},
onDaySelected: (selectedDay, focusedDay) {
 // as per the documentation
 if (!isSameDay(_selectedCalendarDate, selectedDay)) {
   setState(() {
     _selectedCalendarDate = selectedDay;
     _focusedCalendarDate = focusedDay;
   });
 }
},

第三步:将事件添加到TableCalendar

因此,我们已经完成了对TableCalendar 的初始化,并将其风格化以符合我们的用户界面。剩下的事情就是向我们的日历添加事件,这是一个重要的功能。如果没有它,我们的日历只是一个硬拷贝,我们把它放在我们的房子里或冰箱上。

然而,我们中的许多人倾向于在日历上贴一张便利贴,以表明整个月、周、甚至一天的关键事件。在我们的手机上,我们有能力将提醒或事件添加到我们的默认日历应用程序中。

我已经创建了一个名为MyEvents 的模型类,并初始化了两个字符串变量eventTitleeventDescp (描述)。

class MyEvents {
 final String eventTitle;
 final String eventDescp;

 MyEvents({required this.eventTitle, required this.eventDescp});

 @override
 String toString() => eventTitle;
}

在我们的 CustomCalendarTable Dart文件中,我已经添加了两个TextEditingController,一个Map ,和一个方法,我们将在其中保存我们的事件列表,并将其应用到TableCalandar中的eventLoader 属性。

final titleController = TextEditingController();
final descpController = TextEditingController();

late Map<DateTime, List<MyEvents>> mySelectedEvents;

@override
void initState() {
 selectedCalendarDate = _focusedCalendarDate;
 mySelectedEvents = {};
 super.initState();
}

@override
void dispose() {
 titleController.dispose();
 descpController.dispose();
 super.dispose();
}

List<MyEvents> _listOfDayEvents(DateTime dateTime) {
 return mySelectedEvents[dateTime] ?? [];
}

接下来,我在我们的Scaffold 中添加了一个fab按钮,点击fab按钮后,会出现一个AlertDialog ,用户将在这里输入事件的标题和事件的描述。

在点击AlertDialog 内的Add 按钮后,一个事件将被添加到日历下,在添加事件的日期上将看到一个小的彩色圆点。

我还添加了一个SnackBar ,以防用户没有在标题文本字段或描述文本字段中输入任何东西。一个SnackBar ,上面会弹出一条信息,要求输入标题和描述。

如果用户输入了标题和描述,在setState 方法中,它正在检查所选事件的列表是否为空,然后我们将标题和描述添加到MyEvents 模型类中,并创建一个MyEvents 的列表。

一旦有事件被添加,我们就清除Controllers并关闭AlertDialog

_showAddEventDialog() async {
 await showDialog(
     context: context,
     builder: (context) => AlertDialog(
           title: const Text('New Event'),
           content: Column(
             crossAxisAlignment: CrossAxisAlignment.stretch,
             mainAxisSize: MainAxisSize.min,
             children: [
               buildTextField(
                   controller: titleController, hint: 'Enter Title'),
               const SizedBox(
                 height: 20.0,
               ),
               buildTextField(
                   controller: descpController, hint: 'Enter Description'),
             ],           ),
           actions: [
             TextButton(
               onPressed: () => Navigator.pop(context),
               child: const Text('Cancel'),),
             TextButton(
               onPressed: () {
                 if (titleController.text.isEmpty &&
                     descpController.text.isEmpty) {
                   ScaffoldMessenger.of(context).showSnackBar(
                     const SnackBar(
                       content: Text('Please enter title & description'),
                       duration: Duration(seconds: 3),
                     ), );
                   //Navigator.pop(context);
                   return;
                 } else {
                   setState(() {
                if (mySelectedEvents[selectedCalendarDate] != null) {
                     mySelectedEvents[selectedCalendarDate]?.add(MyEvents(
                           eventTitle: titleController.text,
                           eventDescp: descpController.text));
                     } else {
                       mySelectedEvents[selectedCalendarDate!] = [
                         MyEvents(
                             eventTitle: titleController.text,
                             eventDescp: descpController.text)
                       ]; } });

                   titleController.clear();
                   descpController.clear();

                   Navigator.pop(context);
                   return;
                 }
               },
               child: const Text('Add'),
             ),
           ],
         ));}

我已经建立了一个自定义的文本字段,我已经在AlertDialog 里面初始化了。

Widget buildTextField(
   {String? hint, required TextEditingController controller}) {
 return TextField(
   controller: controller,
   textCapitalization: TextCapitalization.words,
   decoration: InputDecoration(
     labelText: hint ?? '',
     focusedBorder: OutlineInputBorder(
       borderSide: const BorderSide(color: AppColors.eggPlant, width: 1.5),
       borderRadius: BorderRadius.circular(
         10.0,
       ),
     ),
     enabledBorder: OutlineInputBorder(
       borderSide: const BorderSide(color: AppColors.eggPlant, width: 1.5),
       borderRadius: BorderRadius.circular(
         10.0,
       ),
     ),
   ),
 );
}

当我添加TableCalendar widget下面的eventLoader 属性并向其添加_listofDayEvents 方法时,一切都会变得很好。

// this property needs to be added to show events
eventLoader: _listOfDayEvents,

Final Flutter Calendar Demo

就这样,我们已经成功地实现了向日历日期添加事件的方法,并在我们的应用程序中的日历下显示。你可以在这里看一下整个代码

正如我在本文前面提到的,有一些优秀的日历库可用,如flutter_calendar_carousel和syncfusion_flutter_calendar。

所有这些的基本实现都是一样的。甚至属性和自定义也与我在本文中提到的TableCalendar 非常相似。即使属性的名称不同,功能也是一样的。

我试图包括尽可能多的细节,以帮助那些希望在他们的应用程序中整合日历的人,但正如我经常说的,发现需要实验,这一直是我的座右铭。所以,玩玩这些代码吧,如果你需要更多的信息,你可以随时参考pub.dev网站上的官方文档。

非常感谢!

The postHow to build a custom calendar in Flutterappeared first onLogRocket Blog.