首页 > 其他分享 >自定义一个简单的日历

自定义一个简单的日历

时间:2024-05-28 17:47:13浏览次数:14  
标签:return 自定义 dateTime 日历 List DateTime controller 简单 month

前言

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

  PageController _pageController = PageController();

  ///设置当前页面显示的月份监听器
  void setPageMonthListener(Function(DateTime)? listener) {
    _pagePositionListener = listener;
  }

  void _setStateContext(Function setState) {
    this._setState = setState;
  }

  /// 设置日历显示的日期范围
  void setDateRange(DateTime startDate, DateTime endDate) {
    //这里将开始与结束日期的日期时间规整到开始月份的第一天和结束月份的最后一天
    this._startDate = startDate.copyWith(day: 1);
    var date = DateTime(endDate.year, endDate.month + 1, 1, 0, 0, 0, 0, 0);
    this._endDate = date.addDaysToDate(-1).copyWith(
        hour: 23,
        minute: 59,
        second: 59,
        millisecond: 999,
        microsecond: 000000);
    if (_setState == null) {
      _updateDateRange();
      return;
    }
    _setState!(() {
      _updateDateRange();
    });
  }

  //更新日历需要显示的日期范围
  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.blue
      ..strokeWidth = 1.0
      ..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: 16.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, 20.0, _checkBgPaint);
    }
    //绘制day的底部背景,这个背景是选中的背景
    _dayBgPaint.color = selectDayBgColor(dateTime);
    Offset bgOffset = Offset(offset.dx + horizontalPadding, offset.dy + verticalPadding / 2);
    canvas.drawCircle(bgOffset, 20.0, _dayBgPaint);
    //绘制day
    TextPainter textPaint = TextPainter(
        textDirection: TextDirection.ltr,
        text: TextSpan(
            text: dayOfMonthValue,
            style: TextStyle(
              textBaseline: TextBaseline.ideographic,
              color: Colors.white,
              fontSize: 15.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

标签:return,自定义,dateTime,日历,List,DateTime,controller,简单,month
From: https://www.cnblogs.com/guanxinjing/p/18218525

相关文章

  • CSS 之 自定义属性(变量)
    一、简介​CSS的自定义属性,又称为CSS变量或级联变量,用于定义一个带有值的、可重复使用的CSS属性(变量)。其包含的值可以在其作用域内的任意属性上重复使用,在使用时需要借助var()函数获取自定义属性的值。当自定义属性的值发生变化时,所有使用该自定义属性的CSS属性都会随之变......
  • 简单工厂模式、工厂模式、抽象工厂模式
    工厂模式(FactoryPattern)是一种创建对象的设计模式,它提供了一种方法来将对象的创建过程与对象的使用过程分离。工厂模式在软件开发中有广泛的应用,适用于不同的场景和需求。下面是几种常见的工厂模式及其应用场景:1.简单工厂模式(SimpleFactoryPattern)简单工厂模式通过一个工厂......
  • python+threading,实现简单的接口并发测试
    #-*-coding:utf-8-*-importthreadingfromutilsimporthttpUtilbody={"claimId":10179599,"protocols":[{"protocolUrl":None,"protocolContent":"<spanclass='c_......
  • 案例一:neo4j构建简单的知识图谱python启动
    案例一里面有4个python文件: 其中test1可以正常启动test4里面没啥内容可以不用管,其他的两个文件,会出现报错: 原因是被爬取信息的网站现在不允许任意获得了,必须要密钥,所以我们要找到这个网站去注册密钥:Tushare数据  这样就可以运行成功了;......
  • LinqPad简单使用
    1.下载安装包安装,图标   ,安装好之后打开2.连接数据库填写数据库连接信息,点击test,会提示下载,点击确认,等待测试连接为Successful保存即可。  3.添加成功后,左侧就可以看到我们的数据库了,直接添加一个Query,写我们的代码,我的表都是提前建好的async Task Main(){ ......
  • 基于linux下c实现的简单版线程池
    #include<iostream>#include<unistd.h>#include<pthread.h>#include<string>#include<signal.h>#include<stdlib.h>#include<string.h>#include<errno.h>#defineDEFAULT_TIME10#defineDEFAULT_STEP15using......
  • Kettle 自定义循环 & 更新变量值
    布局图 Setvariables JavaScript(循环逻辑)varmin=newNumber(parent_job.getVariable("MIN"));varmax=newNumber(parent_job.getVariable("MAX"));if(max>=min){true;}else{false;}JavaScript(更新循环条件)varmax=newNum......
  • 简单理解Zookeeper之数据同步机制
    写入数据流程请求发给Leaderclient向Zookeeper集群的Leader节点发送写请求Leader节点接收到写请求后,会对请求进行预处理,并为这次写操作分配一个全局唯一的递增ID(ZXID)。Leader将这个写请求(提案)广播给所有的Follower节点。这个提案包含了请求的具体内容和分配的ZXID。每个......
  • 简单理解Flume之Channel和Sink
    ChannelMemoryChannel1,MemoryChannel将数据临时存储的到内存队列2,属性属性默认值解释capacity100队列容量,默认情况队列中最多临时存储100条数据,实际过程这个值一般被调节成30W~50WtransacCapacity100PutList向Channel发送的数据条数,实际中一般会调节成3000~5000Fil......
  • 简单理解Flume之Source
    SourceAVROSource1,AVROSource监听指定端口,接收被AVRO序列化之后的数据2,结合AVROSink可以实现多级扇入扇出流动a1.sources=s1a1.channels=c1a1.sinks=k1#配置AVROSourcea1.sources.s1.type=avro#要监听的主机名或者IP地址a1.sources.s1.bind=hadoop0......