android 自定义简单日历功能

60 阅读3分钟

微信截图_20230425102815.png

简单思路:

1.头部的左右切换和日期显示是卸载布局中的

2.日历的头部

3.每一个方格的白色背景

4.显示日历

5.显示标记点

自定义view `

class MyCalendarView : View {

    private val mContext: Context

    private var mWidth = 0

    //当前年月
    private var nowYear = 0
    private var nowMonth = 0

    //是否是当前月份
    private var isNowMonth = true

    //当前年份
    var showYear = 0

    //当前月份
    var showMonth = 0

    //当前天
    private var showDay = 0

    //当前选择的天
    private var selectDay = 0

    //上一年份
    private var lastYear = 0

    //上月
    private var lastMonth = 0

    //下一年
    private var nextYear = 0

    //下月
    private var nextMonth = 0

    //日期的宽高
    private var rowHeight = 0
    private var columnWidth = 0

    //当前月份第一天是星期几
    private var firstDayInMonth = 0

    //行数
    var row = 0

    //列数
    var column = 0

    //文字画笔
    private var textPaint: Paint

    //背景画笔
    private var bgPaint: Paint

    //标记点画笔
    private var pointPaint: Paint

    //头部高度
    private var titleHeight = 0

    //白色背景方块画笔
    private var whiteBgPaint: Paint

    //间隙
    private var gap = 0
    //背景块行数
    private var gapRow = 0
    //背景块列数
    private var gapColunm = 0

    //头部背景画笔
    private var titleBgPaint: Paint


    private val days = mutableListOf<DayEnity>()
    private val weeks = mutableListOf<String>("日", "一", "二", "三", "四", "五", "六")

    //控制标记点的数据
    private var pointData = mutableListOf<MonthlyDataListBean>()

    //点击的x y坐标
    private var downX = 0f
    private var downY = 0f

    //抬起的x y 坐标
    private var touchIndexX = 0
    private var touchIndexY = 0

    //回调监听
    private var mCallback: ((String) -> Unit)? = null

    constructor(context: Context) : this(context, null)

    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)

    constructor(context: Context, attrs: AttributeSet?, style: Int) : super(context, attrs, style) {
        this.mContext = context

        titleHeight = DisplayUtils.dp2px(context, 40f)
        gap = DisplayUtils.dp2px(context, 1f)

        textPaint = Paint()
        textPaint.isAntiAlias = true
        textPaint.style = Paint.Style.FILL
        textPaint.textSize = DisplayUtils.sp2px(mContext, 16f).toFloat()

        bgPaint = Paint()
        bgPaint.isAntiAlias = true
        bgPaint.color = ContextCompat.getColor(context, R.color.blue_9ac)

        pointPaint = Paint()
        pointPaint.isAntiAlias = true
        pointPaint.style = Paint.Style.FILL
        pointPaint.color = ContextCompat.getColor(context, R.color.red_151)

        titleBgPaint = Paint()
        titleBgPaint.isAntiAlias = true
        titleBgPaint.style = Paint.Style.FILL
        titleBgPaint.color = ContextCompat.getColor(context, R.color.gray_f5)

        whiteBgPaint = Paint()
        whiteBgPaint = Paint()
        whiteBgPaint.isAntiAlias = true
        whiteBgPaint.style = Paint.Style.FILL
        whiteBgPaint.color = ContextCompat.getColor(context, R.color.white)

        initDays()
    }

    //初始化日期
    private fun initDays() {

        val calendar = Calendar.getInstance()
        calendar.timeZone = TimeZone.getTimeZone("GMT+8:00")
        showYear = calendar.get(Calendar.YEAR)
        showMonth = calendar.get(Calendar.MONTH)
        nowYear = calendar.get(Calendar.YEAR)
        nowMonth = calendar.get(Calendar.MONTH)
        selectDay = calendar.get(Calendar.DAY_OF_MONTH)
        showDay = calendar.get(Calendar.DAY_OF_MONTH)
//        Log.e("TAG", "showDay----------->$showDay")
        lastYear = showYear
        lastMonth = showMonth - 1
        nextYear = showYear
        nextMonth = showMonth + 1

        firstDayInMonth = CalendarUtil.getFirstDayOfMonth(showYear, showMonth)
//        Log.e("TAG", "showYear-----$showYear------showMonth------$showMonth------$firstDayInMonth")
        days.addAll(CalendarUtil.getDays(showYear, showMonth))


        days.forEachIndexed { index, dayEnity ->
            if (showDay == dayEnity.day) {
//                Log.e("TAG","index----------->$index")
                touchIndexX = (index + firstDayInMonth) / 7
                touchIndexY = (index + firstDayInMonth - 2) % 7
            }
        }
        for (i in 0 until days.size) {
            if (showDay == days[i].day) {
//                Log.e("TAG","index----------->$index")
                touchIndexX = ((i + firstDayInMonth - 1) % 7)
                touchIndexY = (i + firstDayInMonth - 1) / 7 + 1
            }
        }

    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        //行高
//        rowHeight = (resources.displayMetrics.density * 40).toInt()
        rowHeight = MeasureSpec.getSize(widthMeasureSpec - gap * 8) / 7
        //列宽
        columnWidth = (MeasureSpec.getSize(widthMeasureSpec) - gap * 8) / 7
        mWidth = MeasureSpec.getSize(widthMeasureSpec)
        
        //根据天数和每月第一天是周几计算一共需要几列展示
        val h = (days.size + firstDayInMonth - 1) / 7 + 1


        setMeasuredDimension(
            MeasureSpec.getSize(widthMeasureSpec),
            h * columnWidth + (h + 1) * gap+titleHeight
        )
        Log.e("TAG", "设置高度")
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        
        //绘制灰色背景
        canvas.drawRect(0f, 0f, mWidth.toFloat(), mWidth.toFloat(), titleBgPaint)

        //绘制头部
        drawTitle(canvas)

        //绘制背景块
        drawBgRect(canvas)

        //绘制日期
        drwaDays(canvas)


    }


    private fun drawTitle(canvas: Canvas) {

        for (i in 0 until 7) {
            val weekText = weeks[i]
            if(weekText=="日"||weekText=="六"){
                textPaint.color = ContextCompat.getColor(mContext,R.color.color_EC7DB7)
            }else{
                textPaint.color = Color.BLACK
            }

            val rect = Rect()
            textPaint.getTextBounds(weekText, 0, weekText.length, rect)
            val textY = (titleHeight / 2 - (textPaint.ascent() + textPaint.descent()) / 2)
            val textX = columnWidth * i + (columnWidth - textPaint.measureText(weekText)) / 2
            canvas.drawText(weekText, textX, textY, textPaint)
        }
    }

    private fun drawBgRect(canvas: Canvas) {
        gapRow = (days.size + firstDayInMonth - 1) / 7 + 1
        gapColunm = 7
        Log.e("TAG", "行数------------->$gapRow")
        val rect = RectF()
        for (i in 0 until gapRow) {
            for (j in 0 until gapColunm) {

                rect.left = (gap * (j + 1) + columnWidth * j).toFloat()
                rect.top = (gap * (i + 1) + titleHeight + columnWidth * i).toFloat()
                rect.right = (gap * (j + 1) + columnWidth * (j + 1)).toFloat()
                rect.bottom = (gap * (i + 1) + columnWidth * (i + 1) + titleHeight).toFloat()
                canvas.drawRoundRect(rect, 10f, 10f, whiteBgPaint)

            }

        }

    }

    private fun drwaDays(canvas: Canvas) {

        for (i in 0 until days.size) {
            val day = days[i].day
            val dayText = day.toString()
            column = (i + firstDayInMonth - 1) / 7
            row = ((i + firstDayInMonth - 1) % 7)


            val textWidth = textPaint.measureText(dayText)
            val rect = Rect()
            textPaint.getTextBounds(dayText, 0, dayText.length, rect)
            val textHeight = rect.bottom - rect.top
            val textX = (columnWidth * row) + (columnWidth - textWidth) / 2 + gap * (row + 1)
            val textY =
                (rowHeight * column + titleHeight) + (rowHeight + textHeight) / 2 + gap * (column + 1)

            if (row == touchIndexX && (column + 1) == touchIndexY) {
                //绘制背景
                val bgRect = RectF()
                bgRect.left = ((columnWidth * row) + gap * (row + 1)).toFloat()
                bgRect.top = ((rowHeight * column + titleHeight) + gap * (column + 1)).toFloat()
                bgRect.right = ((columnWidth * row) + gap * (row + 1) + columnWidth).toFloat()
                bgRect.bottom =
                    ((rowHeight * column + titleHeight) + gap * (column + 1) + rowHeight).toFloat()
                canvas.drawRoundRect(bgRect, 10f, 10f, bgPaint)
            }

            if (days[i].day == showDay && isNowMonth) {//如果是当天
                textPaint.color = ContextCompat.getColor(mContext, R.color.blue_007)
            } else if (days[i].month == showMonth) {//如果是当月
                textPaint.color = Color.BLACK
            } else {
                textPaint.color = Color.GRAY
            }

            canvas.drawText(dayText, textX, textY.toFloat(), textPaint)

            if (pointData.size > 0 && i < pointData.size) {
                val pointX = columnWidth * row + columnWidth / 2 + gap * (row + 1)
                val pointY =
                    (rowHeight * column + titleHeight) + (rowHeight + textHeight) / 2 + DisplayUtils.dp2px(
                        mContext,
                        10f
                    ) + gap * (column + 1)


                if (pointData[i].state == 0) {
                    //绘制点
                    pointPaint.color = ContextCompat.getColor(mContext, R.color.blue_007)
                    canvas.drawCircle(pointX.toFloat(), pointY.toFloat(), 5f, pointPaint)
                } else if (pointData[i].state == 1) {
                    //绘制点
                    pointPaint.color = ContextCompat.getColor(mContext, R.color.red_151)
                    canvas.drawCircle(pointX.toFloat(), pointY.toFloat(), 5f, pointPaint)
                }

            } else {
                val pointX = columnWidth * row + columnWidth / 2
                val pointY =
                    (rowHeight * column + titleHeight) + (rowHeight + textHeight) / 2 + DisplayUtils.dp2px(
                        mContext,
                        10f
                    )
                pointPaint.color = Color.TRANSPARENT
                canvas.drawCircle(pointX.toFloat(), pointY.toFloat(), 5f, pointPaint)
            }
        }

    }

    override fun onTouchEvent(event: MotionEvent): Boolean {

        when (event.action) {

            MotionEvent.ACTION_DOWN -> {
                downX = event.x
                downY = event.y

            }
            MotionEvent.ACTION_MOVE -> {
            }
            MotionEvent.ACTION_UP -> {
                val upX = event.x
                val upY = event.y
                touchIndexX = (upX / columnWidth).toInt()
                touchIndexY = if (upY > 0 && upY < titleHeight) {
                    0
                } else {
                    ((upY - titleHeight) / rowHeight + 1).toInt()
                }
//                Log.e("TAG", "$touchIndexX----------$touchIndexY")
                val index = (touchIndexY - 1) * 7 + touchIndexX
                Log.e("TAG", "index---------------$index")
                if (index >= 0 && index < days.size) {
                    selectDay = days[index].day

                }
                if (touchIndexX >= 0 && touchIndexX <= 7 && touchIndexY >= 1 && touchIndexY <= 6) {
                    val count = (touchIndexY - 1) * 7 + touchIndexX - firstDayInMonth + 1
                    if (count >= 0 && count < days.size) {
                        selectDay = days.get(count).day
                        if (mCallback != null) {
                            mCallback?.let { it("$showYear-${showMonth + 1}-$selectDay") }
                        }
                        invalidate()
                    }

                }

            }

        }

        return true
    }

    //上一个月
    fun toLastMonth() {
        if (showMonth == 0) {//当年1月,上一月则为12月,年份减一
            showMonth = 11
            showYear--
        } else {
            showMonth--
        }
        days.clear()
        days.addAll(CalendarUtil.getDays(showYear, showMonth))
        isNowMonth = nowYear == showYear && nowMonth == showMonth

        firstDayInMonth = CalendarUtil.getFirstDayOfMonth(showYear, showMonth)
        touchIndexX = -1
        touchIndexY = -1
        invalidate()
    }

    //下一个月
    fun toNextMonth() {
        if (showMonth == 11) {//当年12月,下一月则为1月,年份减加一
            showMonth = 0
            showYear++
        } else {
            showMonth++
        }
        days.clear()
        days.addAll(CalendarUtil.getDays(showYear, showMonth))
        isNowMonth = nowYear == showYear && nowMonth == showMonth
        firstDayInMonth = CalendarUtil.getFirstDayOfMonth(showYear, showMonth)


        touchIndexX = -1
        touchIndexY = -1
//        invalidate()
        requestLayout()
    }
    
    //设置标记点
    fun setPointData(datas: MutableList<MonthlyDataListBean>) {
        pointData.clear()
        pointData.addAll(datas)
        requestLayout()
//        invalidate()
    }
    
    //点击日历回调监听
    fun setCallback(callbak: (String) -> Unit) {
        this.mCallback = callbak
    }

}

工具类

object CalendarUtil {


    //获取当月第一天是星期几
    fun getFirstDayOfMonth(showYear: Int, showMonth: Int): Int {
        val calendar = Calendar.getInstance()
        calendar.set(showYear, showMonth, 1)
        Calendar.JANUARY
//        calendar.set(Calendar.DAY_OF_MONTH,1)
//        Log.e("TAG","----------->${calendar.get(Calendar.DAY_OF_MONTH)}")
//        Log.e("TAG","----------->${calendar.get(Calendar.DAY_OF_YEAR)}")
        return calendar.get(Calendar.DAY_OF_WEEK)
    }

    //获取当前月份有多少天
    fun checkDays(showYear: Int, showMonth: Int): Int {
        var day = 0
        day = if (showYear % 4 == 0 && showYear % 100 != 0 || showYear % 400 == 0) {
            29
        } else {
            28
        }
        when (showMonth) {
            1, 3, 5, 7, 8, 12 -> {
                return 31
            }
            4, 6, 9, 11 -> {
                return 30
            }
            2 -> {
                return day
            }
        }
        return 0
    }

    //获取当前年月天数集合
    fun getDays(showYear: Int, showMonth: Int): MutableList<DayEnity> {
        val days = mutableListOf<DayEnity>()
        val count = checkDays(showYear, showMonth+1)
        for (i in 0 until count) {
            val entity = DayEnity(showYear, showMonth, i + 1)
            days.add(entity)
        }
        return days
    }

    //判断是否是当天日期
    fun  isDateTody(dateString:String):Boolean{
        val sdf = SimpleDateFormat("yyyy-MM-dd")
        try {
            val inputDate = sdf.parse(dateString)
            val inputCalendar = Calendar.getInstance()
            inputCalendar.setTime(inputDate)
            val todayCalendar = Calendar.getInstance()
            return (inputCalendar.get(Calendar.YEAR) == todayCalendar.get(Calendar.YEAR))
                    && (inputCalendar.get(Calendar.MONTH) == todayCalendar.get(Calendar.MONTH))
                    && (inputCalendar.get(Calendar.DAY_OF_MONTH) == todayCalendar.get(Calendar.DAY_OF_MONTH))
        }catch (e:Exception){
            return false
        }
    }

}