Flutter 自定义日历【Flutter 专题 11】

简介: 我们使用的日历多年来一直在发展。从手写日历到印刷日历,我们现在手上都有一个数字日历,它是高度可定制的,并在我们需要提醒的准确时刻提醒我们我们的事件。我们将看到如何在 Flutter 中构建和自定义日历小部件,以便为我们的用户提供这种体验。尽管 Flutter 以日期和时间选择器的形式提供了一个日历小部件,它提供了可自定义的颜色、字体和用法,但它缺少一些功能。您可以使用它来选择日期和时间(或两者)并将其添加到您的应用程序中,但它需要与一个按钮和一个占位符相结合,可以保存选择的日期或时间。

我们使用的日历多年来一直在发展。从手写日历到印刷日历,我们现在手上都有一个数字日历,它是高度可定制的,并在我们需要提醒的准确时刻提醒我们我们的事件。


我们将看到如何在 Flutter 中构建和自定义日历小部件,以便为我们的用户提供这种体验。


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


所以,我将从 Flutter 架构提供的原生日历开始,然后转到 pub.dev 上最流行的日历小部件TableCalendar。您还可以使用许多其他流行的日历小部件,但在本教程中,我们将深入介绍。


  • Flutter 日历小部件
  • TableCalendar ()

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

为了更彻底地解释这个小部件,


首先,让我们回顾一下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,
})

关于上面的默认构造函数,大家可以参考下图,我已经指出了一些重要的属性,可以根据自己的需要进行自定义。

image.png


它是如何工作的?

下面我会先给答案加整个代码,以作展示,用户可以输入会议名称和链接,然后选择日期和时间。

整个代码

