观心静

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

前言

  此博客提供一个个人实现的自定义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

posted on 2024-05-28 17:32  观心静  阅读(133)  评论(3编辑  收藏  举报