因为项目须要,用Flutter重构了以前用Android作过的日历组件,总体效果感受不错,流畅度甚至超过原来的,这里须要提一下官网的作法,以下:html
var date = DateTime.now();
return showDatePicker(
context: context,
initialDate: date,
firstDate: date,
lastDate: date.add(
Duration(days: 30),
),
);
复制代码
官方的作法就是showDatePicker实现的,支持MD和IOS的风格,但据我了解,只支持单选,不支持开始和结束日期的区间选择,体验也与我须要的效果不一致,因此通过考虑以后,仍是决定本身写一个。前端
下面分段对部分代码进行描述。vue
下面就是一个日历选择组件的调用方式:node
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月,而后又有2个参数selectedStartDate和selectedEndDate,这2个参数是给定的默认选中区间,本例中默认选中了2019/8/28和2019/9/2之间的全部日期,默认选中通常是记录用户上次选中的结果。onSelectFinish就是选完以后的回调,以上这些参数是根据实际业务能够灵活设置的。git
这块其实很简单,CalendarList自己就支持从底部滑出,调用的方法是showModalBottomSheet,代码以下:github
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return Container(
height: 600.0,
child: FullScreenDemo(),
);
},
).then((result) {
setState(() {
selectResult2 = result;
});
});
复制代码
其中日历放在了FullScreenDemo里,经过Container包一层设置一个高度,而后就能够经过showModalBottomSheet方法从底部滑出。bash
经过上面的讲述,咱们了解了如何使用CalendarList组件,那么咱们看看源码里面具体作了哪些。笔者在实现该功能时把MonthView做为SliverList的一个build item。放置到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里面传入的month参数若是超过了12,则前面的年会自动“进位”(Flutter设置的太贴心了),好了,在_getMonthView里面,咱们看看return了一个什么样的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),
);
}
复制代码
好,这里就是传入了MonthView,设置了年、月,dateTimeStart,dateTimeEnd,today高亮颜色这些参数。下面,咱们看看MonthView里面又作了啥学习
MonthView其实就是真正绘制每月有多少个星期,而后每一个星期的7天展现,经过每行(Row)放置7个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();
}
复制代码
这样,一个日历就出来了,不过光有这些是不行的,由于还没开始作选择器,即(单选,多选,反选,取消这些),须要高亮出来,高亮的逻辑大体以下:
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 moment进行筛选,若是是在区间范围内,则选中该区间,猜猜怎么让DayNumber高亮起来? OK,其实知道了高亮区间以后,在DayNumber里就能够传入默认选中isDefaultSelected,下面,咱们看看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,
),
),
);
}
复制代码
其中Container里面声明了decoration,经过BoxDecoration设置了背景色,代码中把选中的效果优先于today高亮色,这样就能够覆盖当天的颜色,具体的Day则是Text绘制的。
经过上面的描述,咱们了解了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是否为null判断用户的点击行为落在哪一个if else里面,经过setState从新设置开始和结束日期,这样就能够“刷新”MonthView里面的DayNumber选择范围,好了,大体的核心源码就分析到这里。
本例中相关的代码放在
github地址:github.com/heruijun/fl…
此例已经做为补充内容添加至个人《Flutter从0到1构建大前端应用》一书的源码中,是一个知识点比较多的综合案例,再版时会根据读者意见考虑加入到书中讲解。
你们好,下面插播一条广告,我是《Flutter从0到1构建大前端应用》的做者,感谢已经购买的读者,此书属于入门上手的书籍,以简单明了的代码实例说明问题,也便于读者查阅相关内容。
从Flutter基础开始讲解,结合实际案例,让读者逐步掌握Flutter的核心内容,实战项目篇又经过2个实战项目让读者除了掌握Flutter相关知识以外,对node、mongo,vue作了一些介绍,可让更多的读者拥抱目前最火的大前端技术。
京东购买连接:item.jd.com/12546599.ht…