Android自定义日历控件

概述

日历有阳(公)历、阴(农)历之分,咱们从下面两个方面区分一下(当然,他们的区别还是挺多的,有兴趣的可以研究研究)

  • 月份的定义:阳(公)历 一年12 个月,每个月的天数各不同;阴(农)历,每个月固定28天
  • 每周的第一天:阳(公)历星期日是第一天;阴(农)历,星期一是第一天

好了,咱们言归正传。日历翻译成英文就是Calendar,然而咱们平使用的是阳(公)历,翻译成英文就是GregorianCalendar。而咱们要自定义的日历控件主要用到的就是Calendar这个类

要点

一、Calendar的使用

1、Calendar的初始化

a.单例初始化
Calendar mCalendar = Calendar.getInstance();
b.使用GregorianCalendar初始化
Calendar mCalendar = new GregorianCalendar();

这里有人就回问,那这两种初始化有什么不同。其实,这两种初始化方式并没有什么不同,殊途同归。咱们可以通过源码去了解一下就清楚了

首先咱们先看一下Calendar这个类的源码,并且这个类还是一个抽象类。这里我只挑出了相关的代码

public abstract class Calendar implements Serializable, Cloneable, Comparable<Calendar> {

        /**
         * Gets a calendar using the default time zone and locale. The
         * <code>Calendar</code> returned is based on the current time
         * in the default time zone with the default locale.
         *
         * @return a Calendar.
         */
        public static Calendar getInstance(){
            Calendar cal = createCalendar(TimeZone.getDefaultRef(),
            Locale.getDefault(Locale.Category.FORMAT));
            cal.sharedZone = true;
            return cal;
        }

        public static Calendar getInstance(TimeZone zone){
            return createCalendar(zone, Locale.getDefault(Locale.Category.FORMAT));
        }

        public static Calendar getInstance(Locale aLocale){
            Calendar cal = createCalendar(TimeZone.getDefaultRef(), aLocale);
            cal.sharedZone = true;
            return cal;
        }

        public static Calendar getInstance(TimeZone zone,Locale aLocale){
            return createCalendar(zone, aLocale);
        }

       /**
        *   哈哈,看到这里是不是就明白了。
        *   他们最终都会去new一个GregorianCalendar实实例给你返回
        */
        private static Calendar createCalendar(TimeZone zone,Locale aLocale){
            return new GregorianCalendar(zone, aLocale);
        }
}

来来来,我们来看一下GregorianCalendar这个类到底是何方神圣

public class GregorianCalendar extends Calendar {
    /**
     * Constructs a default <code>GregorianCalendar</code> using the current time
     * in the default time zone with the default locale.
     */
    public GregorianCalendar() {
        this(TimeZone.getDefaultRef(), Locale.getDefault(Locale.Category.FORMAT));
        setZoneShared(true);
    }
}

看到这里是不是就明白了,其实这两种初始化是没有区别的。GregorianCalendar这个类也是继承了Calendar的。

2、Calendar方法详解

获取值

方法结果
cal.get(Calendar.YEAR)
cal.get(Calendar.MONTH) + 1月(必须要+1)
cal.get(Calendar.DATE)
cal.get(Calendar.HOUR_OF_DAY)
cal.get(Calendar.MINUTE)
cal.get(Calendar.SECOND)
cal.get(Calendar.DAY_OF_WEEK)星期(Locale.ENGLISH情况下,周日是1,剩下自己推算)

设置值

方法结果
cal.set(2013, 5, 4, 13, 44, 51)年月日时分秒(月份0代表1月)
cal.set(Calendar.YEAR, 2014)
cal.set(Calendar.MONTH, 7)月(月份0代表1月)
cal.set(Calendar.DATE, 11)
cal.set(Calendar.HOUR_OF_DAY, 15)
cal.set(Calendar.MINUTE, 33)
cal.set(Calendar.SECOND, 32)

运算值

方法结果
cal.add(Calendar.YEAR, 1)
cal.add(Calendar.MONTH, 1)
cal.add(Calendar.DATE, 1)
cal.add(Calendar.HOUR_OF_DAY, -1)
cal.add(Calendar.MINUTE, 1)
cal.add(Calendar.SECOND, 1)
cal.add(Calendar.DATE, 7)

