自定义view之实现日历界面

现在网上有很多自定义view实现日历的demo,今天讲一讲如何自己实现这个自定义view。

看一下最终效果图:

在这个自定义view中,我使用了各种奇技淫巧的方法来实现这个日历,真是费尽心思。废话少说,开始进坑。

界面分析

头部是一个textview,显示年份和月份,然后下边一行是星期几,这两行可以固定住,不随月份切换而进出屏幕。

再下边就是我们自定义view 的主角,每个月的天数。目前规定是星期日为每星期第一天。上个月的天数填充满第一行,下个月的前几天填充完最后一行,颜色设置为灰色,本月日期中的周一至周五设置为红色,周六周日设置为青色,特殊日期设置为绿色,并且在右上角填充特殊标识符,用四分之三的圆弧包裹(上个月和下个月的日期没有)。

此处还有个小细节,每月的总行数会不断改变,但是view的总高度并未改变,所以视觉效果会不一样。

构造方法

 public MyCalendar(Context context) {
        super(context);
    }

    public MyCalendar(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);

    }

主要是实现上面两个构造方法,第一个是用来在java代码中使用的,第二个是用来在xml布局文件中使用的。

暴露的接口

目前接口共有下面几个,setDate(CustomDate customDate),setWeekendHighLight(boolean b),setSpecialDay(int[] ints)

其中第一个是必须要设置的,否则是不会显示任何东西,第二个设置的是否周末高亮,第三个设置的是特殊显示的日期,第四个是设置是否可以点击前一个月或者后一个月的日期,默认为不设置,后期可以根据自己需求增加其他接口。

    /**
     * 暴露接口,设置日期
     *
     * @param customDate
     */
    public void setDate(CustomDate customDate) {
        Log.d(TAG, customDate.toString());
        this.date = customDate;
        firstDayOfWeek = date.getFirstDayOfWeek();
        Log.d(TAG, (date.getMonth() + 1) + "月1号是星期" + firstDayOfWeek);
        lastDayOfWeek = date.getLastDayOfWeek();
        lineCount = calculateLineNum() + 1;
        lastMonthTotalDays = date.getLastMonthDays();
    }

    /**
     * 暴露接口,设置是否周末高亮
     *
     * @param b
     */
    public void setWeekendHighLight(boolean b) {
        this.weekendHighlight = b;
    }

    public void setSpecialDay(int[] ints) {
        this.specialDays = ints;
    }

    /**
     * 暴露接口,设置是否可以点击前一个月和后一个月的日期
     *
     * @param b
     */
    public void setCanClickNextOrPreMonth(boolean b) {
        this.canClickNextOrPreMonth = b;
    }

在这里说明一下计算显示行数的方法,首先要注意我们获取的星期数与实际的星期几会有一个增加一天的问题,也就是当前是星期4,那么你获取的int将会是5.

 /**
     * 获得应该设置为多少行
     *
     * @return
     */
    private int calculateLineNum() {
        monthDaySum = date.getTotalDayOfMonth();
        return (firstDayOfWeek - 1 + monthDaySum) / 7;
    }

我们将第一天是星期几减去一后加上这个月总共多少天,就可以获得最后一天是在什么位置,然后除以七取商的整数部分,然后在进一法即可获得应该显示多少行。

onSizechanged方法

onSizechanged方法中已经可以获得显示的尺寸了,此时我们需要做一些工作:

 protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        this.viewWidth = w;
        this.viewHeight = h;
        Log.d(TAG, "onSizeChanged" + w + h);
        cutGrid();
        init();
        setCellDay();
    }

首先是将宽和高引入进来,方便后边使用。

cutGrid()方法是将区域分割为行X列的格式。

init()方法初始化了一些画笔。

setCellDay()方法将每月的天对应过到坐标上。

