Code前端首页关于Code前端联系我们

Flutter开发:一个很酷的带有动画的列表式多选日历组件的实现

terry 2年前 (2023-09-23) 阅读数 74 #移动小程序

Flutter重构了之前用Android创建的日历组件。整体效果感觉不错,流畅度比原来还要好。这里不得不提一下官网。方法如下:

    var date = DateTime.now();
    return showDatePicker(
      context: context,
      initialDate: date,
      firstDate: date,
      lastDate: date.add(
        Duration(days: 30),
      ),
    );
复制代码

官方方法是showDatePicker实现的,支持MD和IOS风格。但据我所知它只支持单选,不支持开始和结束日期的间隔选择。体验也与我需要的效果不符。 ,所以想了想,我决定自己写一个。

一、效果图

Flutter开发:实现一个酷炫带动画的列表型多选日历组件

实现的功能和需求

  1. 画出“日”“月”“年”组件,年嵌套几个月,月嵌套几周,然后是日期
  2. 绘制日历表头和底部确认选择按钮
  3. 支持单日选择、开始日期和结束日期多选、反向选择(先选择结束日期再选择开始日期)、跨月选择、选择退出等。事件
  4. 向外界公开 CalendarList 组件。该组件是列表类型的,也就是说它是几个月的集合。

部分代码在以下各节中描述。

我们先从调用输入来分析

下面是调用日历选择组件的方法:

return CalendarList(
  firstDate: DateTime(2019, 8),
  lastDate: DateTime(2020, 8),
  selectedStartDate: DateTime(2019, 8, 28),
  selectedEndDate: DateTime(2019, 9, 2),
  onSelectFinish: (selectStartTime, selectEndTime) {
    List<DateTime> result = <DateTime>[];
    result.add(selectStartTime);
    if (selectEndTime != null) {
      result.add(selectEndTime);
    }
    Navigator.pop(context, result);
  },
);
复制代码

其中firstDate和lastDate是选择的月份列表。本例中截至2019年8月,结束时间为2020年8月,然后有两个参数选择开始日期和选择结束日期。这两个参数是给定的默认选择间隔。本例中默认选择 2019/8/28 到 2019/9/2 之间的所有日期。 ,默认选择通常会记录用户上次选择的结果。 onSelectFinish 是选择后的回调。以上参数可根据实际业务灵活设置。

底部弹出模式的日期模式

这个其实很简单。 CalendarList本身支持从底部推出。调用方法是showModalBottomSheet。代码如下:

    showModalBottomSheet(
      context: context,
      builder: (BuildContext context) {
        return Container(
          height: 600.0,
          child: FullScreenDemo(),
        );
      },
    ).then((result) {
      setState(() {
        selectResult2 = result;
      });
    });
复制代码

日历通过Container包放到FullScreenDemo中。设置一层的高度,然后通过showModalBottomSheet方法将其从底部推出。

CalendarList下拉绘图

通过上面的描述我们了解了CalendarList组件的使用方法,那么我们来看看源码中具体做了什么。在实现这个功能时,作者使用MonthView作为SliverList的构建元素。将其放置在 CustomScrollView 的 Sliver 中。我们在这里回顾一下。 Sliver的作用其实就是“胶水”,它将几个部件粘合在一起,形成滚动区域。设置如下:

CustomScrollView(
    slivers: <Widget>[
      SliverList(
        delegate: SliverChildBuilderDelegate(
          (BuildContext context, int index) {
            int month = index + monthStart;
            DateTime calendarDateTime = DateTime(yearStart, month);
            return _getMonthView(calendarDateTime);
          },
          childCount: count,
        ),
      ),
    ],
),
复制代码

在BuildContext中,通过index和monthStart如果要添加,计算日历,即8,9,10,11...这几个月。需要注意的是,如果DateTime中传入的月份参数超过12,就会自动“携带”上一年(Flutter设置的这么体贴),好吧,在_getMonthView中,我们看看返回的是一个什么样的widget。代码如下:

  Widget _getMonthView(DateTime dateTime) {
    int year = dateTime.year;
    int month = dateTime.month;
    return MonthView(
      context: context,
      year: year,
      month: month,
      padding: HORIZONTAL_PADDING,
      dateTimeStart: selectStartTime,
      dateTimeEnd: selectEndTime,
      todayColor: Colors.deepOrange,
      onSelectDayRang: (dateTime) => onSelectDayChanged(dateTime),
    );
  }
复制代码

Ok,这里是提交的月视图,设置了年、月、dateTimeStart、dateTimeEnd、今天。突出显示这些参数的颜色。接下来我们看一下MonthView中做了什么

MonthView绘图

MonthView实际上是绘制每个月有多少周,然后显示每周的7天,每行放置7(row) DayNumber 组件根据每周循环出整个月的数据。代码片段如下:

      dayRowChildren.add(
        DayNumber(
          size: widget.itemWidth,
          day: day,
          isToday: isToday,
          isDefaultSelected: isDefaultSelected,
          todayColor: widget.todayColor,
          onDayTap: (day) {
            selectedDate = DateTime(widget.year, widget.month, day);
            widget.onSelectDayRang(selectedDate);
          },
        ),
      );

      if ((day - 1 + firstWeekdayOfMonth) % DateTime.daysPerWeek == 0 ||
          day == daysInMonth) {
        dayRows.add(
          Row(
            children: List<DayNumber>.from(dayRowChildren),
          ),
        );
        dayRowChildren.clear();
      }
