欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android 一个日历控件的实现代码

程序员文章站 2023-11-29 10:53:34
先看几张动态的效果图吧! 项目地址:https://github.com/othershe/calendarview 这里主要记录一下在编写日历...

Android 一个日历控件的实现代码

先看几张动态的效果图吧!

Android 一个日历控件的实现代码

Android 一个日历控件的实现代码

Android 一个日历控件的实现代码

项目地址:https://github.com/othershe/calendarview

这里主要记录一下在编写日历控件过程中一些主要的点:

一、主要功能

1、支持农历、节气、常用节假日
2、日期范围设置,默认支持的最大日期范围[1900.1~2049.12]
3、默认选中日期设置
4、单选、多选
5、跳转到指定日期
6、通过自定义属性定制日期外观,以及简单的日期item布局配置

二、基本结构

我们要实现的日历控件采用viewpager作为主框架,calendarview继承viewpager,这样就天生拥有左右滑动和缓存的功能。目前我们设定日历左右滑动为月份切换的操作,每一个月份显示通过自定义viewgroup实现,也就是我们的monthview,月份中的日期是通过layout布局解析出的view,根据月份的不同每个monthview可能包含6 x 7或5 x 7个日期view,由于给viewpager绑定数据需要通过pageradapter,所以继承pageradapter我们扩展了一个calendarpageradapter,来完成monthview的相关初始化和日期数据的绑定。

三、计算每个monthview需要填充的日期数据

从上边的截图可以看出,每个monthview的日期数据应该由上个月的后0~6天、当前月的天数和下个月的前0~6天组成。首先计算出当前月有多少天,这个简单,以及根据年月算出当前月的第一天是星期几:

public static int getfirstweekofmonth(int year, int month) {
    calendar calendar = calendar.getinstance();
    calendar.set(year, month, 1);
    return calendar.get(calendar.day_of_week) - 1;
  }

返回0代表周日,1~6代表周一到周六,以上边的截图为例,可以知道2017年5月的第一天是周一:week = getfirstweekofmonth(2017, 5-1),按照如下伪码则可计算出包含的上个月的日期:

for (int i = 0; i < week; i++) {
      ld = 上个月天数 - week + 1 + i;
    }

至于包含的下个月的日期和当前monthview显示的行数有关,如果 当前月的天数+week可以被7整除则不需要包含下月日期,否则需要计算包含的下月日期,伪码如下:

for (int i = 0; i < 7 * 显示的行数 - 当月天数 - week; i++) {
      nd = i + 1;
    }

这样需要的日期数据就计算完了,详细的算法可参考源码。

四、 计算日历的总页数

总页数应由日历的起始年月得到,其实就是确定viewpager的总页数,这样好理解点。可按照如下方法计算:

复制代码 代码如下:

count = (dateend[0] - datestart[0]) * 12 + dateend[1] - datestart[1] + 1

其中datestart、dateend是包含日历开始年月和结束年月的数组。这个count也是calendarpageradapter必须的。

五、用position计算日期

pageradapter有个instantiateitem()方法:

public object instantiateitem(viewgroup container, int position) {
    return instantiateitem((view) container, position);
  }

来创建viewpager的每一页,所以日历每一页也是在这里创建的,也就是monthview,这里有个关键的点就是根据 positon 参数推算出日历每一页对应的年月,然后通过年月计算出当前monthview需要的日期数据。如何根据position推算出年月呢?

public static int[] positiontodate(int position, int starty, int startm) {
    int year = position / 12 + starty;
    int month = position % 12 + startm;

    if (month > 12) {
      month = month % 12;
      year = year + 1;
    }

    return new int[]{year, month};
  }

其中starty、startm代表日历的其实年月。有了对应的年月就可以用第二点中的方式计算日期数据,然后填充到mothview中。

六、mothview

前边已经提到了,monthview继承viewgroup,也就是日历的每一页,接收到日期数据后,在monthview中根据数据构造对应的日期view,然后添加view到monthview中,最后通过onmeasure、onlayout确定每个view最终大小和位置。到这里运行一个viewpager的基本条件就满足了,在上边提到的instantiateitem()方法中完成mothview的初始化:

public object instantiateitem(viewgroup container, int position) {
    monthview view = new monthview(container.getcontext());
    //根据position计算对应年、月
    int[] date = calendarutil.positiontodate(position, datestart[0], datestart[1]);
    view.setdatelist(calendarutil.getmonthdate(date[0], date[1]), solarutil.getmonthdays(date[0], date[1]));
    container.addview(view);

    return view;
  }

这里只保留了核心的代码,当日历切换月份时,会自动根据position计算出对应月份的日期数据,然后传给monthview,最后将monthview添加到viewpager中。

七、切换月份选中日期

按照目前的设定,当选择当前月的某天后,然后切换月份,新的月份中会找到上次选中的日期,并标记为选中状态,如果找不到则选中新月份的最后一天。其实逻辑很简单,关键是如何在新月份中找到相应的日期并选中。首先记录上次选中的日期,由于viewpager默认会缓存两页,再加上当前页共三页,在calendarpageradapter中根据position保存三页缓存,当viewpager切换到某一页后会执行如下回调:

