前言
此博客提供一个个人实现的自定义View,日历的内容全部是通过绘制实现的。 虽然是使用flutter实现自定义日历View的, 并且日历可能很简单,不是一个封装完整的日历。但是关键核心思想是一致的,可以提供给你自行实现自定义日历的思维,这样你可以自行实现复杂效果。这边放到博客中提供给各位参考。 后续有时间会继续提供Android版本的自定义日历。
效果图
代码
最关键的是绘制日历内容的4个函数: _dartWeek 、 _drawItemWeek 、 _drawDay 、 _drawItemDay 。
其他部分注释很多,直接查看即可。
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:mentech_wear/ext/date_time_ext.dart';
/*
* @Title: calendar_view
* @Description: 生理周期使用的日历View
* @author
* @date 2024/5/27 20:46
*/
class CalendarView extends StatefulWidget {
late CalendarController controller;
CalendarView(this.controller);
@override
_CalendarViewState createState() => _CalendarViewState();
}
class _CalendarViewState extends State<CalendarView> {
late PageController _pageController;
@override
Widget build(BuildContext context) {
widget.controller._setStateContext(setState);
widget.controller._updateDateRange();
_pageController = widget.controller._getInitialPageController();
return Container(
padding: EdgeInsets.all(0),
child: PageView(
controller: _pageController,
scrollBehavior: ScrollBehavior(),
clipBehavior: Clip.none,
children: <Widget>[
for (var date in widget.controller._dateList) _itemMonthView(date)
],
onPageChanged: (index) {
DateTime current = widget.controller._dateList[index];
if (widget.controller._pagePositionListener != null) {
widget.controller._pagePositionListener!(current);
}
},
));
}
Widget _itemMonthView(DateTime dateTime) {
_CalendarInteriorController controller = _CalendarInteriorController();
List<SelectDay> currentMonthSelectDayList = widget.controller._selectDayList
.where((it) =>
it.dateTime.year == dateTime.year &&
it.dateTime.month == dateTime.month)
.toList();
return Container(
child: GestureDetector(
onTapUp: (TapUpDetails details) {
Map? clickDayData =
controller.getClickData(details.localPosition);
if (clickDayData != null) {
widget.controller.onClickDateTimeListener?.call(clickDayData["day"] as DateTime);
setState(() {
widget.controller._checkDateTime =
clickDayData["day"] as DateTime;
});
}
},
child: CustomPaint(
painter: _Calendar(
dateTime,
widget.controller._weeks,
controller,
widget.controller._checkDateTime,
currentMonthSelectDayList))));
}
}
class CalendarController {
List<String> _weeks = ["一", "二", "三", "四", "五", "六", "日"];
DateTime? _checkDateTime = null;
List<SelectDay> _selectDayList = [SelectDay(DateTime.now(), Colors.red)];
DateTime startDate = DateTime.now().copyWith(
month: 1,
day: 1,
hour: 0,
minute: 0,
second: 0,
millisecond: 0,
microsecond: 0);
DateTime endDate = DateTime.now().copyWith(
month: 12,
day: 31,
hour: 23,
minute: 59,
second: 59,
millisecond: 999,
microsecond: 999);
List<DateTime> _dateList = [];
Function? _setState = null;
Function(DateTime)? _pagePositionListener = null;
Function(DateTime)? onClickDateTimeListener = null;
PageController _pageController = PageController();
CalendarController(this.startDate, this.endDate);
///设置点击日期的监听器
setOnClickDateTimeListener(Function(DateTime)? listener) {
onClickDateTimeListener = listener;
}
///设置当前页面显示的月份监听器
void setPageMonthListener(Function(DateTime)? listener) {
_pagePositionListener = listener;
}
void _setStateContext(Function setState) {
this._setState = setState;
}
//更新日历需要显示的日期范围
void _updateDateRange() {
_dateList.clear();
_dateList.add(startDate);
while (true) {
DateTime nextDate =
DateTime(_dateList.last.year, _dateList.last.month + 1);
if (nextDate.isAfter(endDate)) {
break;
}
_dateList.add(nextDate);
}
}
/// 设置星期的文字
void setWeeks(List<String> value) {
if (_setState == null) {
_weeks = value;
return;
}
_setState!(() {
_weeks = value;
});
}
/// 选择的日期
void setSelectDayList(List<SelectDay> value) {
if (_setState == null) {
_selectDayList = value;
return;
}
_setState!(() {
_selectDayList = value;
});
}
/// 设置点击选中的日期
void setCheckDateTime(DateTime? value) {
if (_setState == null) {
_checkDateTime = value;
return;
}
_setState!(() {
_checkDateTime = value;
});
}
///Page的控制器
PageController _getInitialPageController() {
if (_dateList.isEmpty) {
_pageController = PageController(initialPage: 0);
return _pageController;
}
DateTime now = DateTime.now();
DateTime month = _dateList
.firstWhere((it) => it.year == now.year && it.month == now.month);
_pageController = PageController(initialPage: _dateList.indexOf(month));
return _pageController;
}
///滚动到指定月份的页面 返回的布尔值ture表示跳转成功 false表示跳转失败
bool rollToMonthPage(DateTime month, {bool isAnimate = false}){
if(month.isAfter(endDate)){
//要跳转的月份大于日历的显示的范围
return false;
}
if(month.isBefore(startDate)){
//要跳转的月份小于日历的显示的范围
return false;
}
DateTime? jumpMonth = _dateList.firstWhereOrNull((it) => it.year == month.year && it.month == month.month);
if(jumpMonth == null){
return false;
}
int jumpIndex = _dateList.indexOf(month);
if(isAnimate){
_pageController.animateToPage(jumpIndex, duration: Duration(milliseconds: 500), curve: Curves.ease);
} else {
_pageController.jumpToPage(jumpIndex);
}
return true;
}
}
class _Calendar extends CustomPainter {
DateTime dateTime = DateTime.now();
List<Offset> weeksOffset = [];
List<String> weeks = ["一", "二", "三", "四", "五", "六", "日"];
List<Offset> dayOffset = [];
List<Offset> dayActualDrawOffset = [];
List<String> days = [];
//选择的日期
List<SelectDay> selectDayList = [];
//点击后选中的日期
DateTime? checkDateTime = null;
//月份的天数
int dayCountOfMonth = 0;
//日期的背景
late Paint _dayBgPaint;
//选中日期的背景
late Paint _checkBgPaint;
//内部的控制器
_CalendarInteriorController _controller = _CalendarInteriorController();
//左内边距
double horizontalPadding = 10;
//上内边距
double verticalPadding = 10;
_Calendar(this.dateTime, this.weeks, this._controller, this.checkDateTime,
this.selectDayList) {
initPaint();
//计算这个月需要显示多少天数
DateTime endOfMonth = _getEndTimeOfMonth(dateTime);
dayCountOfMonth = endOfMonth.day;
days = List.generate(dayCountOfMonth, (index) => (index + 1).toString());
}
void initPaint() {
_dayBgPaint = Paint()
..color = Colors.grey
..strokeWidth = 1.0
..style = PaintingStyle.fill;
_checkBgPaint = Paint()
..color = Colors.white
..strokeWidth = 1.5
..style = PaintingStyle.stroke;
}
@override
void paint(Canvas canvas, Size size) {
horizontalPadding = size.width * 0.08;
verticalPadding = size.height * 0.12;
_controller.setPadding(horizontalPadding, verticalPadding);
_dartWeek(canvas, size);
_drawDay(canvas, size);
}
void _dartWeek(Canvas canvas, Size size) {
double itemWidth = size.width / 7;
for (int i = 0; i < 7; i++) {
weeksOffset.add(Offset(itemWidth * i, 0));
}
for (int i = 0; i < 7; i++) {
_drawItemWeek(canvas, weeksOffset[i], weeks[i]);
}
}
//绘制item星期
void _drawItemWeek(Canvas canvas, Offset offset, String weekValue) {
TextPainter textPaint = TextPainter(
textDirection: TextDirection.ltr,
text: TextSpan(
text: weekValue,
style: TextStyle(
color: Colors.white,
fontSize: 13.sp,
)));
textPaint.layout();
Offset actualOffset = Offset(
(offset.dx - textPaint.width / 2) + horizontalPadding,
offset.dy - textPaint.height / 2 + verticalPadding / 2);
textPaint.paint(canvas, actualOffset);
}
//绘制天,这个函数主要做一些绘制前的计算工作,负责计算每一个day的位置
void _drawDay(Canvas canvas, Size size) {
double itemWidth = size.width / 7;
//这里设置7列,因为还需要将星期的那一列计算进去,而剩下的6列是因为有一些月份是含有6个星期的
double itemHeight = size.height / 7;
List<DateTime> daysOfMonth = [];
for (int i = 0; i < dayCountOfMonth; i++) {
DateTime currentDay = dateTime.copyWith(
day: i + 1, hour: 0, minute: 0, second: 0, millisecond: 1);
daysOfMonth.add(currentDay);
//星期几,这个用来确定x轴坐标
int weekday = currentDay.weekday - 1;
//星期数(这个月的第几周),这个用来确定y轴坐标
int weekNum = getWeekOfMonth(currentDay);
dayOffset.add(Offset(itemWidth * weekday, itemHeight * weekNum));
days.add((i + 1).toString());
}
_controller.setDayOffset(dayOffset);
_controller.setDayOfMonth(daysOfMonth);
for (int i = 0; i < dayCountOfMonth; i++) {
_drawItemDay(canvas, dayOffset[i], days[i], daysOfMonth[i]);
}
}
//绘制item日期
void _drawItemDay(
Canvas canvas, Offset offset, String dayOfMonthValue, DateTime dateTime) {
//绘制选中的背景
if (isCheckDay(dateTime)) {
Offset checkBgOffset = Offset(
offset.dx + horizontalPadding, offset.dy + verticalPadding / 2);
canvas.drawCircle(checkBgOffset, 17, _checkBgPaint);
}
//绘制day的底部背景,这个背景是选中的背景
_dayBgPaint.color = selectDayBgColor(dateTime);
Offset bgOffset = Offset(offset.dx + horizontalPadding, offset.dy + verticalPadding / 2);
canvas.drawCircle(bgOffset, 14, _dayBgPaint);
//绘制day
TextPainter textPaint = TextPainter(
textDirection: TextDirection.ltr,
text: TextSpan(
text: dayOfMonthValue,
style: TextStyle(
textBaseline: TextBaseline.ideographic,
color: Colors.white,
fontSize: 14.sp,
)));
textPaint.layout();
//这里x轴减去的textPaint.width / 2,是为了将文字的绘制点移动到中心点,y轴同理
Offset actualOffset = Offset(
(offset.dx - textPaint.width / 2) + horizontalPadding,
offset.dy - textPaint.height / 2 + verticalPadding / 2);
textPaint.paint(canvas, actualOffset);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
///得到月的结束时间
DateTime _getEndTimeOfMonth(DateTime dateTime) {
var date = DateTime(dateTime.year, dateTime.month + 1, 1, 0, 0, 0, 0, 0);
return date.addDaysToDate(-1).copyWith(
hour: 23,
minute: 59,
second: 59,
millisecond: 999,
microsecond: 000000);
}
int getWeekOfMonth(DateTime date) {
final firstDayOfMonth = DateTime(date.year, date.month, 1); // 获取月份的第一天
final differenceInDays =
date.difference(firstDayOfMonth).inDays; // 当前日期与月份第一天的天数差
final firstDayOfWeek =
firstDayOfMonth.weekday - 1; // 调整到ISO 8601标准下的第一天(周一是0,周日是6)
// 计算当前日期处于本月的第几周
final weekNumber = (differenceInDays + firstDayOfWeek) ~/ 7 + 1;
return weekNumber;
}
///是否是点击选中的日期
bool isCheckDay(DateTime dateTime) {
if (checkDateTime != null) {
if (dateTime.year == checkDateTime!.year &&
dateTime.month == checkDateTime!.month &&
dateTime.day == checkDateTime!.day) {
return true;
}
}
return false;
}
Color selectDayBgColor(DateTime dateTime) {
if (selectDayList.isEmpty) {
return Colors.transparent;
}
for (SelectDay item in selectDayList) {
if (dateTime.year == item.dateTime.year &&
dateTime.month == item.dateTime.month &&
dateTime.day == item.dateTime.day) {
return item.color;
}
}
return Colors.transparent;
}
}
//日历的内部控制器
class _CalendarInteriorController {
List<Offset> dayOffset = [];
List<DateTime> dayOfMonth = [];
//左内边距
double horizontalPadding = 10;
//上内边距
double verticalPadding = 10;
_CalendarInteriorController();
//设置内边距,这个内边距传到控制类是为了给下面计算点击位置时的补正使用
setPadding(double horizontal, double vertical) {
horizontalPadding = horizontal;
verticalPadding = vertical;
}
setDayOffset(List<Offset> offset) {
dayOffset = offset;
}
setDayOfMonth(List<DateTime> list) {
dayOfMonth = list;
}
//根据点击位置信息,获得被点击的日期数据
Map? getClickData(Offset offset) {
for (var item in dayOffset) {
//这里加减20是为了增加点击的判断范围
double left = item.dx + horizontalPadding - 20;
double right = item.dx + horizontalPadding + 20;
double top = item.dy + verticalPadding / 2 - 20;
double bottom = item.dy + verticalPadding / 2 + 20;
if (offset.dx > left &&
offset.dx < right &&
offset.dy > top &&
offset.dy < bottom) {
int position = dayOffset.indexOf(item);
Map clickMap = {
"offset": item,
"position": position,
"day": dayOfMonth[position]
};
return clickMap;
}
}
return null;
}
}
class SelectDay {
DateTime dateTime = DateTime.now();
Color color = Colors.grey;
SelectDay(this.dateTime, this.color);
}
end
本文来自博客园,作者:观心静 ,转载请注明原文链接:https://www.cnblogs.com/guanxinjing/p/18218525
本文版权归作者和博客园共有,欢迎转载,但必须给出原文链接,并保留此段声明,否则保留追究法律责任的权利。