首先看一下cutGrid()方法:

 /**
     * 切分为每天
     */
    private void cutGrid() {
        cellWidth = (float) viewWidth / ROW_COUNT;
        cellHeight = (float) viewHeight / lineCount;
        this.radius = Math.min(cellWidth / 2, cellHeight / 2);
        for (int i = 0; i < lineCount; i++) {
            for (int j = 0; j < ROW_COUNT; j++) {
                points.add(new PointF(cellWidth * j + cellWidth / 2, cellHeight * i + cellHeight / 2));
            }
        }
    }

cellWidth是每天的宽度,其中ROW_COUNT是一个常量7,表示每周7天;cellHeight是每行的高度,linecount是一个变量,需要我们根据日期计算,后边会说到;radius是我们绘制区域的半径,这个值是我们取宽度和高度中较小的值的一半。然后我们将每个方格中心坐标点利用双重循环放入一个List<Point> points中。

整个view被分割为如上的形状。

下面来看一下init()方法:

private void init() {
        circlePaint = new Paint();
        circlePaint.setStyle(Paint.Style.STROKE);
        circlePaint.setAntiAlias(true);
        circlePaint.setColor(Color.BLUE);
        textPaint = new Paint();
        textPaint.setAntiAlias(true);
        textPaint.setColor(Color.BLACK);
        textPaint.setTextSize(radius / 2);
        selectPaint = new Paint();
        selectPaint.setColor(Color.YELLOW);
        selectPaint.setAlpha(10);
        selectPaint.setAntiAlias(true);
        selectPaint.setStyle(Paint.Style.FILL);
        selectTextPaint = new Paint();
        selectTextPaint.setColor(Color.WHITE);
        selectTextPaint.setAntiAlias(true);
        selectTextPaint.setTextSize(radius / 2);
        selectTextPaint.setStyle(Paint.Style.FILL);
    }

基本都是画笔工具。

然后是setAllDays()方法:

 /**
     * 设置总共显示多少天,每天的状态
     */
    private void setCellDay() {
        cellDays = new CellDay[lineCount * ROW_COUNT];
        for (int i = 0, length = cellDays.length; i < length; i++) {
            cellDays[i] = new CellDay();
            cellDays[i].setPointX(points.get(i).x);
            cellDays[i].setPointY(points.get(i).y);
            if (firstDayOfWeek > 1 && i < firstDayOfWeek - 1) {
                cellDays[i].setDayState(DayState.LASTMONTH);
                cellDays[i].setDate(String.valueOf(lastMonthTotalDays - firstDayOfWeek + i + 2));
                cellDays[i].setCustomDate(new CustomDate(
                        date.getYear(), date.getMonth() - 1, lastMonthTotalDays - firstDayOfWeek + i + 2));
            }
            if (i >= firstDayOfWeek - 1 && i < monthDaySum + firstDayOfWeek - 1) {

                cellDays[i].setDayState(CURRENTMONTH);
                cellDays[i].setDate(String.valueOf(i + 2 - firstDayOfWeek));
                cellDays[i].setCustomDate(new CustomDate(
                        date.getYear(), date.getMonth(), i - firstDayOfWeek + 2));
                //设置周末高亮
                if (weekendHighlight) {
                    if (i % 7 == 0 || i % 7 == 6) {
                        cellDays[i].setDayState(WEEKEND);
                    }
                }
            }
            if (i >= monthDaySum + firstDayOfWeek - 1) {
                cellDays[i].setDayState(NEXTMONTH);
                cellDays[i].setDate(String.valueOf(i - monthDaySum - firstDayOfWeek + 2));
                cellDays[i].setCustomDate(new CustomDate(
                        date.getYear(), date.getMonth() + 1, i - monthDaySum - firstDayOfWeek + 2));
            }
            for (int j = 0, s = specialDays.length; j < s; j++) {
                if (specialDays[j] + firstDayOfWeek - 2 == i) {
                    cellDays[i].setDayState(SPECIALDAY);
                }
            }
        }
    }

