Flutter开发:一个很酷的带有动画的列表式多选日历组件的实现
Flutter重构了之前用Android创建的日历组件。整体效果感觉不错,流畅度比原来还要好。这里不得不提一下官网。方法如下:
var date = DateTime.now();
return showDatePicker(
context: context,
initialDate: date,
firstDate: date,
lastDate: date.add(
Duration(days: 30),
),
);
复制代码
官方方法是showDatePicker实现的,支持MD和IOS风格。但据我所知它只支持单选,不支持开始和结束日期的间隔选择。体验也与我需要的效果不符。 ,所以想了想,我决定自己写一个。
一、效果图

实现的功能和需求
- 画出“日”“月”“年”组件,年嵌套几个月,月嵌套几周,然后是日期
- 绘制日历表头和底部确认选择按钮
- 支持单日选择、开始日期和结束日期多选、反向选择(先选择结束日期再选择开始日期)、跨月选择、选择退出等。事件
- 向外界公开 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选择区域。好了,一般的内核源码就分析到这里了。
总结一下,通过这个例子可以学到以下知识点
- 参数传递和参数返回的路由
- 父子组件之间的正向和反向通信
- 日期函数DateTime的使用
- lives in CustomScrollView应用
- 日历绘制方式
- 如何使用底部弹出组件
- 其他各种布局技巧和细节
可以改进的地方
- 支持国际化Ctom颜色
- 后续发布到Flutter Pub
代码地址
本例中相关代码位于
github地址:github.com/heruijun/fl…
来源❙❝:掘金
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。