Android史上最好的日历,是怎样设计开发的?

Stella981
• 阅读 823

首先放出码云仓库地址

对于开源的UI库,最突出的特点莫过于以下三点
  • UI方便自定义

  • 性能要优秀

  • 功能要强大

记得2017年的时候,我从github、git上寻找过数个star数比较高的开源日历,但他们都有如下缺点
  • 性能好的,写死了UI

  • 可以修改UI的,基于ViewGroup,View的数量太多,性能差

  • 功能实在不足以应对日益增长的需求

例如Android非常出名的 material-calendarview

Android史上最好的日历,是怎样设计开发的?

作者对他的封装非常深,以致于该日历几乎就是Android系统日历的UI表现,甚至于日历头部的左右翻页按钮都写死在框架上,而不是提供2个API接口供用户调用,无法进行个性化修改,这就需要用户下载修改源码,而有时,这非常消耗时间。

还有下图钉钉的日历,是基于GridView或RecycerView这种ViewGroup生成的

Android史上最好的日历,是怎样设计开发的?

Android开发者一看就知道日历这里的控件数量太多了,打开这样一个界面是相当耗时的,要优化也只能延迟加载日历,这里仅日历月份部分就达到 42(天) * 4(每天4个view) * 3(ViewPager持有3个页面) = 504个View,如果这里用一个View的Canvas操作来替换 RecyclerView 中的 168个View ,少去这些View之间的互相测量性能消耗和内存消耗,加上 ViewPager 缓存3个页面的特点,总共可减少500+的View,性能可以提升数百倍,优势就相当明显了。

前方高能,注意保护眼睛

出于对技术的热爱,我决定自己开发一款日历,解决以上痛点,核心点如下

  • 框架本身仅实现算法、逻辑,UI交给用户,让用户很简单就能实现个性化定制

  • 出色的性能表现,极速加载,基于 Canvas 绘制实现,省去大量View的layout性能消耗和内存消耗

  • 高度可配置的热插拔设计模式,自由组合,人性化使用,应对各种产品形态

Canvas版日历的初步设计如下图

Android史上最好的日历,是怎样设计开发的?

每个月份仅用一个View来实现,由框架来提供每一天的 (x,y)坐标轴、宽、高、画笔、当前年月日、标记 等元数据,也就是说日历控件如同一张白纸,不参与UI部分的实现,用户仅需调用 Canvas.drawXXX() 等API即可实现日记UI,完全自定义。

采用 插拔式设计

插拔式设计:好比插座一样,插上灯泡就会亮,插上风扇就会转,看用户电器需求是什么而不是看插座有什么,只要是电器即可。为了让日历更加开放,此框架使用 热插拔式设计,既可以在编译时指定年月日视图,如: **app:month_view="xxx.xxx.MonthView.class"**,也可在运行时动态更换年月日视图,如: **CalendarView.setMonthViewClass(MonthView.Class)**,从而达到UI即插即用的效果,只需遵守插拔式接口即可随意定制,自由化程度非常高。

CalendarView 的终极特性

  • 基于Canvas绘制,极速性能
  • 热插拔思想,任意定制周视图、月视图,即插即用!
  • 支持单选、多选、范围选择、国内手机日历默认自动选择等选择模式
  • 支持静态、动态设置周起始,一行代码搞定
  • 支持静态、动态设置日历项高度、日历填充模式
  • 支持设置任意日期范围、任意拦截日期
  • 支持多点触控、手指平滑切换过渡,拒绝界面抖动
  • 类NestedScrolling特性,嵌套滚动
  • 既然这么多支持,那一定支持英语、繁体、简体,任意定制实现

如何使用CalendarView?

