由于公司需求,需要一个日历控件,本来想用第三方的,但是好像没有第三方东西满足这个需求和样式,于是自己就撸了一个日历控件出来,这个给分享一下怎么撸出来的
如果觉得看博客有点无聊的话,直接看git地址:https://github.com/yinjinyj/SuperCalendar,使用方法:
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
dependencies {
compile 'com.github.yinjinyj:SuperCalendar:1.0.5'
}
你的star是对我最大的支持
一,先看效果图
这个主要是当前日期和过期日期的color不同,当前日期要求加粗
这个按天买买过的并且过期的样式,和选中要买的日期
这个是,按月买(按季节买是一样的逻辑),要买的样式(里面存在按天买买过的并且过期的样式和按天买买过的并且为过期的样式)
这个是按月或者季买,买过并且未过期的样式
二、实现方式
1,我先画最上面的周(日、一....六)
2,然后就开始画天
这里先说一下贴一下每个天的位置;由于多重循环,所以我打算用空间换时间的方法来减少UI卡顿。
然后计算日期,并且给相应的日期设置相应的颜色
/** * 画里面的日期 */ private fun createMonthContentData() { circleBitmapBeanListByDay.clear() circleBitmapBeanDay.clear() circleBitmapBeanListBySelectingDay.clear() circleBitmapBeanListByMonth.clear() circleBitmapBeanListBySelectingMonth.clear() //当天是星期几 val firstDayWeek = CalendarUtil.getFirstDayWeek(year, month) //上一个月的总天数 val daysLastMonth = if (month == 1) { CalendarUtil.getDaysInMonth(year, 12) } else { CalendarUtil.getDaysInMonth(year, month - 1) } //当前月的总天数 val daysCurrentMonth = CalendarUtil.getDaysInMonth(year, month) //获取当月的行数(吧上个月的加上) var rowNumber = (daysCurrentMonth + firstDayWeek - 1) / 7 val remainder = (daysCurrentMonth + firstDayWeek - 1) % 7 if (remainder > 0) { rowNumber += 1 } viewHeight = (rowNumber * lineHeight + weekTextSize + dayMarinWeekSize + 10).toInt() //每次进行onDraw的时候清空里面的数据,防止重叠 TouchManager.monthDayBeanRect.clear() TouchManager.monthAllDayBean.clear() when (firstDayWeek) { 1 -> { for (i in 1..rowNumber) { for (k in firstDayWeek..7) { val dayBean = DayBean() //画日期 if ((k + (i - 1) * 7) > daysCurrentMonth) { textContent = ((k + (i - 1) * 7) - daysCurrentMonth).toString() //画圈 当前月份为12 下一个月就是1月 if (month == 12) { dayBean.year = year + 1 dayBean.month = 1 } else { dayBean.year = year dayBean.month = month + 1 } dayBean.day = ((k + (i - 1) * 7) - daysCurrentMonth) //设置画笔颜色 当前选择的月的下一个月 val circleBitmapBean = CircleBitmapBean() createCalendarData(dayBean, circleBitmapBean, k, i, false) } else { textContent = (k + (i - 1) * 7).toString() //画圈 dayBean.year = year dayBean.month = month dayBean.day = ((k + (i - 1) * 7)) //设置画笔颜色 当前选择的月 val circleBitmapBean = CircleBitmapBean() createCalendarData(dayBean, circleBitmapBean, k, i, true) } } } } else -> { //画第一行 for (k in 1..7) { val dayBean = DayBean() if (firstDayWeek > k) { textContent = (daysLastMonth + k - (firstDayWeek - 1)).toString() //画圈 月份为1 上一个月就是12月 if (month == 1) { dayBean.year = year - 1 dayBean.month = 12 } else { dayBean.year = year dayBean.month = month - 1 } dayBean.day = (daysLastMonth + k - (firstDayWeek - 1)) //设置画笔颜色 当前选择的月的下一个月 val circleBitmapBean = CircleBitmapBean() createCalendarData(dayBean, circleBitmapBean, k, 1, false) } else { textContent = (k - firstDayWeek + 1).toString() //画圈 dayBean.year = year dayBean.month = month dayBean.day = (k - firstDayWeek + 1) //设置画笔颜色 当前选择的月 val circleBitmapBean = CircleBitmapBean() createCalendarData(dayBean, circleBitmapBean, k, 1, true) } } //画剩下的几行 for (i in 2..rowNumber) { for (k in 1..7) { val dayBean = DayBean() textContent = (((k - firstDayWeek + 1) + (i - 1) * 7) - daysCurrentMonth).toString() //设置画笔颜色 当前选择的月的下一个月 if (((k - firstDayWeek + 1) + (i - 1) * 7) > daysCurrentMonth) { //画圈 当前月份为12 下一个月就是1月 if (month == 12) { dayBean.year = year + 1 dayBean.month = 1 } else { dayBean.year = year dayBean.month = month + 1 } dayBean.day = (((k - firstDayWeek + 1) + (i - 1) * 7) - daysCurrentMonth) //设置画笔颜色 当前选择的月的下一个月 val circleBitmapBean = CircleBitmapBean() createCalendarData(dayBean, circleBitmapBean, k, i, false) } else { textContent = ((k - firstDayWeek + 1) + (i - 1) * 7).toString() //画圈 dayBean.year = year dayBean.month = month dayBean.day = ((k - firstDayWeek + 1) + (i - 1) * 7) //设置画笔颜色 当前选择的月的下一个月 val circleBitmapBean = CircleBitmapBean() createCalendarData(dayBean, circleBitmapBean, k, i, true) } } } } } }
所谓的控件就是用一个数据来进行存储要画的属性
data class WeekBean(val weekPaintColor: Int, val content: String) data class CircleBitmapBean(var dayPaintColor: Int? = 0, var dayPaintTypeface: Typeface? = Typeface.DEFAULT, var circlePaintColor: Int? = 0, var type: Int? = -1, var circleX: Float? = 0F, var circleY: Float? = 0f, var textX: Float? = 0F, var textY: Float? = 0f, var k: Int? = 0, var lineStartX: Float? = 0f, var lineCenterY: Float? = 0f, var lineEndX: Float? = 0f, var content: String? = null, var dayBean: DayBean? = null, var bitmap: Bitmap? = null, var dst: Rect? = null, var leftDayRect: Rect? = null, var rightDayRect: Rect? = null, var centerDayRect: Rect? = null, var circleDayRect: RectF? = null)
者就是画天和画周需要的属性,然后把这个对象放到集合里面,最后遍历来画
大体思路是这样的,下面就简单的说一下怎么画连起来的圆角矩形
首先我是对每个天来进行维护的而不是画一个整体的圆角矩形,因为这样做的话,逻辑比较清晰,维护和扩展要强一点,虽然代码比较多和onDraw比较费时一点。
我先判断要画的日期在什么位置
/** *日期是否在该区间 * @return 1:在区间并且过期 ;2在区间未过期且为当前日期;3在区间未过期且不为当前日期; * 4在起始位置并且过期,5在起始位置并且未过期且为当前日期,6在起始位置并且未过期且不为当前日期, * 7在结束位置并且过期,8在结束位置并且未过期且为当前时间,9在结束位置并且未过期且为不当前时间 */ fun compareTwoDayBean(dayBean: DayBean, startDayBean: DayBean, currentDayBean: DayBean?, buyType: BuyType): Int { val dateTime = dayBean2Date(dayBean)!!.time val startDateTime = dayBean2Date(startDayBean)!!.time val currentDate = dayBean2Date(currentDayBean!!)!!.time var endTime: Long? = 100L when (buyType) { BuyType.MONTH -> { endTime = startDateTime + 29 * 24 * 60 * 60 * 1000L } BuyType.SEASON -> { endTime = startDateTime + 89 * 24 * 60 * 60 * 1000L } } return when (dateTime) { in (startDateTime + 1)..(endTime!! - 1) -> when { dateTime < currentDate -> //在区间并且过期 1 dateTime == currentDate -> //在区间未过期且为当前日期 2 else -> //在区间未过期且不为当前日期 3 } startDateTime -> when { startDateTime < currentDate -> //在起始位置并且过期 4 startDateTime == currentDate -> //在起始位置并且未过期且为当前日期 5 else -> //在起始位置并且未过期且不为当前日期 6 } endTime -> when { endTime < currentDate -> //在结束位置并且过期 7 endTime == currentDate -> //在结束位置并且未过期且为当前时间 8 else -> //在结束位置并且未过期且为不当前时间 9 } else -> 10 } }
然后不同的位置坐不同得draw
/** * 画选择过的日期(月和季) */ private fun drawSelectedDateByMonthOrSeason(circleBitmapBean: CircleBitmapBean, canvas: Canvas?) { when (circleBitmapBean.type) { 2 -> { circlePaint.color = circleBitmapBean.circlePaintColor!! when (circleBitmapBean.k) { 7 -> { drawCircle(canvas, circleBitmapBean.circleX!!, circleBitmapBean.circleY!!, selectedDateRadius, circlePaint) } else -> { drawLeftRect(canvas, circleBitmapBean) } } drawBitmap(canvas, circleBitmapBean.dst!!, circleBitmapBean.bitmap) } 3 -> { when (circleBitmapBean.k) { 1 -> { drawLeftRect(canvas, circleBitmapBean) } 7 -> { drawRightRect(canvas, circleBitmapBean) } else -> { drawCenterRect(canvas, circleBitmapBean) } } drawBitmap(canvas, circleBitmapBean.dst!!, circleBitmapBean.bitmap) } 5 -> { circlePaint.color = circleBitmapBean.circlePaintColor!! when (circleBitmapBean.k) { 7 -> { drawCircle(canvas, circleBitmapBean.circleX!!, circleBitmapBean.circleY!!, selectedDateRadius, circlePaint) } else -> { drawLeftRect(canvas, circleBitmapBean) } } drawBitmap(canvas, circleBitmapBean.dst!!, circleBitmapBean.bitmap) } 6 -> { circlePaint.color = circleBitmapBean.circlePaintColor!! when (circleBitmapBean.k) { 7 -> { drawCircle(canvas, circleBitmapBean.circleX!!, circleBitmapBean.circleY!!, selectedDateRadius, circlePaint) } else -> { drawLeftRect(canvas, circleBitmapBean) } } drawBitmap(canvas, circleBitmapBean.dst!!, circleBitmapBean.bitmap) } 8 -> { circlePaint.color = circleBitmapBean.circlePaintColor!! when (circleBitmapBean.k) { 1 -> { drawCircle(canvas, circleBitmapBean.circleX!!, circleBitmapBean.circleY!!, selectedDateRadius, circlePaint) } else -> { drawRightRect(canvas, circleBitmapBean) } } drawBitmap(canvas, circleBitmapBean.dst!!, circleBitmapBean.bitmap) } 9 -> { circlePaint.color = circleBitmapBean.circlePaintColor!! when (circleBitmapBean.k) { 1 -> { drawCircle(canvas, circleBitmapBean.circleX!!, circleBitmapBean.circleY!!, selectedDateRadius, circlePaint) } else -> { drawRightRect(canvas, circleBitmapBean) } } drawBitmap(canvas, circleBitmapBean.dst!!, circleBitmapBean.bitmap) } 1, 4, 7 -> { circlePaint.color = circleBitmapBean.circlePaintColor!! drawCircle(canvas, circleBitmapBean.circleX!!, circleBitmapBean.circleY!!, selectedDateRadius, circlePaint) drawBitmap(canvas, circleBitmapBean.dst!!, circleBitmapBean.bitmap) } } }
基本上思路就是这个样子
3,画完之后要处理点击事件了,点击事件肯定要重写onTouchEvent
override fun onTouchEvent(event: MotionEvent?): Boolean { when (event?.action) { MotionEvent.ACTION_DOWN -> { if (isEnableTouch) { startX = event.x startY = event.y } return true } MotionEvent.ACTION_UP -> { try { //判断是否可以点击,方便进行回调 var isClick = true //判断用户是点击还是滑动 if (isEnableTouch && Math.abs(event.x - startX) <= ViewConfiguration.get(context).scaledTouchSlop && Math.abs(event.y - startY) <= ViewConfiguration.get(context).scaledTouchSlop) { //便利循环找到点击区域对应的日期 TouchManager.monthDayBeanRect.forEach { if (it.contains(event.x.toInt(), event.y.toInt())) { val dayBean = TouchManager.monthAllDayBean[TouchManager.monthDayBeanRect.indexOf(it)] dayBean.type = DataManger.useBuyType //判断日期是否在可点击的范围内 //通过选择类型来获取选择时间的显示 val limitDay = when (DataManger.useBuyType) { BuyType.DAY -> { 2 } BuyType.MONTH, BuyType.SEASON -> { 8 } } val dayBeanState = when { dayBean.year!! > currentYear -> DayState.ENABLE dayBean.year!! == currentYear -> when { dayBean.month!! > currentMonth -> DayState.ENABLE dayBean.month!! == currentMonth -> when { dayBean.day!! > currentDay + limitDay -> DayState.ENABLE dayBean.day!! == currentDay -> DayState.CURRENT else -> DayState.NOT_ENABLE } else -> DayState.NOT_ENABLE } else -> DayState.NOT_ENABLE } //日期不可点击 if (dayBeanState != DayState.ENABLE) { isClick = false } //根据不同的type做相应的逻辑 when (DataManger.useBuyType) { BuyType.DAY -> { //按天选选中的日期(天)里面有这个日期 if (DataManger.selectedDateByDay.isNotEmpty() && isClick) { if (DataManger.selectedDateByDay.contains(dayBean)) { isClick = false } } //按天选正在选的日期(月或者季)里面有这个日期 if (DataManger.selectedDayByMonthOrSeason.isNotEmpty() && isClick) { if (!DateUtil.isSelect(dayBean)) { isClick = false } } } BuyType.MONTH, BuyType.SEASON -> { //按月或者季选正在选的日期(天)里面是否包含已选过的日期 if (DataManger.selectedDateByDay.isNotEmpty() && isClick) { if (!DateUtil.isSelectByMonth(dayBean)) { isClick = false } } //按月或者季选正在选的日期(月或者季)里面是否包含已选过的日期 if (DataManger.selectedDayByMonthOrSeason.isNotEmpty() && isClick) { if (!DateUtil.isSelect2ByMonth(dayBean)) { isClick = false } } } } //是否可以点击并进行回调 if (isClick) { when (DataManger.useBuyType) { BuyType.DAY -> { //先判断是否选中,没有选中就添加进去,选中了就去掉 if (DataManger.selectingDateByDay.contains(dayBean)) { DataManger.selectingDateByDay.remove(dayBean) } else { DataManger.selectingDateByDay.add(dayBean) } } BuyType.MONTH, BuyType.SEASON -> { //直接清空里面数据,原因是只能选中一次,然后提交,然后才能再选 DataManger.selectingDayByMonthOrSeason.clear() DataManger.selectingDayByMonthOrSeason.add(dayBean) } } monthViewClick?.click(dayBean, DataManger.useBuyType, id) refreshView() } else { isEnableTouch = true monthViewClick?.unClick(dayBean, DataManger.useBuyType, "您已经选过该日期") } } } } } catch (e: Exception) { Log.e("...", "产生了ConcurrentModificationException异常,让用户重新选") } return true } } return super.onTouchEvent(event) }
处理过程就是通过点击的xy判断属于哪个日期,然后在判断是否可以点击该日期(比如选者过的就不能在选择了)
差不多大题就这样
三、心得
拿到一个需要自定义View的需求是,先分析从哪儿开始画,然后怎么去实现好一点,比如我看到这个自定义日历的时候,
我肯定会先画上面的周,然后画下面的天,由于不同的日期有不同的状态,所以我打算建一个实体类来维护每一个状态。
把每一个要画的属性放进一个该实体里面,最后都放进一个集合里面,这样做的好处是,计算逻辑和画的逻辑分开了,
然后就直接遍历这个集合就可以进行相应的draw