二、设计控件

1、最终效果图

首先,先来看一下最终要实现什么样的效果

这里写图片描述

2、实现思路

接下来先对要实现的效果进行分析,整个布局可分为一下三大部分:

  • 顶部的ActionBar布局
  • 当前月份的显示,以及上月和下月的左右箭头
  • 底部的星期名称布局和当前月份的信息

这里写图片描述

结合以前所学的MVC模式,即模型(Model)-视图(View)-控制器(Controller)对整个布局进行分析

Model:CalendarDate

咱们主要关注最底部的当前月份信息,咱们可把最底部的View拆分成一个一个小方块,每一个小方块即代表的是一天。而小方块就是我们要建立的数据模型。

我们把每一个小方块都看成是一个对象,可以根据上面的布局分析得出每个对象的属性:

  • 每个对象的位置:第几行、第几列
  • 每个对象的宽和高
  • 每个对象所代表的年(year)、月(month)、日(day)
  • 每个对象的字体、大小、颜色
  • 每个对象的背景颜色

这样,我们的CalendarDate就出来了

private class CalendarDate {
        private int year;
        private int month;
        private int day;
        /**
         * 单个日期格子的宽
         */
        private int width;
        /**
         * 单个日期格子的高
         */
        private int height;
        /**
         * 日期的文本
         */
        private String text;
        /**
         * 文本字体的颜色
         */
        private int textColor;
        /**
         * 日期背景的类型 0代表无任何背景,1代表即是当前日期
         */
        private int backgroundStyle;
        /**
         * 字体的大小
         */
        private float textSize;
        /**
         * 背景的大小
         */
        private int backgroundSize;

        /**
         * 字体在第几行
         */
        private int location_x;
        /**
         * 字体在第几列
         */
        private int location_y;

        /**
         * 创建日期对象
         *
         * @param width  每个日期格子的宽度
         * @param height 每个日期格子的高度
         */
        private CalendarDate(int width, int height) {
            this.width = width;
            this.height = height;
        }

       ......

    }

而每个模型只有属性是不行的,还得有行为。这里我们让它可以画出自己,调用自己的drawDays()方法即可

private class CalendarDate {

        ......

        /**
         * 画自己
         *
         * @param canvas 要画的画布
         * @param paint  画笔
         */
        private void drawDays(Canvas canvas, Paint paint) {
            //取窄的边框做为边框大小
            backgroundSize = width > height ? height : width;
            //画背景
            drawBackground(canvas, paint);
            //画数字
            drawText(canvas, paint);
        }

        /**
         * 画数字
         */
        private void drawText(Canvas canvas, Paint paint) {
            //根据单个框的大小设置字体的大小
            textSize = backgroundSize / 3;
            paint.setTextSize(textSize);
            paint.setColor(textColor);
            paint.setStyle(Paint.Style.FILL);
            //计算文字的宽度
            Rect rect = new Rect();
            paint.getTextBounds(text, 0, text.length(), rect);
            int w = rect.width();
            //计算画文字的位置
            float x = location_x * width + (width - w) / 2;
            float y = location_y * height + (height + textSize / 2) / 2;
            canvas.drawText(text, x, y, paint);
        }

        /**
         * 画背景
         */
        private void drawBackground(Canvas canvas, Paint paint) {
            //画背景 根据背景状态设置画笔类型
            if (backgroundStyle == 0) {
                return;
            }
            paint.setColor(0xFF095CBF);
            paint.setStyle(Paint.Style.FILL);
            canvas.drawRect(location_x * width, location_y * height, location_x * width + width, location_y * height + height, paint);
        }
    }

Controller:CalendarManager