1、引入到Android项目中,Androidx版本
implementation 'com.haibin:calendarview:3.6.9'
2、参考自己的产品需求,选择合适的日历模式
  • 仅需要一个单选或类似手机自带日历,则新建2个View,一个继承自MonthView,一个继承WeekViewselect_mode="default_mode"select_mode="single_mode" 或者代码调用 setSelectSingleMode();setSelectDefaultMode();

  • 如果需要范围选择的日历,则新建2个View,一个继承自RangeMonthView,一个继承 RangeWeekView,然后在xml布局设置select_mode="range_mode" 或者代码调用 setSelectRangeMode();

  • 如果需要多选的日历,则新建2个View,一个继承自MultiMonthView,一个继承 MultiWeekView ,然后xml设置select_mode="multi_mode" 或者代码调用 setSelectMultiMode();

这里提供一个月视图三个待实现的方法,周视图代码一样,只是方法不需要y参数

public class MeiZuMonthView extends MonthView {

    /**
     * 绘制选中的日子
     *
     * @param canvas    canvas
     * @param calendar  日历日历calendar
     * @param x         日历Card x起点坐标
     * @param y         日历Card y起点坐标
     * @param hasScheme hasScheme 非标记的日期
     * @return 返回true 则绘制onDrawScheme,因为这里背景色不是是互斥的,所以返回true
     */
    @Override
    protected boolean onDrawSelected(Canvas canvas, Calendar calendar, int x, int y, boolean hasScheme) {
        //这里绘制选中的日子样式,看需求需不需要继续调用onDrawScheme
        return true;
    }

    /**
     * 绘制标记的事件日子
     *
     * @param canvas   canvas
     * @param calendar 日历calendar
     * @param x        日历Card x起点坐标
     * @param y        日历Card y起点坐标
     */
    @Override
    protected void onDrawScheme(Canvas canvas, Calendar calendar, int x, int y) {
       //这里绘制标记的日期样式,想怎么操作就怎么操作
    }

    /**
     * 绘制文本
     *
     * @param canvas     canvas
     * @param calendar   日历calendar
     * @param x          日历Card x起点坐标
     * @param y          日历Card y起点坐标
     * @param hasScheme  是否是标记的日期
     * @param isSelected 是否选中
     */
    @Override
    protected void onDrawText(Canvas canvas, Calendar calendar, int x, int y, boolean hasScheme, boolean isSelected) {
        //这里绘制文本,不要怎么隐藏农历了,不要再问我怎么把某个日期换成特殊字符串了,要怎么显示你就在这里怎么画,你不画就不显示,是看你想怎么显示日历的,而不是看框架
    }
}
3、当你实现好之后,直接在xml界面上添加视图,编译后可以即时预览效果:
app:month_view="com.haibin.calendarviewproject.MeiZuMonthView"

app:week_view="com.haibin.calendarviewproject.MeiZuWeekView"
4、如果静态模式无法满足你的需求,你可能需要动态变换定制的视图界面,你可以使用热插拔特性,即插即用,不爽就换:
mCalendarView.setWeekView(MeiZuWeekView.class);

mCalendarView.setMonthView(MeiZuMonthView.class);
5、如果你需要像小米一样可收缩的日历,你可以在 CalendarView 父布局添加 CalendarLayout,当然不需要也可以不用,例如原生日历,使用如下:
<com.haibin.calendarview.CalendarLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        app:default_status="shrink"
        app:calendar_show_mode="only_week_view"
        app:calendar_content_view_id="@+id/recyclerView">

        <com.haibin.calendarview.CalendarView
             android:id="@+id/calendarView"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:background="#fff"
             app:month_view="com.haibin.calendarviewproject.simple.SimpleMonthView"
             app:week_view="com.haibin.calendarviewproject.simple.SimpleWeekView"
             app:week_bar_view="com.haibin.calendarviewproject.EnglishWeekBar"
             app:calendar_height="50dp"
             app:current_month_text_color="#333333"
             app:current_month_lunar_text_color="#CFCFCF"
             app:min_year="2004"
             app:other_month_text_color="#e1e1e1"
             app:scheme_text="假"
             app:scheme_text_color="#333"
             app:scheme_theme_color="#333"
             app:selected_text_color="#fff"
             app:selected_theme_color="#333"
             app:week_start_with="mon"
             app:week_background="#fff"
             app:month_view_show_mode="mode_only_current"
             app:week_text_color="#111" />

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#ffffff" />
    </com.haibin.calendarview.CalendarLayout>