addonpagechangelistener(new simpleonpagechangelistener() {
      @override
      public void onpageselected(int position) {
      }
    });

在onpageselected(int position)方法中通过position从缓存中拿到对应的monthview,也是是切换到的页,这样就能在monthview中根据记录的日期找到对应的子日期view,然后更改为选中状态。

八、多选

一个理想的多选功能应该是在当前月份选中多个日期后,切换到其它月份,之后回到有选中日期的月份依然能够标记出选中的日期,因为viewpager有默认的三页缓存,所以在当前月份切换到上月或下月不会有什么问题,但如果切换到前几个月或后几个月,再回到有选中日期的月份,由于之前缓存的页面已经被销毁重建,所以选中的月份也就看不到了。我们的日期点击事件在monthview中,当每次点击选中时我们需要记录对应年月选中的日期,取消选中时要从记录中删除对应日期,怎么保存呢?在calendarview类中我们定义一个sparsearray

复制代码 代码如下:

sparsearray<hashset<integer>> choosedate = new sparsearray<>()

其中的hashset就是指定年月选中的日期,按照我们的规则设定不同年月转换得到的position是唯一对应的,所以我们用position作为sparsearray的key,最后在calendarview中接收选中或取消选中的操作:

public void setlastchoosedate(int day, boolean flag) {
    hashset<integer> days = choosedate.get(currentposition);
    if (flag) {
      if (days == null) {
        days = new hashset<>();
        choosedate.put(currentposition, days);
      }
      days.add(day);
    } else {
      days.remove(day);
    }
  }

之后就是在月份切换过程中,根据保存的日期数据刷新对应的monthview,实现选中状态的恢复,这个和第六点类似。

九、跳转到指定日期

要跳转到指定日期,首先要根据日期的年月计算出目标monthview在日历中的position:

public static int datetoposition(int year, int month, int starty, int startm) {
    return (year - starty) * 12 + month - startm;
  }

viewpager有一个setcurrentitem(int item, boolean smoothscroll)方法,这样就能跳转到position对应的monthview,然后结合第六点的方法选中对应的日期view。这样跳转到日历设定日期范围内的任意一天都是没问题的。

十、自定义日历样式

目前calendarview提供的自定义属性如下:

<declare-styleable name="calendarview">
    <!--是否多选-->
    <attr name="multi_choose" format="boolean" />
    <!--是否显示农历-->
    <attr name="show_lunar" format="boolean" />
    <!--是否显示上月和下月-->
    <attr name="show_last_next" format="boolean" />
    <!--是否显示节假日-->
    <attr name="show_holiday" format="boolean" />
    <!--是否显示节气-->
    <attr name="show_term" format="boolean" />
    <!--开始日期(1990.1)-->
    <attr name="date_start" format="string" />
    <!--结束日期(2020.12)-->
    <attr name="date_end" format="string" />
    <!--默认展示、选中的日期(2016.10.1)-->
    <attr name="date_init" format="string" />
    <!--是否禁用默认选中日期前的所有日期-->
    <attr name="disable_before" format="boolean" />
    <!--阳历的日期颜色-->
    <attr name="color_solar" format="color" />
    <!--阳历的日期尺寸-->
    <attr name="size_solar" format="integer" />
    <!--农历的日期颜色-->
    <attr name="color_lunar" format="color" />
    <!--农历的日期尺寸-->
    <attr name="size_lunar" format="integer" />
    <!--节日文字颜色-->
    <attr name="color_holiday" format="color" />
    <!--选中的日期文字颜色-->
    <attr name="color_choose" format="color" />
    <!--选中的日期背景(图片)-->
    <attr name="day_bg" format="reference" />
    <!--单选时切换月份,是否选中上次的日期-->
    <attr name="switch_choose" format="boolean" />
  </declare-styleable>

基本可以满足日常的需求,默认的日期布局是阳历、阴历垂直排列,节假日会覆盖在农历上显示,这个从上边的静态截图可以看出。如果要使用其它的排列方式,例如水平排列等,就需要提供一个自定的layout(但目前只支持两个textview显示)。例如:

calendarview.setoncalendarviewadapter(r.layout.item_layout, new calendarviewadapter() {
      @override
      public textview[] convertview(view view, datebean date) {
        textview solarday = (textview) view.findviewbyid(r.id.solar_day);
        textview lunarday = (textview) view.findviewbyid(r.id.lunar_day);
        return new textview[]{solarday, lunarday};
      }
    });

给calendarview绑定一个接口,传入lauoyt,然后返回一个代表阳历和农历的textview数组。

十一、weekview

我们将日期和星期的显示功能分割开了,所以calendarview并不负责星期的显示,weekview是星期显示的自定义view,从周日开始依次是周一到周六,可通过自定义属性来配置星期的显示文字,以及文字的颜色、尺寸,这个还是相对简单,具体可见github中的使用介绍。

十二、小结

这里我们只介绍了日历的基本实现原理,和一些关键的点,其实这种实现方式相对还是比较简单的,容易理解,当然难免有不足的地方,后边根据需要再逐步完善和扩展吧。尽管github上有许多现成的calendar,但自己动手实现一个还是收获满满,一个看起来简单的东西,只有亲自尝试了才能体会到其中的滋味,最后希望对大家有所帮助吧!