复制代码

这样就输出了一个日历,但是仅仅这些还不够,因为选择器还没有启动,即(单选、多选Select、反选、取消这些),必须突出显示。高亮的逻辑大致如下:

      DateTime moment = DateTime(widget.year, widget.month, day);
      final bool isToday = dateIsToday(moment);

      bool isDefaultSelected = false;
      if (widget.dateTimeStart == null &&
          widget.dateTimeEnd == null &&
          selectedDate == null) {
        isDefaultSelected = false;
      }
      if (widget.dateTimeStart == selectedDate &&
          widget.dateTimeEnd == null &&
          selectedDate?.day == day &&
          day > 0) {
        isDefaultSelected = true;
      }
      if (widget.dateTimeStart != null && widget.dateTimeEnd != null) {
        isDefaultSelected = (moment.isAtSameMomentAs(widget.dateTimeStart) ||
                    moment.isAtSameMomentAs(widget.dateTimeEnd)) ||
                moment.isAfter(widget.dateTimeStart) &&
                    moment.isBefore(widget.dateTimeEnd) &&
                    day > 0
            ? true
            : false;
      }
复制代码

上面的代码可以说是核心逻辑的一部分。它将根据 CalendarList 传入的选择间隔过滤 DateTime 时刻。如果在范围内,则选择范围。猜猜如何突出显示 DayNumber? OK,实际上,知道了高亮间隔后,就可以在DayNumber中传递默认选择isDefaultSelected了。接下来我们看看DayNumber是做什么的

DayNumber绘图

与CalendarList和MonthView相比,DayNumber是小弟,具体绘图代码如下:

  Widget _dayItem() {
    return Container(
      width: widget.size - itemMargin * 2,
      height: widget.size - itemMargin * 2,
      margin: EdgeInsets.all(itemMargin),
      alignment: Alignment.center,
      decoration: (isSelected && widget.day > 0)
          ? BoxDecoration(color: Colors.blue)
          : widget.isToday ? BoxDecoration(color: widget.todayColor) : null,
      child: Text(
        widget.day < 1 ? '' : widget.day.toString(),
        textAlign: TextAlign.center,
        style: TextStyle(
          color: (widget.isToday || isSelected) ? Colors.white : Colors.black87,
          fontSize: 15.0,
          fontWeight: FontWeight.normal,
        ),
      ),
    );
  }
复制代码

容器的背景色是,背景色容器的属性是通过BoxDecoration设置的。所选效果优先于代码中当前高亮颜色,这样就可以覆盖当前颜色。具体日期是绘制的文本。

通过上面的描述我们了解了Calendar、MonthView和DayNumber之间的关系。这几乎就是核心代码了。

接下来我们看一下单选、多选、反选、取消逻辑是如何实现的

单选、多选、反选、取消逻辑是如何实现的

代码有点长,先贴出来,然后我们来分析一下:

  // 选项处理回调
  void onSelectDayChanged(dateTime) {
    if (selectStartTime == null && selectEndTime == null) {
      selectStartTime = dateTime;
    } else if (selectStartTime != null && selectEndTime == null) {
      selectEndTime = dateTime;
      // 如果选择的开始日期和结束日期相等,则清除选项
      if (selectStartTime == selectEndTime) {
        setState(() {
          selectStartTime = null;
          selectEndTime = null;
        });
        return;
      }
      // 如果用户反选,则交换开始和结束日期
      if (selectStartTime?.isAfter(selectEndTime)) {
        DateTime temp = selectStartTime;
        selectStartTime = selectEndTime;
        selectEndTime = temp;
      }
    } else if (selectStartTime != null && selectEndTime != null) {
      selectStartTime = null;
      selectEndTime = null;
      selectStartTime = dateTime;
    }
    setState(() {
      selectStartTime;
      selectEndTime;
    });
  }
复制代码

onSelectDayChanged 其实就是用户点击 DayNumber 事件的回调。这是一个典型的代码段,其中子组件调用父组件来更改其状态。根据selectStartTime和selectEndTime是否为零进行评估,以确定用户的点击行为落在哪里。另外,通过setState重置开始日期和结束日期,以便可以“更新”月视图中的DayNumber选择区域。好了,一般的内核源码就分析到这里了。

总结一下,通过这个例子可以学到以下知识点

  1. 参数传递和参数返回的路由
  2. 父子组件之间的正向和反向通信
  3. 日期函数DateTime的使用
  4. lives in CustomScrollView应用
  5. 日历绘制方式
  6. 如何使用底部弹出组件
  7. 其他各种布局技巧和细节

可以改进的地方

  1. 支持国际化Ctom颜色
  2. 后续发布到Flutter Pub

代码地址

本例中相关代码位于

github地址:github.com/heruijun/fl…

来源❙❝:掘金

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门