6、使用可收缩的日历你可以使用监听器,监听视图变换
public void setOnViewChangeListener(OnViewChangeListener listener);
7、 CalendarLayout 有很多特性可提供周月视图无缝切换,而且,平滑手势不抖动!使用 CalendarLayout,你需要指定 calendar_content_view_id,用来平移收缩月视图,更多特性如下:
<!-- 日历显示模式 -->
<attr name="calendar_show_mode">
      <enum name="both_month_week_view" value="0" /><!-- 默认都有 -->
      <enum name="only_week_view" value="1" /><!-- 仅周视图 -->
      <enum name="only_month_view" value="2" /><!-- 仅月视图 -->
</attr>

<attr name="default_status">
      <enum name="expand" value="0" /> <!--默认展开-->
      <enum name="shrink" value="1" /><!--默认搜索-->
</attr>

<attr name="calendar_content_view_id" format="integer" /><!--内容布局id,用于提供月视图平移过渡-->
8、 CalendarView 可以设置全屏,只需设置 app:calendar_match_parent="true"即可,全屏CalendarView是不需要周视图的,不必嵌套CalendarLayout

Android史上最好的日历,是怎样设计开发的?

9、 CalendarView 也提供了高效便利的年视图,可以快速切换年份、月份,十分便利,年视图也是热插拔设计,你可以自己继承YearView实现定制或者使用默认的YearView

Android史上最好的日历,是怎样设计开发的?

10、如果你希望像弹出 DatePickerView,通过它来跳转日期,你可以使用以下的API来让日历与其它控件联动
CalendarView.scrollToCalendar();

CalendarView.scrollToNext();

CalendarView.scrollToPre();

CalendarView.scrollToXXX();
11、你也许需要像魅族日历一样,可以静态、动态更换周起始
app:week_start_with="mon、sun、sat"

CalendarView.setWeekStarWithSun();

CalendarView.setWeekStarWithMon();

CalendarView.setWeekStarWithSat();
11、假如你是做酒店、旅游等应用场景的APP的,那么需要可选范围的日历,你可以这样继承,和普通视图实现完全一样,然后你需要设置选择模式为范围模式:select_mode="range_mode"
public class CustomRangeMonthView extends RangeMonthView{

}

public class CustomRangeWeekView extends RangeWeekView{

}

Android史上最好的日历,是怎样设计开发的?

12、酒店式日历场景当然是不能从昨天开始订房的,也不能无限期订房,所以你需要静态或动态设置日历范围、精确到具体某一天!!!
<attr name="min_year" format="integer" />
<attr name="max_year" format="integer" />
<attr name="min_year_month" format="integer" />
<attr name="max_year_month" format="integer" />
<attr name="min_year_day" format="integer" />
<attr name="max_year_day" format="integer" />

CalendarView.setRange(int minYear, int minYearMonth, int minYearDay,
         int maxYear, int maxYearMonth, int maxYearDay)
13、还有更特殊的日子也是不能选择的,例如:某月某号起这N天时间内因为超强台风来袭,酒店需停止营业N天,这段期间不可订房,这时日期拦截器就排上用场了
//设置日期拦截事件
mCalendarView.setOnCalendarInterceptListener(new CalendarView.OnCalendarInterceptListener() {
     @Override
     public boolean onCalendarIntercept(Calendar calendar) {
         //这里写拦截条件,返回true代表拦截,尽量以最高效的代码执行
         return calendar.isWeekend();
     }

     @Override
     public void onCalendarInterceptClick(Calendar calendar, boolean isClick) {
         //todo 点击拦截的日期回调
     }
});
14、添加日期拦截器和范围设置后,你可以在周月视图按需求获得他们的结果
boolean isInRange = isInRange(calendar);//日期是否在范围内,超出范围的可以置灰