image.png

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
const Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
  runApp(MyApp());
}
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark().copyWith(
        scaffoldBackgroundColor: darkBlue,
      ),
      debugShowCheckedModeBanner: false,
      home: FlutterDatePickerExample());
  }
}
class FlutterDatePickerExample extends StatelessWidget {
  final ValueNotifier<DateTime?> dateSub = ValueNotifier(null);
  final ValueNotifier<DateTime?> longDateSub = ValueNotifier(null);
  final ValueNotifier<TimeOfDay?> timeSub = ValueNotifier(null);
  final ValueNotifier<TimeOfDay?> timeSubShort = ValueNotifier(null);
  final TextEditingController meetingName = TextEditingController();
  final TextEditingController meetingLink = TextEditingController();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Vanilla Calendar Flutter'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(14.0),
        child: SingleChildScrollView(
        child:Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            const Text(
              ' 创建会议',
              textAlign: TextAlign.center,
              style: TextStyle(fontSize: 24.0),
            ),
            const SizedBox(
              height: 20,
            ),
            buildTextField(controller: meetingName, hint: '输入会议名称'),
            const SizedBox(
              height: 20,
            ),
            buildTextField(controller: meetingLink, hint: '输入会议链接'),
            const SizedBox(
              height: 10,
            ),
            const Text(
              ' Short Date',
              textAlign: TextAlign.left,
              style: TextStyle(fontSize: 18.0),
            ),
            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: const ColorScheme.light(
                                      primary: Colors.blueGrey,
                                      onSurface: AppColors.blackCoffee,
                                    )
                                ),
                                child: child!,
                              );
                            });
                        dateSub.value = date;
                      },
                      child: buildDateTimePicker(
                          dateVal != null ? convertDate(dateVal) : ''));
                }),
            const SizedBox(
              height: 10,
            ),
            const Text(
              ' 12H Format Time',
              textAlign: TextAlign.left,
              style: TextStyle(fontSize: 18.0),
            ),
            ValueListenableBuilder<TimeOfDay?>(
                valueListenable: timeSubShort,
                builder: (context, timeVal, child) {
                  return InkWell(
                      onTap: () async {
                        TimeOfDay? time = await showTimePicker(
                          context: context,
                          builder: (context, child) {
                            return Theme(
                              data: Theme.of(context)
                              child: child!,
                            );
                          },
                          initialTime: TimeOfDay.now(),
                        );
                        timeSubShort.value = time;
                      },
                      child: buildDateTimePicker(timeVal != null
                          ? convertTime(timeVal)
                          : ''));
                }),
            const SizedBox(
              height: 20.0,
            ),
            const Text(
              ' Long Date',
              textAlign: TextAlign.left,
              style: TextStyle(fontSize: 18.0),
            ),
            ValueListenableBuilder<DateTime?>(
                valueListenable: longDateSub,
                builder: (context, dateVal, child) {
                  return InkWell(
                      onTap: () async {
                        DateTime? date = await showDatePicker(
                            context: context,
                            initialDate: DateTime.now(),
                            firstDate: DateTime.now(),
                            lastDate: DateTime(2050),
                            builder: (context, child) {
                              return Theme(
                                data: Theme.of(context),
                                child: child!,
                              );
                            });
                        longDateSub.value = date;
                      },
                      child: buildDateTimePicker(
                          dateVal != null ? longDate(dateVal) : ''));
                }),
            const SizedBox(
              height: 10,
            ),
            const Text(
              ' 24H Format Time',
              textAlign: TextAlign.left,
              style: TextStyle(fontSize: 18.0),
            ),
            ValueListenableBuilder<TimeOfDay?>(
                valueListenable: timeSub,
                builder: (context, timeVal, child) {
                  return InkWell(
                      onTap: () async {
                        TimeOfDay? time = await showTimePicker(
                          context: context,
                          builder: (context, child) {
                            return MediaQuery(
                              data: MediaQuery.of(context).copyWith(
                                  alwaysUse24HourFormat: true),
                              child: child!,
                            );
                          },
                          initialTime: TimeOfDay.now(),
                        );
                        timeSub.value = time;
                      },
                      child: buildDateTimePicker(timeVal != null
                          ? timeVal.format(context)
                          : ''));
                }),
            const SizedBox(height: 20.0,),
            ElevatedButton(onPressed: () {
              ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                content: Text('会议创建成功'),
                duration: Duration(seconds: 5),));
            }, child: const Text('提交')),
          ],
        ),
        ),
      ),
    );
  }
  String convertDate(DateTime dateTime) {
    return DateFormat('yyyy年MM月dd日').format(dateTime);
  }
  String longDate(DateTime dateTime) {
    return DateFormat('EEE, MMM d, yyy').format(dateTime);
  }
  String convertTime(TimeOfDay timeOfDay) {
    DateTime tempDate = DateFormat('hh:mm').parse(
        timeOfDay.hour.toString() + ':' + timeOfDay.minute.toString());
    var dateFormat = DateFormat('h:mm a');
    return dateFormat.format(tempDate);
  }
  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,
      ),
    );
  }
  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,
          ),
        ),
      ),
    );
  }
}
 class AppColors {
  AppColors._();
  static const Color blackCoffee = Color(0xFF352d39);
  static const Color eggPlant = Color(0xFF6d435a);
  static const Color celeste = Color(0xFFb1ede8);
  static const Color babyPowder = Color(0xFFFFFcF9);
  static const Color ultraRed = Color(0xFFFF6978);
}

第 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);
}

这是代码实现时的样子:


image.png


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


有几种自定义可能性,讨论所有这些都超出了本文的范围。因此,我会尽量做到具体,只讨论其中最重要的部分。当然,也有我亲自试验过的代码实现,以及可供参考的图像。

表日历

安装非常简单:您需要从这里复制并粘贴pubspec.yaml文件中的依赖项table_calendar


最新版本是:


table_calendar: ^3.0.2



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


  1. 设置TableCalendar小部件
  2. 根据您的应用程序需求设计日历样式
  3. 将事件添加到日历


这样您就可以轻松理解代码并知道如何成功实现它。

第 1 步:设置TableCalendar小部件

我已用SingleChildScrollView做我的父小部件,然后在Card小部件中添加了一个Column小部件,以稍微提升日历。然后,我在TableCalendar小部件中添加了小Card部件作为其子部件:

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小部件的类文件中已经给出了解释,但有时用更简单的术语更容易理解属性。我习惯于阅读所有内容,理解它,然后我尝试为我的读者简化,以便他们在实现代码之前不必遍历每一行。

image.png

第 2 步: TableCalendar的样式

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

根据应用程序的主题,您可以自定义所有内容,以便日历的外观和感觉,基本上是日历的整个 UI,与您的应用程序的 UI 相匹配。


再次将代码拆分为 3 部分:

headerStyle

// 日历标题样式
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,
 ),
),

image.png

标题下方的样式天数

在这里,您可以为周末、工作日和假期设置不同的颜色(如果您已设置):