在这里我们用到了一个自定的类-CellDay。
CellDay有以下几个字段

        private String date;
        private DayState dayState;
        private CustomDate customDate;
        private float pointX;
        private float pointY;
        private boolean isSelected;
  1. String date表示当前的日期。
  2. dayState是一个美剧类型,定义了天的状态值。
    LASTMONTH:上个月的日期
    CURRENTMONTH:本月的日期
    NEXTMONTH: 下个月的日期
    CURRENTDAY: 今天的日期
    WEEKEND:周末的日期
    SPECIALDAY:用户自定义的可以设置状态的日期

其中可以设置多种状态,用法和SPECIALDAY基本一样。

  1. cusomedate是我们自己定义的一个工具类,包含项目中需要用到的一系列方法。
  2. pointX是横坐标。
  3. pointY是纵坐标。
  4. isSelceted表示有没有被选中。

CustomDate工具

public class CustomDate {
    private Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT+8"));
    private int year;
    private int month;
    private int day;
    private int dayOfWeek;

    public CustomDate() {
    }

    /**
     * 获取当前的日期
     * @return
     */
    public CustomDate getCurrentDate() {
        this.year = calendar.get(Calendar.YEAR);
        this.month = calendar.get(Calendar.MONTH);
        this.day = calendar.get(Calendar.DAY_OF_MONTH);
        this.dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
        return new CustomDate(year, month, day);
    }

    public CustomDate(int year, int month, int day) {
        this.year = year;
        this.month = month;
        this.day = day;
        calendar.set(year, month, day);
        dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
    }

    /**
     * 获取上个月的天数
     * @return
     */
    public int getLastMonthDays() {
        return this.getDaysOfMonth(this.year, this.month - 1);
    }

    /**
     * 获取第一天是星期几
     *
     * @return
     */
    public int getFirstDayOfWeek() {
        calendar.set(this.year, this.month, 1);
        return calendar.get(Calendar.DAY_OF_WEEK);
    }

    /**
     * 获取最后一天是星期几
     *
     * @return
     */
    public int getLastDayOfWeek() {
        calendar.set(this.year, this.month, getTotalDayOfMonth());
        return calendar.get(Calendar.DAY_OF_WEEK);
    }

    /**
     * 获取这个月总共的天数
     * @return
     */
    public int getTotalDayOfMonth() {
        return this.getDaysOfMonth(year, month);
    }

    public int getTotalWeekOfMonth() {
        return calendar.getMaximum(Calendar.WEEK_OF_MONTH);
    }

    public int getYear() {
        return year;
    }

    public void setYear(int year) {
        this.year = year;
    }

    public int getMonth() {
        return month;
    }

    public void setMonth(int month) {
        this.month = month;
    }

    public int getDay() {
        return day;
    }

    public void setDay(int day) {
        this.day = day;
    }

    public int getDayOfWeek() {
        return dayOfWeek;
    }

    public void setDayOfWeek(int dayOfWeek) {
        this.dayOfWeek = dayOfWeek;
    }

    @Override
    public String toString() {
        return "CustomDate{" +
                "year=" + year +
                ", month=" + (getMonth() + 1) +
                ", day=" + day +
                ", dayOfWeek=" + dayOfWeek +
                '}';
    }

    /**
     * 获取年中每月的天数
     * @param year
     * @param month
     * @return
     */
    private int getDaysOfMonth(int year, int month) {
        if (month > 11) {
            month = 0;
            year += 1;
        } else if (month < 0) {
            month = 11;
            year -= 1;
        }

        int[] arr = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
        int daysOfMonth = 0;
        if (year % 4 == 0 && year % 100 != 0 || year % 400 == 0) {
            arr[1] = 29;
        }
        daysOfMonth = arr[month];
        return daysOfMonth;
    }
}

注释中对每个方法的说明已经非常清晰了。

  1. int getLastMonthDays()
    获取上个月的天数是用来计算上个月最后一天是星期几,然后以此推导出上个月在本月中显示的天数和对应的星期。
  2. getFirstDayOfWeek()
    获取本月第一天是星期几,然后排序本月的天数与对应的星期。
  3. int getTotalDayOfMonth()
    获取本月总共多少天。配合第一天是星期几用来计算总共分为几行,也就是确定linenumber。

下一节将介绍如何绘制和分发touch事件。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容