这里的的Controller就是要将我们的Model和View连接起来,并且进行相应的管理。可以简化外部使用,即CalendarManager

    /**
     * 管理日期类
     */
    private class CalendarManager {
        /**
         * 记录当前的时间
         */
        private String currentTime;

        /**
         * 当前的日期
         */
        private int current = -1;
        /**
         * 储存当前的日期
         */
        private int tempCurrent = -1;
        /**
         *
         */
        private String[] weeks = {"S", "M", "T", "W", "T", "F", "S"};
        private String[] dayArray = {"01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15",
                "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31"};

        /**
         * 设置当前的时间
         *
         * @param currentTime 当前的时间
         */
        private void setCurrentTime(String currentTime) {
            this.currentTime = currentTime;
        }

        /**
         * 获取当前的时间
         */
        private String getCurrentTime() {
            return currentTime;
        }


        private void setTempCurrent(int tempCurrent) {
            this.tempCurrent = tempCurrent;
        }

        private int getTempCurrent() {
            return tempCurrent;
        }


        private void setCurrent(int current) {
            this.current = current;
        }

        /**
         * 根据日历对象创建日期集合
         *
         * @param calendar 日历
         * @param width    控件的宽度
         * @param height   控件的高度
         * @return 返回的天数的集合
         */
        private List<CalendarDate> createDayByCalendar(Calendar calendar, int width, int height) {

            List<CalendarDate> calendarDates = new ArrayList<>();

                ...

            return calendarDates;
        }
    }

View:CalendarView

最后,最底部的月份布局信息决定得采用继承View来达到最终的效果,所以说继承自View的CalendarView就是我们MVC中的视图层了

    /**
     * 自定义日期View
     */
    private class CalendarView extends View {

        List<CalendarDate> mCalendarDateList;
        /**
         * 画笔
         */
        private Paint paint;

        /**
         * 重绘,并更改当前状态,由于绘图是在calendar基础上进行绘制的,所以改变calendar就可以改变图片
         */
        public void upDateDraw() {
            if ((mCalendar.get(Calendar.MONTH) + "" + mCalendar.get(Calendar.YEAR)).equals(mCalendarManager.getCurrentTime())) {
                mCalendarManager.setCurrent(mCalendarManager.getTempCurrent());
            } else {
                mCalendarManager.setCurrent(-1);
            }
            invalidate();
        }

        public CalendarView(Context context) {
            super(context);
            //初始化控件
            initView();
        }


        public CalendarView(Context context, AttributeSet attrs) {
            super(context, attrs);
            //初始化控件
            initView();
        }

        public CalendarView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            //初始化控件
            initView();
        }

        /***
         * 初始化控件
         */
        private void initView() {
            paint = new Paint();
            paint.setAntiAlias(true);
            mCalendar = Calendar.getInstance();
            mCalendarManager.setCurrent(mCalendar.get((Calendar.DAY_OF_MONTH)));
            mCalendarManager.setTempCurrent(mCalendar.get(Calendar.DAY_OF_MONTH
            ));
            mCalendarManager.setCurrentTime(mCalendar.get(Calendar.MONTH) + "" + mCalendar.get(Calendar.YEAR));
        }


        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            //获取day集合并绘制
            mCalendarDateList = mCalendarManager.createDayByCalendar(mCalendar, getMeasuredWidth(), getMeasuredHeight());
            for (CalendarDate calendarDate : mCalendarDateList) {
                calendarDate.drawDays(canvas, paint);
            }

        }

        @Override
        public boolean onTouchEvent(MotionEvent event) {
            if (MotionEvent.ACTION_DOWN == event.getAction()) {
                //判断点击的是哪个日期
                float x = event.getX();
                float y = event.getY();
                //计算点击的是哪个日期
                int locationX = (int) (x * 7 / getMeasuredWidth());
                int locationY = (int) (y * 7 / getMeasuredHeight());
                if (locationY == 0) {
                    return super.onTouchEvent(event);
                }
                CalendarDate calendarDate = findDate(locationX, locationY);
                if (null != mOnSelectChangeListener && null != calendarDate) {
                    mOnSelectChangeListener.getDate(calendarDate.year, calendarDate.month, calendarDate.day);
                    hide();
                }
            }
            return super.onTouchEvent(event);
        }

        private CalendarDate findDate(int locationX, int locationY) {
            for (CalendarDate calendarDate : mCalendarDateList) {
                if (calendarDate.location_x == locationX && calendarDate.location_y == locationY)
                    return calendarDate;
            }
            return null;
        }
    }

到这里,整个自定义日历控件的MVC就已经全部说完了。这里我将这三个部分全部封装到一个LinearLayout里面了,方便导入和使用,但是内部还是以MVC为架构进行模块区分。由于文件代码过长,这里就不贴了,想要的小伙伴可以去这里下载源码下载

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值