// 日历日样式
daysOfWeekStyle: const DaysOfWeekStyle(
 // Weekend days color (Sat,Sun)
 weekendStyle: TextStyle(color: AppColors.ultraRed),
),


在上面的代码中,我为我在实现TableCalendar小部件时最初设置的周末添加了颜色。


image.png

日期样式

您可以在此处为特定的周末日期或假期日期添加颜色。此外,可以自定义当前日期和所选日期的突出显示颜色。


// 日历日期样式
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,
 ),
),



image.png


接下来的代码块是从所提供的官方文件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;
   });
 }
},

复制代码

第 3 步:将事件添加到 TableCalendar

所以我们已经完成了初始化TableCalendar并将其风格化以匹配我们的 UI。剩下的唯一事情就是将事件添加到我们的日历中,这是一项重要功能。没有它,我们的日历只是一份硬拷贝,我们保存在家里或冰箱上。


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


我创建了一个模型类,命名MyEvents并初始化了两个 String 变量eventTitleeventDescp


class MyEvents {
 final String eventTitle;
 final String eventDescp;
 MyEvents({required this.eventTitle, required this.eventDescp});
 @override
 String toString() => eventTitle;
}



在我们的 CustomCalendarTable文件中,我添加了两个TextEditingControllers、a和一个map,我们将在其中保存我们的事件列表并将其应用于TableCalandar 中的属性:


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] ?? [];
}



接下来,我向我们添加了一个 fab 按钮,Scaffold单击 fab 按钮时,将出现一个AlertDialog ,用户将在其中输入事件标题和事件描述。


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


我还添加了一个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,
       ),
     ),
   ),
 );
}



当我添加小部件eventLoader下的属性并向其TableCalendar添加_listofDayEvents方法时,一切都汇集在一起


// 需要添加这个属性来显示事件
eventLoader: _listOfDayEvents,



就是这样,我们已经成功地实现了将事件添加到日历日期并将其显示在我们的应用程序中的日历下的方法。


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


所有的基本实现保持不变。甚至属性和自定义也与我TableCalendar在本文中提到的非常相似。尽管属性的名称不同,但功能保持不变。


我试图包含尽可能多的细节,以帮助希望在其应用程序中集成日历的任何人,但正如我经常说的,发现则去实验,这一直是我的座右铭。因此,请使用代码,如果您需要更多关于这方面的信息,可以关注我。非常感谢!


最后附上完整代码

