美文网首页Android开发首页投稿(暂停使用,暂停投稿)@IT·互联网
一步一步构建自己的简单日历控件 MySimpleCalendar

一步一步构建自己的简单日历控件 MySimpleCalendar

作者: 涤生_Woo | 来源:发表于2018-01-26 17:27 被阅读180次

    一、概述

    接着上一篇文章:一步一步构建自己的简单日历控件 MySimpleCalendar(篇一)

    SimpleCalendarView

    上一篇的实现方式为:

    • 自定义 ViewGroup(MonthCalendarView)
    • 控件高度在 onMeasure() 方法中动态计算
    • 日期 item 通过 addViewInLayout() 方法,将构建的一个一个 View 添加到 ViewGroup 中
    • 日期 item 在 onMeasure() 方法中计算其宽高,并在 onLayout() 方法中布局坐标

    现在换一种实现方式:

    • 自定义 View(MonthCalendarView2)
    • 控件高度在 onMeasure() 方法中根据宽度写死
    • 日期 item 在 onSizeChanged() 方法里确定宽度
    • 在 onDraw() 方法里根据日期的排布确定 item 的高度,再借助 Region 直接绘制每一个日期 item

    二、自定义View

    上一篇文章已经做了比较详细的解释,这一篇就只做重点分析,下面只列出关键代码

    1、数据准备和工具类

    数据准备和工具类直接沿用上一篇文章,工具类这里只新增了下面一个方法:

        /**
         * 获取当前月份的周数
         * @param year
         * @param month
         * @return
         */
        public int getWeekCountsOfMonth(int year, int month) {
            int lastDayOfMonth = getDaysOfCertainMonth(year, month);
            Calendar calendar = Calendar.getInstance();
            calendar.set(year, month - 1, lastDayOfMonth);
            return calendar.get(Calendar.WEEK_OF_MONTH);
        }
    

    其他的在这里将代码直接贴出来:

    package com.example.deesonwoo.mysimplecalendar.calendar;
    
    /**
     * Created by deeson.woo
     */
    public class MyCalendarBean {
    
        private int year;
        private int month;//1-12
        private int day;//1-31
        private boolean isCurrentMonth = true;//是否为当前月份的日期
    
        public MyCalendarBean(int year, int month, int day) {
            this.year = year;
            this.month = month;
            this.day = day;
        }
    
        public int getYear() {
            return year;
        }
    
        public int getMonth() {
            return month;
        }
    
        public int getDay() {
            return day;
        }
    
        public boolean isCurrentMonth() {
            return isCurrentMonth;
        }
    
        public void setCurrentMonth(boolean currentMonth) {
            isCurrentMonth = currentMonth;
        }
    
        @Override
        public String toString() {
            return year + "/" + month + "/" + day;
        }
    }
    
    package com.example.deesonwoo.mysimplecalendar.calendar;
    
    import android.content.Context;
    
    import java.util.ArrayList;
    import java.util.Calendar;
    import java.util.Date;
    import java.util.List;
    import java.util.TimeZone;
    
    /**
     * Created by deeson.woo
     */
    
    public class MyCalendarUtils {
    
        private Context mContext;
    
        public MyCalendarUtils(Context context) {
            this.mContext = context;
        }
    
        /**
         * 获取具体月份的最大天数
         *
         * @param year
         * @param month
         * @return
         */
        public static int getDaysOfCertainMonth(int year, int month) {
            Calendar calendar = Calendar.getInstance();
            calendar.set(year, month - 1, 1);
            return calendar.getActualMaximum(Calendar.DATE);
        }
    
        /**
         * 获取当前月份的日期列表
         *
         * @param year
         * @param month
         * @return
         */
        public List<MyCalendarBean> getDaysListOfMonth(int year, int month) {
    
            List<MyCalendarBean> list = new ArrayList<>();
    
            int daysOfMonth = getDaysOfCertainMonth(year, month);
    
            //找到当前月第一天的星期,计算出前面空缺的上个月的日期个数,填充到当月日期列表中
            int weekDayOfFirstDay = getWeekDayOnCertainDate(year, month, 1);
            int preMonthDays = weekDayOfFirstDay - 1;
    
            for (int i = preMonthDays; i > 0; i--) {
                MyCalendarBean preMonthBean = generateCalendarBean(year, month, 1 - i);
                preMonthBean.setCurrentMonth(false);
                list.add(preMonthBean);
            }
    
            for (int i = 0; i < daysOfMonth; i++) {
                MyCalendarBean monthBean = generateCalendarBean(year, month, i + 1);
                monthBean.setCurrentMonth(true);
                list.add(monthBean);
            }
            return list;
        }
    
        /**
         * 构建具体一天的对象
         *
         * @param year
         * @param month
         * @param day
         * @return
         */
        public MyCalendarBean generateCalendarBean(int year, int month, int day) {
            Calendar calendar = Calendar.getInstance();
            calendar.set(year, month - 1, day);
            year = calendar.get(Calendar.YEAR);
            month = calendar.get(Calendar.MONTH) + 1;
            day = calendar.get(Calendar.DATE);
    
            return new MyCalendarBean(year, month, day);
        }
    
        /**
         * 获取具体一天对应的星期
         *
         * @param year
         * @param month
         * @param day
         * @return 1-7(周日-周六)
         */
        private int getWeekDayOnCertainDate(int year, int month, int day) {
            Calendar calendar = Calendar.getInstance();
            calendar.set(year, month - 1, day);
            return calendar.get(Calendar.DAY_OF_WEEK);
        }
    
        /**
         * 获取当前月份的周数
         * @param year
         * @param month
         * @return
         */
        public int getWeekCountsOfMonth(int year, int month) {
            int lastDayOfMonth = getDaysOfCertainMonth(year, month);
            Calendar calendar = Calendar.getInstance();
            calendar.set(year, month - 1, lastDayOfMonth);
            return calendar.get(Calendar.WEEK_OF_MONTH);
        }
    
        /**
         * 格式化标题展示
         *
         * @param year
         * @param month
         * @return
         */
        public static String formatYearAndMonth(int year, int month) {
            Calendar calendar = Calendar.getInstance();
            calendar.set(year, month - 1, 1);
            year = calendar.get(Calendar.YEAR);
            month = calendar.get(Calendar.MONTH) + 1;
            return year + "年" + month + "月";
        }
    
        /**
         * 获取系统当前年月日
         *
         * @return
         */
        public static int[] getNowDayFromSystem() {
            Calendar cal = Calendar.getInstance();
            cal.setTime(new Date());
            return new int[]{cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DATE)};
        }
    
        /**
         * 判断是否为系统当天
         *
         * @param bean
         * @return
         */
        public static boolean isToday(MyCalendarBean bean) {
            int[] nowDay = getNowDayFromSystem();
            return bean.getYear() == nowDay[0] && bean.getMonth() == nowDay[1] && bean.getDay() == nowDay[2];
        }
    
        public static int dp2px(Context context, float dp) {
            float scale = context.getResources().getDisplayMetrics().density;
            return (int) (dp * scale + 0.5f);
        }
    }
    

    2、自定义View(MonthCalendarView2)

    (1)onMeasure() 方法
    • 一个月的日期,7列排布,最少4行,最多6行
    • 将日历高度设为其宽度的 6/7
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
            setMeasuredDimension(measureWidth, measureWidth * 6 / column);
        }
    
    (2)onSizeChanged() 方法
    • 确定控件的宽高,以及 item 的宽度
        @Override
        protected void onSizeChanged(int w, int h, int oldW, int oldH) {
            parentWidth = w;
            parentHeight = h;
            itemWidth = w / column;
        }
    
    (3)onDraw() 方法
    • 获取当前月份的周数,根据周数确定日期 item 的高度
    int weekCounts = calendarUtils.getWeekCountsOfMonth(currentYear, currentMonth);
    itemHeight = parentHeight / weekCounts;
    
    • 根据上面确定的 item 的高度,计算出每个 item 的 left, top, right, bottom,设置到 Region 里面
    • 将日期绘制到 Region 的中间(这里获取到的当前月的日期列表中包含了个别上个月的数据,所以绘制的时候要做一个是否是当前月的判断)
        currentMonthDays = calendarUtils.getDaysListOfMonth(currentYear, currentMonth);
        for (int i = 0; i < currentMonthDays.size(); i++) {
            int columnCount = i % column;
            int rowCount = i / column;
    
            Region region = new Region();
            region.set(columnCount * itemWidth, rowCount * itemHeight, (columnCount * itemWidth) + itemWidth, (rowCount * itemHeight) + itemHeight);
    
            MyCalendarBean bean = currentMonthDays.get(i);
            if (bean.isCurrentMonth()) {
                canvas.drawText(String.valueOf(bean.getDay()), region.getBounds().centerX(), region.getBounds().centerY(), mPaint);
            }
        }
    
    (4)暴露一个初始化的方法
        public void setMonth(int year, int month) {
            currentYear = year;
            currentMonth = month;
            invalidate();
        }
    
    (5)获取当前显示的年月
        public String getCurrentYearAndMonth() {
            return MyCalendarUtils.formatYearAndMonth(currentYear, currentMonth);
        }
    

    3、在 SimpleCalendarView 中添加

    • 跟上一篇文章的控件一样的用法,直接贴出代码:
            //月历视图
            LayoutParams monthParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
    //        monthCalendarView = new MonthCalendarView(context);
    //        initCalendarDate();
    //        monthCalendarView.setOnDatePickUpListener(this);
    //        addView(monthCalendarView, monthParams);
    
            monthCalendarView2 = new MonthCalendarView2(context);
            initCalendarDate2();
            addView(monthCalendarView2, monthParams);
    
        private void initCalendarDate2() {
            int[] nowDay = MyCalendarUtils.getNowDayFromSystem();
            monthCalendarView2.setMonth(nowDay[0], nowDay[1]);
            updateTitle2();
        }
    
        private void updateTitle2() {
            if (null != title && null != monthCalendarView2) {
                title.setText(monthCalendarView2.getCurrentYearAndMonth());
            }
        }
    

    效果跟上一篇文章是一样的:


    MonthCalendarView2

    4、补充前翻页、后翻页方法

    方法跟上一篇也是一样,贴出代码:

        /**
         * 展示上一个月
         */
        public void moveToPreMonth() {
            currentMonth -= 1;
            invalidate();
        }
    
        /**
         * 展示下一个月
         */
        public void moveToNextMonth() {
            currentMonth += 1;
            invalidate();
        }
    

    效果看下面动态图:


    MonthCalendarView2

    5、补充高亮显示系统当天日期

    • 在 onDraw() 方法中绘制日期的同时,判断为系统当天时间,绘制一个圆形背景
        @Override
        protected void onDraw(Canvas canvas) {
            canvas.drawColor(getResources().getColor(R.color.white));
            draw(canvas, currentYear, currentMonth);
        }
    
        private void draw(Canvas canvas, int year, int month) {
            int weekCounts = calendarUtils.getWeekCountsOfMonth(year, month);
            itemHeight = parentHeight / weekCounts;
    
            currentMonthDays = calendarUtils.getDaysListOfMonth(year, month);
    
            for (int i = 0; i < currentMonthDays.size(); i++) {
                int columnCount = i % column;
                int rowCount = i / column;
    
                MyCalendarBean bean = currentMonthDays.get(i);
    
                Region region = new Region();
                region.set(columnCount * itemWidth, rowCount * itemHeight, (columnCount * itemWidth) + itemWidth, (rowCount * itemHeight) + itemHeight);
    
                if (bean.isCurrentMonth()) {
                    drawTodayBG(canvas, region.getBounds(), bean);
                    drawText(canvas, region.getBounds(), bean);
                }
            }
        }
    
        private void drawText(Canvas canvas, Rect rect, MyCalendarBean bean) {
            canvas.drawText(String.valueOf(bean.getDay()), rect.centerX(), rect.centerY() + (textSize / 4), mPaint);
        }
    
        private void drawTodayBG(Canvas canvas, Rect rect, MyCalendarBean bean) {
            if (MyCalendarUtils.isToday(bean)) {
                canvas.drawCircle(rect.centerX(), rect.centerY(), circleRadius, mTodayBGPaint);
            }
        }
    

    效果出来:


    高亮显示今天

    6、补充点击日期效果

    (1)点击回调
    • onDraw() 方法
    • 将当前月份的 Region 都添加到临时列表中
        private void draw(Canvas canvas, int year, int month) {
            //...
            //...
            for (int i = 0; i < currentMonthDays.size(); i++) {
                //...
                //...
                Region region = new Region();
                region.set(columnCount * itemWidth, rowCount * itemHeight, (columnCount * itemWidth) + itemWidth, (rowCount * itemHeight) + itemHeight);
                tempRegions.add(region);
                //...
                //...
            }
        }
    
    • 重写 onTouchEvent() 方法
    • 根据点击的位置,找到对应的日期,回调
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    for (int i = 0; i < tempRegions.size(); i++) {
                        Region region = tempRegions.get(i);
                        if (region.contains((int) event.getX(), (int) event.getY())) {
                            MyCalendarBean bean = currentMonthDays.get(i);
                            if (bean.isCurrentMonth()) {
                                if (null != onDatePickUpListener) {
                                    onDatePickUpListener.onDatePickUp2(bean);
                                }
                            }
                        }
                    }
                    break;
            }
            return true;
        }
    
    • 回调接口
        private OnDatePickUpListener onDatePickUpListener;
        public void setOnDatePickUpListener(OnDatePickUpListener onDatePickUpListener) {
            this.onDatePickUpListener = onDatePickUpListener;
        }
    
        public interface OnDatePickUpListener {
            void onDatePickUp2(MyCalendarBean bean);
        }
    

    效果出来,简单弹窗显示:


    MonthCalendarView2
    (2)高亮显示点击选中的日期
    • 在 onTouchEvent() 方法中记录选中的 Region
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    for (int i = 0; i < tempRegions.size(); i++) {
                        Region region = tempRegions.get(i);
                        if (region.contains((int) event.getX(), (int) event.getY())) {
                            MyCalendarBean bean = currentMonthDays.get(i);
                            if (bean.isCurrentMonth()) {
                                //看这里
                                pickRegion.set(region);
                                invalidate();
    
                                if (null != onDatePickUpListener) {
                                    onDatePickUpListener.onDatePickUp2(bean);
                                }
                            }
                        }
                    }
                    break;
            }
            return true;
        }
    
    • 在 onDraw() 方法中绘制选中的 Region 对应的圆形背景
        private void draw(Canvas canvas, int year, int month) {
            tempRegions.clear();
            int weekCounts = calendarUtils.getWeekCountsOfMonth(year, month);
            itemHeight = parentHeight / weekCounts;
    
            currentMonthDays = calendarUtils.getDaysListOfMonth(year, month);
    
            for (int i = 0; i < currentMonthDays.size(); i++) {
                int columnCount = i % column;
                int rowCount = i / column;
    
                MyCalendarBean bean = currentMonthDays.get(i);
    
                Region region = new Region();
                region.set(columnCount * itemWidth, rowCount * itemHeight, (columnCount * itemWidth) + itemWidth, (rowCount * itemHeight) + itemHeight);
                tempRegions.add(region);
    
                if (bean.isCurrentMonth()) {
                    drawTodayBG(canvas, region.getBounds(), bean);
                    drawText(canvas, region.getBounds(), bean);
                }
            }
            drawPickUpCircle(canvas);
            pickRegion.setEmpty();
        }
    
        private void drawPickUpCircle(Canvas canvas) {
            if(!pickRegion.isEmpty()){
                canvas.drawCircle(pickRegion.getBounds().centerX(), pickRegion.getBounds().centerY(), circleRadius, mPickUpCirclePaint);
            }
        }
    

    效果如下:


    MonthCalendarView2

    7、优化一下点击处理

    • 将点击位置的判断放到 MotionEvent.ACTION_UP 处,并做滑动取消点击的判断处理
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    cancelClick = false;
                    lastX = event.getX();
                    lastY = event.getY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    if ((Math.abs(lastX - event.getX()) >= moveLimit) || (Math.abs(lastY - event.getY()) >= moveLimit)) {
                        cancelClick = true;
                    }
                    break;
                case MotionEvent.ACTION_UP:
                    if (!cancelClick) {
                        for (int i = 0; i < tempRegions.size(); i++) {
                            Region region = tempRegions.get(i);
                            if (region.contains((int) event.getX(), (int) event.getY())) {
                                MyCalendarBean bean = currentMonthDays.get(i);
                                if (bean.isCurrentMonth()) {
                                    pickRegion.set(region);
                                    invalidate();
                                    if (null != onDatePickUpListener) {
                                        onDatePickUpListener.onDatePickUp2(bean);
                                    }
                                }
                            }
                        }
                    }
                    break;
            }
            return true;
        }
    

    三、MonthCalendarView2

    • 将自定义View (MonthCalendarView2)全部代码放出来
    package com.example.deesonwoo.mysimplecalendar.calendar;
    
    import android.content.Context;
    import android.graphics.Canvas;
    import android.graphics.Paint;
    import android.graphics.Rect;
    import android.graphics.Region;
    import android.view.MotionEvent;
    import android.view.View;
    
    import com.example.deesonwoo.mysimplecalendar.R;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * MonthCalendarView2
     */
    public class MonthCalendarView2 extends View {
    
        private MyCalendarUtils calendarUtils;
    
        protected Paint mPaint = new Paint();
        protected Paint mTodayBGPaint = new Paint();
        protected Paint mPickUpCirclePaint = new Paint();
    
        private int column = 7;
    
        private int currentYear, currentMonth;
        private int parentWidth, parentHeight;
        private int itemWidth, itemHeight;
        private int circleRadius;
        private int textSize;
        private List<MyCalendarBean> currentMonthDays;
        private List<Region> tempRegions = new ArrayList<>();
        private Region pickRegion = new Region();
        private float lastX, lastY;
        private float moveLimit = 25F;
        private boolean cancelClick = false;
        private OnDatePickUpListener onDatePickUpListener;
    
        public MonthCalendarView2(Context context) {
            super(context);
            calendarUtils = new MyCalendarUtils(context);
            textSize = MyCalendarUtils.dp2px(context, 15);
            mPaint.setTextAlign(Paint.Align.CENTER);
            mPaint.setTextSize(textSize);
            mPaint.setColor(getResources().getColor(R.color.text_black));
            mTodayBGPaint.setColor(getResources().getColor(R.color.theme_color));
            mPickUpCirclePaint.setColor(getResources().getColor(R.color.theme_color));
            mPickUpCirclePaint.setStyle(Paint.Style.STROKE);
            mPickUpCirclePaint.setStrokeWidth(MyCalendarUtils.dp2px(context, 2));
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
            setMeasuredDimension(measureWidth, measureWidth * 6 / column);
        }
    
        @Override
        protected void onSizeChanged(int w, int h, int oldW, int oldH) {
            parentWidth = w;
            parentHeight = h;
            itemWidth = w / column;
            circleRadius = itemWidth * 3 / 8;
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            canvas.drawColor(getResources().getColor(R.color.white));
    
            draw(canvas, currentYear, currentMonth);
        }
    
        private void draw(Canvas canvas, int year, int month) {
            tempRegions.clear();
            int weekCounts = calendarUtils.getWeekCountsOfMonth(year, month);
            itemHeight = parentHeight / weekCounts;
    
            currentMonthDays = calendarUtils.getDaysListOfMonth(year, month);
    
            for (int i = 0; i < currentMonthDays.size(); i++) {
                int columnCount = i % column;
                int rowCount = i / column;
    
                MyCalendarBean bean = currentMonthDays.get(i);
    
                Region region = new Region();
                region.set(columnCount * itemWidth, rowCount * itemHeight, (columnCount * itemWidth) + itemWidth, (rowCount * itemHeight) + itemHeight);
                tempRegions.add(region);
    
                if (bean.isCurrentMonth()) {
                    drawTodayBG(canvas, region.getBounds(), bean);
                    drawText(canvas, region.getBounds(), bean);
                }
            }
            drawPickUpCircle(canvas);
            pickRegion.setEmpty();
        }
    
        private void drawText(Canvas canvas, Rect rect, MyCalendarBean bean) {
            canvas.drawText(String.valueOf(bean.getDay()), rect.centerX(), rect.centerY() + (textSize / 4), mPaint);
        }
    
        private void drawTodayBG(Canvas canvas, Rect rect, MyCalendarBean bean) {
            if (MyCalendarUtils.isToday(bean)) {
                canvas.drawCircle(rect.centerX(), rect.centerY(), circleRadius, mTodayBGPaint);
            }
        }
    
        private void drawPickUpCircle(Canvas canvas) {
            if(!pickRegion.isEmpty()){
                canvas.drawCircle(pickRegion.getBounds().centerX(), pickRegion.getBounds().centerY(), circleRadius, mPickUpCirclePaint);
            }
        }
    
        public void setMonth(int year, int month) {
            currentYear = year;
            currentMonth = month;
            invalidate();
        }
    
        public String getCurrentYearAndMonth() {
            return MyCalendarUtils.formatYearAndMonth(currentYear, currentMonth);
        }
    
        /**
         * 展示上一个月
         */
        public void moveToPreMonth() {
            currentMonth -= 1;
            invalidate();
        }
    
        /**
         * 展示下一个月
         */
        public void moveToNextMonth() {
            currentMonth += 1;
            invalidate();
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    cancelClick = false;
                    lastX = event.getX();
                    lastY = event.getY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    if ((Math.abs(lastX - event.getX()) >= moveLimit) || (Math.abs(lastY - event.getY()) >= moveLimit)) {
                        cancelClick = true;
                    }
                    break;
                case MotionEvent.ACTION_UP:
                    if (!cancelClick) {
                        for (int i = 0; i < tempRegions.size(); i++) {
                            Region region = tempRegions.get(i);
                            if (region.contains((int) event.getX(), (int) event.getY())) {
                                MyCalendarBean bean = currentMonthDays.get(i);
                                if (bean.isCurrentMonth()) {
                                    pickRegion.set(region);
                                    invalidate();
                                    if (null != onDatePickUpListener) {
                                        onDatePickUpListener.onDatePickUp2(bean);
                                    }
                                }
                            }
                        }
                    }
                    break;
            }
            return true;
        }
    
        public void setOnDatePickUpListener(OnDatePickUpListener onDatePickUpListener) {
            this.onDatePickUpListener = onDatePickUpListener;
        }
    
        public interface OnDatePickUpListener {
            void onDatePickUp2(MyCalendarBean bean);
        }
    }
    

    最后实现的效果跟上一篇文章的实现是一模一样的。

    四、后续

    相关文章

      网友评论

        本文标题:一步一步构建自己的简单日历控件 MySimpleCalendar

        本文链接:https://www.haomeiwen.com/subject/jbevaxtx.html