序
因为之前公司的产品需求,需要做一个符合日历view做某些功能,当时想着说,日历view还是挺简单的,官方也有封装,要不了多久,因此提前调研的时候主要看了一下官方的CalendarView以及一些github上的开源日历view。
然而等到开始开发,拿到UI和UE的交互图时,我傻了,和说好的不一样啊,UE跟我说,你去看看飞猪的那个日历,交互和那个差不多。行吧,只能重新撸代码了。
飞猪的那个日历,是一个竖向日历,和显示连续几个月的日期,然后不可选择的日期标灰,然后还会显示一些节假日,支持区间选择,每个月份有一个悬停的标题。这边我们的需求没有区间选择,但是有每个日期的view有不同的显示需求。
思路
一个竖向滚动的日历,最顶部是周几,每个月的日期数不一样,要有留白,以及顶部的月份标识,我觉得大家看到这种需求,第一反应都是RecyclerView对吧,当然我也是这么想的,每日都是一个item,然后留白就是空的占位,大致的思路是可以决定了的。
实现
顶部星期显示
因为顶部的星期是固定显示的,而且是横向铺开的,所以一个LinearLayout即可实现
val week = LinearLayout(context).apply {
val headParams = LayoutParams(LayoutParams.MATCH_PARENT, dip2px(context, 30f))
layoutParams = headParams
orientation = HORIZONTAL
setBackgroundColor(ContextCompat.getColor(context,R.color.color_F1F5F8))
setPadding(dip2px(context, 15f), 0, dip2px(context, 15f), 0)
}
val list = listOf("日", "一", "二", "三", "四", "五", "六")
val itemParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)
for(i in list){
val tv = TextView(context).apply {
layoutParams = itemParams
gravity = Gravity.CENTER
setTextColor(ContextCompat.getColor(context,R.color.color_92a0aa))
setTextSize(TypedValue.COMPLEX_UNIT_SP, 13f)
text = i
}
week.addView(tv)
}
复制代码
做的比较简陋,顶部星期的显示写成了一个固定的值,如果配置成xml属性,可以通过属性去控制每周的起始日是星期天还是星期一
日期显示
需要显示日期的话,我们得把需要构造一个List,将所有需要显示的数据放进List,再将它放到RecyclerView的Adapter里。因为日期分月和每天,因此我们需要两个dataBean,一个是月份的Bean,一个是每天日期的Bean。
那么如何构建这个List呢?
每个月的数据是一个MonthBean,里面包含年份,月份,以及一个List,List内包含的是这个月的每天的DateBean。每日的DateBean,需要的数据有,年费,月份,日期,同时因为置灰以及点击的判断,我们还需要一些布尔值去判断。那么这样基本可以确定MonthBean和DateBean的类型,大致代码如下:
MonthBean
data class MonthBean(var year: Int, var month: Int, var dateList: MutableList<DateBean> = mutableListOf())
复制代码
DateBean
data class DateBean(var year: Int = 2019, var month: Int, var day: Int, var type: Int,
var isToday: Boolean = false, //是否是当天
var isChooseDay: Boolean = false, // 是否是选择日期
) {
// 分组
val groupName: String
get() {
val sMonth = if (month < 10) String.format("0%d", month) else String.format("%d", month)
return year.toString() + "年" + sMonth + "月"
}
// 当日的准确日期
val date: String
get() {
val sMonth = if (month < 10) String.format("0%d", month) else String.format("%d", month)
val sDate = if (day < 10) String.format("0%d", day) else String.format("%d", day)
return year.toString() + sMonth + sDate
}
}
复制代码
接下来我们就可以构建Adapter所需要的list了
val calendar = Calendar.getInstance()
calendar.add(Calendar.MONTH, -MAX_MONTH_COUNT + 1)
for (i in 0 until MAX_MONTH_COUNT) {
val year = calendar.get(Calendar.YEAR)
val month = calendar.get(Calendar.MONTH) + 1
val bean = MonthInfoBean(year, month)
monthList.add(bean)
calendar.add(Calendar.MONTH, 1)
}
复制代码
MAX_MONTH_COUNT指的是最多显示的月份,然后把需要显示的月份的数据add到Calendar的实例内,monthList是需要展示的月份的MonthBean的List,之后再循环给monthList内添加dateList,同时通过Calendar的计算,去添加月份初始以及月份结束的空白的占位符,代码较长,具体的代码之后可以看附上的github链接。 通过上面的循环,我们可以构造出一个含有空白占位符的每日的List,之后我们再给它添加title,作为每个月份的title。
构建对应的Adapter和Decoration
完成了List的创建,我们就可以创建对应的Adapter以及Decoration。Adapter内我们需要区分View的种类,月份的title,空白日期的占位符,以及显示每日的数据。
Adapter内我们可以定义三种ViewType,去绘制不同的item。分割线和悬浮的title只需要重写RecyclerView.ItemDecoration内的onDrawOver函数,即可做到,下面是大致代码
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDrawOver(c, parent, state)
val manager = parent.layoutManager as GridLayoutManager
val position = manager.findFirstVisibleItemPosition()
if (position == RecyclerView.NO_POSITION) {
return
}
val viewHolder = parent.findViewHolderForAdapterPosition(position)
val item = viewHolder?.itemView
var flag = false
// 判断下一个title是否滑动上来了
if (isLast(position) && null != item) {
if (item.height + item.top < top) {
c.save()
flag = true
c.translate(0f, (item.height + item.top - top).toFloat())
}
}
// 绘制title
val rect = RectF(
0f,
parent.paddingTop.toFloat(),
parent.right.toFloat(),
(parent.paddingTop + top).toFloat())
c.drawRect(rect, mPaint)
c.drawText(mCallBack(position),
rect.centerX(),
rect.centerY() + mTopPadding,
mTextPaint)
if (flag) {
c.restore()
}
}
复制代码
mCallBack是根据position取对应item的分组的回调函数,isLast是判断当前的位置是不是处于最后一排。 之后将Adapter和Decoration加到对应的RecyclerView内就可以完成了。
效果
大致效果如下gif
总结
这种方式实现的日历,功能比较简单,也没有特别多的扩展功能,而且因为使用的是RecyclerView,如果需要显示的数据多了,代码性能是不太OK的,而且构造日历的部分的代码是循环套循环,时间复杂度上也是非常低效的。也就是抠脚写法,代码写的也挺简陋,具体细节可以参考github链接,欢迎各位拿去修改使用,提出各种意见啥的。
最后放上项目的github地址 链接
参考博客和项目