class CustomTableCalendar extends StatefulWidget {
  const CustomTableCalendar({Key? key}) : super(key: key);
  @override
  _CustomTableCalendarState createState() => _CustomTableCalendarState();
}
class _CustomTableCalendarState extends State<CustomTableCalendar> {
  final todaysDate = DateTime.now();
  var _focusedCalendarDate = DateTime.now();
  final _initialCalendarDate = DateTime(2000);
  final _lastCalendarDate = DateTime(2050);
  DateTime? selectedCalendarDate;
  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] ?? [];
  }
  _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'),
                ),
              ],
            ));
  }
  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,
          ),
        ),
      ),
    );
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Custom Calendar'),
      ),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: () => _showAddEventDialog(),
        label: const Text('Add Event'),
      ),
      body: 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(
                focusedDay: _focusedCalendarDate,
                // today's date
                firstDay: _initialCalendarDate,
                // earliest possible date
                lastDay: _lastCalendarDate,
                // latest allowed date
                calendarFormat: CalendarFormat.month,
                // default view when displayed
                // default is Saturday & Sunday but can be set to any day.
                // instead of day 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,
                // this property needs to be added if we want to show events
                eventLoader: _listOfDayEvents,
                // 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,
                  ),
                ),
                // Calendar Days Styling
                daysOfWeekStyle: const DaysOfWeekStyle(
                  // Weekend days color (Sat,Sun)
                  weekendStyle: TextStyle(color: AppColors.ultraRed),
                ),
                // 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,
                  ),
                  markerDecoration: BoxDecoration(
                      color: AppColors.ultraRed, shape: BoxShape.circle),
                ),
                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;
                    });
                  }
                },
              ),
            ),
            ..._listOfDayEvents(selectedCalendarDate!).map(
              (myEvents) => ListTile(
                leading: const Icon(
                  Icons.done,
                  color: AppColors.eggPlant,
                ),
                title: Padding(
                  padding: const EdgeInsets.only(bottom: 8.0),
                  child: Text('Event Title:   ${myEvents.eventTitle}'),
                ),
                subtitle: Text('Description:   ${myEvents.eventDescp}'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
class MyEvents {
  final String eventTitle;
  final String eventDescp;
  MyEvents({required this.eventTitle, required this.eventDescp});
  @override
  String toString() => eventTitle;
}
相关文章
|
10月前
|
Dart 前端开发
【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
346 75
【05】flutter完成注册页面完善样式bug-增加自定义可复用组件widgets-严格规划文件和目录结构-规范入口文件-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
|
10月前
|
Dart 前端开发 容器
【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
318 18
【07】flutter完成主页-完成底部菜单栏并且做自定义组件-完整短视频仿抖音上下滑动页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
|
UED 开发者 容器
Flutter&鸿蒙next 的 Sliver 实现自定义滚动效果
Flutter 提供了强大的滚动组件,如 ListView 和 GridView,但当需要更复杂的滚动效果时,Sliver 组件是一个强大的工具。本文介绍了如何使用 Sliver 实现自定义滚动效果,包括 SliverAppBar、SliverList 等常用组件的使用方法,以及通过 CustomScrollView 组合多个 Sliver 组件实现复杂布局的示例。通过具体代码示例,展示了如何实现带有可伸缩 AppBar 和可滚动列表的页面。
426 1
Flutter 自定义组件继承与调用的高级使用方式
本文深入探讨了 Flutter 中自定义组件的高级使用方式,包括创建基本自定义组件、继承现有组件、使用 Mixins 和组合模式等。通过这些方法,您可以构建灵活、可重用且易于维护的 UI 组件,从而提升开发效率和代码质量。
355 1
|
前端开发 开发者
深入探索 Flutter 鸿蒙版的画笔使用与高级自定义动画
本文深入探讨了 Flutter 中的绘图功能,重点介绍了 CustomPainter 和 Canvas 的使用方法。通过示例代码,详细讲解了如何绘制自定义图形、设置 Paint 对象的属性以及实现高级自定义动画。内容涵盖基本绘图、动画基础、渐变动画和路径动画,帮助读者掌握 Flutter 绘图与动画的核心技巧。
266 1
|
Dart UED 开发者
Flutter&鸿蒙next中的按钮封装:自定义样式与交互
在Flutter应用开发中,按钮是用户界面的重要组成部分。Flutter提供了多种内置按钮组件,但有时这些样式无法满足特定设计需求。因此,封装一个自定义按钮组件变得尤为重要。自定义按钮组件可以确保应用中所有按钮的一致性、可维护性和可扩展性,同时提供更高的灵活性,支持自定义颜色、形状和点击事件。本文介绍了如何创建一个名为CustomButton的自定义按钮组件,并详细说明了其样式、形状、颜色和点击事件的处理方法。
257 1
|
Dart 搜索推荐 API
Flutter & 鸿蒙next版本:自定义对话框与表单验证的动态反馈与错误处理
在现代移动应用开发中,用户体验至关重要。本文探讨了如何在 Flutter 与鸿蒙操作系统(HarmonyOS)中创建自定义对话框,并结合表单验证实现动态反馈与错误处理,提升用户体验。通过自定义对话框和表单验证,开发者可以提供更加丰富和友好的交互体验,同时利用鸿蒙next版本拓展应用的受众范围。
254 1
|
前端开发
Flutter快速实现自定义折线图,支持数据改变过渡动画
Flutter快速实现自定义折线图,支持数据改变过渡动画
331 4
Flutter快速实现自定义折线图,支持数据改变过渡动画
|
前端开发 搜索推荐
Flutter中自定义气泡框效果的实现
Flutter中自定义气泡框效果的实现
434 3
|
开发者 监控 开发工具
如何将JSF应用送上云端?揭秘在Google Cloud Platform上部署JSF应用的神秘步骤
【8月更文挑战第31天】本文详细介绍如何在Google Cloud Platform (GCP) 上部署JavaServer Faces (JSF) 应用。首先,确保已准备好JSF应用并通过Maven构建WAR包。接着,使用Google Cloud SDK登录并配置GCP环境。然后,创建`app.yaml`文件以配置Google App Engine,并使用`gcloud app deploy`命令完成部署。最后,通过`gcloud app browse`访问应用,并利用GCP的监控和日志服务进行管理和故障排查。整个过程简单高效,帮助开发者轻松部署和管理JSF应用。
155 0