boolean isEnable = !onCalendarIntercept(calendar);//日期是否可用,没有被拦截,被拦截的可以置灰
15、假如你是做清单类、任务类APP的,可能会有这样的需求:标记某天事务的进度,这也很简单,因为:日历界面长什么样,你自己说了算!!!

Android史上最好的日历,是怎样设计开发的?

16、也许你只需要像原生日历那样就够了,但原生日历那奇怪且十分不友好的style,受到theme的影响,各种头疼,使用此控件,你只需要简简单单定制月视图就够了,CalendarView 能非常简单就高仿各种日历UI
17、CalendarView 提供了 setSchemeDate(Map<String, Calendar> mSchemeDates) 这个十分高效的API用来动态标记事务,即时你的数据量达到数千、数万、数十万,都不会对UI渲染造成影响
  • 日历类 Calendar 提供了许多十分有用的API

    boolean isWeekend();//判断是不是周末,可以用不同的画笔绘制周末的样式

    int getWeek();//获取星期

    String getSolarTerm();//获取24节气,可以用不同颜色标记不同节日

    String getGregorianFestival();//获取公历节日,自由判断,把节日换上喜欢的颜色

    String getTraditionFestival();//获取传统节日

    boolean isLeapYear();//是否是闰年

    int getLeapMonth();//获取闰月

    boolean isSameMonth(Calendar calendar);//是否相同月

    int compareTo(Calendar calendar);//比较日期大小 <0 0 >0

    long getTimeInMillis();//获取时间戳

    int differ(Calendar calendar);//日期运算,相差多少天

CalendarView 的全部xml特性如下:

<declare-styleable name="CalendarView">

        <attr name="calendar_padding" format="dimension" /><!--日历内部左右padding-->

        <attr name="month_view" format="color" /> <!--自定义类日历月视图路径-->
        <attr name="week_view" format="string" /> <!--自定义类周视图路径-->
        <attr name="week_bar_height" format="dimension" /> <!--星期栏的高度-->
        <attr name="week_bar_view" format="color" /> <!--自定义类周栏路径,通过自定义则 week_text_color week_background xml设置无效,当仍可java api设置-->
        <attr name="week_line_margin" format="dimension" /><!--线条margin-->

        <attr name="week_line_background" format="color" /><!--线条颜色-->
        <attr name="week_background" format="color" /> <!--星期栏的背景-->
        <attr name="week_text_color" format="color" /> <!--星期栏文本颜色-->
        <attr name="week_text_size" format="dimension" /><!--星期栏文本大小-->

        <attr name="current_day_text_color" format="color" /> <!--今天的文本颜色-->
        <attr name="current_day_lunar_text_color" format="color" /><!--今天的农历文本颜色-->

        <attr name="calendar_height" format="string" /> <!--日历每项的高度,56dp-->
        <attr name="day_text_size" format="string" /> <!--天数文本大小-->
        <attr name="lunar_text_size" format="string" /> <!--农历文本大小-->

        <attr name="scheme_text" format="string" /> <!--标记文本-->
        <attr name="scheme_text_color" format="color" /> <!--标记文本颜色-->
        <attr name="scheme_month_text_color" format="color" /> <!--标记天数文本颜色-->
        <attr name="scheme_lunar_text_color" format="color" /> <!--标记农历文本颜色-->

        <attr name="scheme_theme_color" format="color" /> <!--标记的颜色-->

        <attr name="selected_theme_color" format="color" /> <!--选中颜色-->
        <attr name="selected_text_color" format="color" /> <!--选中文本颜色-->
        <attr name="selected_lunar_text_color" format="color" /> <!--选中农历文本颜色-->

        <attr name="current_month_text_color" format="color" /> <!--当前月份的字体颜色-->
        <attr name="other_month_text_color" format="color" /> <!--其它月份的字体颜色-->

        <attr name="current_month_lunar_text_color" format="color" /> <!--当前月份农历节假日颜色-->
        <attr name="other_month_lunar_text_color" format="color" /> <!--其它月份农历节假日颜色-->

        <!-- 年视图相关 -->
        <attr name="year_view_month_text_size" format="dimension" /> <!-- 年视图月份字体大小 -->
        <attr name="year_view_day_text_size" format="dimension" /> <!-- 年视图月份日期字体大小 -->
        <attr name="year_view_month_text_color" format="color" /> <!-- 年视图月份字体颜色 -->
        <attr name="year_view_day_text_color" format="color" /> <!-- 年视图日期字体颜色 -->
        <attr name="year_view_scheme_color" format="color" /> <!-- 年视图标记颜色 -->

        <attr name="min_year" format="integer" />  <!--最小年份1900-->
        <attr name="max_year" format="integer" />  <!--最大年份2099-->
        <attr name="min_year_month" format="integer" /> <!--最小年份对应月份-->
        <attr name="max_year_month" format="integer" /> <!--最大年份对应月份-->

        <!--月视图是否可滚动-->
        <attr name="month_view_scrollable" format="boolean" />
        <!--周视图是否可滚动-->
        <attr name="week_view_scrollable" format="boolean" />
        <!--年视图是否可滚动-->
        <attr name="year_view_scrollable" format="boolean" />
        
        <!--配置你喜欢的月视图显示模式模式-->
        <attr name="month_view_show_mode">
             <enum name="mode_all" value="0" /> <!--全部显示-->
             <enum name="mode_only_current" value="1" /> <!--仅显示当前月份-->
             <enum name="mode_fix" value="2" /> <!--自适应显示,不会多出一行,但是会自动填充-->
        </attr>

        <!-- 自定义周起始 -->
        <attr name="week_start_with">
             <enum name="sun" value="1" />
             <enum name="mon" value="2" />
             <enum name="sat" value="7" />
        </attr>

        <!-- 自定义选择模式 -->
        <attr name="select_mode">
              <enum name="default_mode" value="0" />
              <enum name="single_mode" value="1" />
              <enum name="range_mode" value="2" />
              <enum name="multi_mode" value="3" />
        </attr>

        <!-- when select_mode = multi_mode -->
        <attr name="max_multi_select_size" format="integer" />

        <!-- 当 select_mode=range_mode -->
        <attr name="min_select_range" format="integer" />
        <attr name="max_select_range" format="integer" />
        
        <!-- auto select day -->
        <attr name="month_view_auto_select_day">
              <enum name="first_day_of_month" value="0" />
              <enum name="last_select_day" value="1" />
              <enum name="last_select_day_ignore_current" value="2" />
        </attr>
</declare-styleable>

此日历仅不到2年时间,就从零登顶同类开源项目,受到广大需要开源日历的用户的支持,广泛应用大多公司日历模块中,此后本项目仅在码云上更新。

码云仓库地址

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Easter79 Easter79
2年前
swap空间的增减方法
(1)增大swap空间去激活swap交换区:swapoff v /dev/vg00/lvswap扩展交换lv:lvextend L 10G /dev/vg00/lvswap重新生成swap交换区:mkswap /dev/vg00/lvswap激活新生成的交换区:swapon v /dev/vg00/lvswap
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
2年前
Java获得今日零时零分零秒的时间(Date型)
publicDatezeroTime()throwsParseException{    DatetimenewDate();    SimpleDateFormatsimpnewSimpleDateFormat("yyyyMMdd00:00:00");    SimpleDateFormatsimp2newS
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
2个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这