说起日历控件这个名词,感觉这在我们日常生活中必不可少的,很多项目中都用到了这个空间,下面我就来给你讲一下怎样通过自定义View实现日历这个小控件吧!

我用的是CustomCalendar实现的日历控件,感觉用这个即简单又方便,话不多说,我就开始了,准备好你的代码之旅吧!

自定义当然不是为了装逼呐,请认真看需求,我们需要在日期下面显示任务完成情况,当日被切换之后需要标注为灰色圆圈背景,这些都是自带日历控件不可达到的,当然,我们也可以继承系统CalendarView然后试着修改;那为什么不选择开源的日历控件呢?如果我们不会自定义控件或者时间很紧,开源的当然是首选,谁叫我闲的慌,开源的控件也会有些问题,有的可能不够完善很多bug,比较完善的可能内容太多,如果要抽取有用的内容还是需要花一定时间,如果整个库工程都弄过来会造成大量的代码冗余。另外一点很重要,任务情况需要从服务器上获取,当切换日期之后,日历控件下方要显示那天的任务详情(可能需要请求数据),这么多问题如果去修改开源库工程工作量不一定比自定义小。综上,还是自定义更适合我,整个日历控件500多行代码就搞定(包括注释、接口、各种变量),后面如果项目需求有变动,改动起来也是so easy!  当然,日常开发中,自带控件能搞定的尽量就用系统自带的。下面我们一起看看这个控件是怎样实现的。

先看效果图:

1、进入正题:

如果要绘制出这个控件,我们首先要得到某个月的所有天数(从1号开始….)、1号是星期几(从什么位置开始展示),有了这两个数据,我们就能得到第一行从哪里开始绘制,能绘制多少天,最后一行能绘制多少天,其他中间的都是绘制7天;接下来需要绘制当前日期和被选中日期的背景,其实就是在绘制日期时先判断下日期是不是当前日期,如果是就给他先画一个背景,被选择的也是一样。我们先看看获取日期的算法:

/**设置月份*/
private void setMonth(String Month){//设置的月份(2017年01月)month = str2Date(Month);Calendar calendar = Calendar.getInstance();calendar.setTime(new Date());//获取今天是多少号currentDay = calendar.get(Calendar.DAY_OF_MONTH);Date cM = str2Date(getMonthStr(new Date()));//判断是否为当月if(cM.getTime() == month.getTime()){isCurrentMonth = true;selectDay = currentDay;//当月默认选中当前日}else{isCurrentMonth = false;selectDay = 0;}Log.d(TAG, "设置月份:"+month+"   今天"+currentDay+"号, 是否为当前月:"+isCurrentMonth);calendar.setTime(month);dayOfMonth = calendar.getActualMaximum(Calendar.DAY_OF_MONTH);//第一行1号显示在什么位置(星期几)firstIndex = calendar.get(Calendar.DAY_OF_WEEK)-1;lineNum = 1;//第一行能展示的天数firstLineNum = 7-firstIndex;lastLineNum = 0;int shengyu = dayOfMonth - firstLineNum;while (shengyu>7){lineNum ++;shengyu-=7;}if(shengyu>0){lineNum ++;lastLineNum = shengyu;}Log.i(TAG, getMonthStr(month)+"一共有"+dayOfMonth+"天,第一天的索引是:"+firstIndex+"   有"+lineNum+"行,第一行"+firstLineNum+"个,最后一行"+lastLineNum+"个");
}

  2、 自定义属性

<?xml version="1.0" encoding="utf-8"?>
<resources><declare-styleable name="CustomCalendar"><!--这四个颜色分别是月份、星期、日期、任务的背景色,只是方便调试测量时使用,正式试用时可配置透明色--><attr name="mBgMonth" format="color" /><attr name="mBgWeek" format="color" /><attr name="mBgDay" format="color" /><attr name="mBgPre" format="color" /><attr name="mTextColorMonth" format="color" />           <!--标题字体颜色--><attr name="mTextColorWeek" format="color" />            <!--星期字体颜色--><attr name="mTextColorDay" format="color" />             <!--日期字体颜色--><attr name="mTextColorPreFinish" format="color" />       <!--任务次数字体颜色--><attr name="mTextColorPreUnFinish" format="color" /><attr name="mSelectTextColor" format="color" />          <!--选中日期字体颜色--><attr name="mSelectBg" format="color" />                 <!--选中日期背景--><attr name="mCurrentBg" format="color" />                <!--当天日期背景--><attr name="mCurrentBgStrokeWidth" format="dimension" /> <!--当天日期背景虚线宽度--><attr name="mCurrentBgDashPath" format="reference" />    <!--当天日期背景虚线数组--><attr name="mTextSizeMonth" format="dimension" />        <!--标题字体大小--><attr name="mTextSizeWeek" format="dimension" />         <!--星期字体大小--><attr name="mTextSizeDay" format="dimension" />          <!--日期字体大小--><attr name="mTextSizePre" format="dimension" />          <!--任务次数字体大小--><attr name="mMonthRowL" format="reference" />            <!--月份箭头--><attr name="mMonthRowR" format="reference" />            <!--月份箭头--><attr name="mMonthRowSpac" format="dimension" /><attr name="mSelectRadius" format="dimension" />         <!--选中日期背景半径--><attr name="mMonthSpac" format="dimension" />            <!--标题月份上下间隔--><attr name="mLineSpac" format="dimension" />             <!--日期行间距--><attr name="mTextSpac" format="dimension" />             <!--日期和任务次数字体上下间距--></declare-styleable>
</resources>

3、onMeasure():

得到需要绘制的数据之后,接下来就是重写onMeasure()方法了,这个控件需要多宽多高?宽度直接填充父窗体即可,总高度=月份高度+星期高度+日期高度,相应的数据在上面的算法中都得到了

 /**计算相关常量,构造方法中调用*/private void initCompute(){mPaint = new Paint();bgPaint = new Paint();mPaint.setAntiAlias(true); //抗锯齿bgPaint.setAntiAlias(true); //抗锯齿map = new HashMap<>();//标题高度mPaint.setTextSize(mTextSizeMonth);titleHeight = FontUtil.getFontHeight(mPaint) + 2 * mMonthSpac;//星期高度mPaint.setTextSize(mTextSizeWeek);weekHeight = FontUtil.getFontHeight(mPaint);//日期高度mPaint.setTextSize(mTextSizeDay);dayHeight = FontUtil.getFontHeight(mPaint);//次数字体高度mPaint.setTextSize(mTextSizePre);preHeight = FontUtil.getFontHeight(mPaint);//每行高度 = 行间距 + 日期字体高度 + 字间距 + 次数字体高度oneHeight = mLineSpac + dayHeight + mTextSpac + preHeight;//默认当前月份String cDateStr = getMonthStr(new Date());
//        cDateStr = "2015年08月";setMonth(cDateStr);}@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {//宽度 = 填充父窗体int widthSize = MeasureSpec.getSize(widthMeasureSpec);   //获取宽的尺寸columnWidth = widthSize / 7;//高度 = 标题高度 + 星期高度 + 日期行数*每行高度float height = titleHeight + weekHeight + (lineNum * oneHeight);Log.v(TAG, "标题高度:"+titleHeight+" 星期高度:"+weekHeight+" 每行高度:"+oneHeight+" 行数:"+ lineNum + "  \n控件高度:"+height);setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),(int)height);}

  4、onDraw():

@Override
protected void onDraw(Canvas canvas) {drawMonth(canvas);drawWeek(canvas);drawDayAndPre(canvas);
}

①、绘制月份:

private void drawMonth(Canvas canvas){//背景bgPaint.setColor(mBgMonth);RectF rect = new RectF(0, 0, getWidth(), titleHeight);canvas.drawRect(rect, bgPaint);//绘制月份mPaint.setTextSize(mTextSizeMonth);mPaint.setColor(mTextColorMonth);float textLen = FontUtil.getFontlength(mPaint, getMonthStr(month));float textStart = (getWidth() - textLen)/ 2;canvas.drawText(getMonthStr(month), textStart,mMonthSpac+FontUtil.getFontLeading(mPaint), mPaint);/*绘制左右箭头*/Bitmap bitmap = BitmapFactory.decodeResource(getResources(), mMonthRowL);int h = bitmap.getHeight();rowWidth = bitmap.getWidth();//float left, float toprowLStart = (int)(textStart-2*mMonthRowSpac-rowWidth);canvas.drawBitmap(bitmap, rowLStart+mMonthRowSpac , (titleHeight - h)/2, new Paint());bitmap = BitmapFactory.decodeResource(getResources(), mMonthRowR);rowRStart = (int)(textStart+textLen);canvas.drawBitmap(bitmap, rowRStart+mMonthRowSpac, (titleHeight - h)/2, new Paint());
}

 ②、绘制星期:

private String[] WEEK_STR = new String[]{"Sun", "Mon", "Tues", "Wed", "Thur", "Fri", "Sat", };
private void drawWeek(Canvas canvas){//背景bgPaint.setColor(mBgWeek);RectF rect = new RectF(0, titleHeight, getWidth(), titleHeight + weekHeight);canvas.drawRect(rect, bgPaint);//绘制星期:七天mPaint.setTextSize(mTextSizeWeek);mPaint.setColor(mTextColorWeek);for(int i = 0; i < WEEK_STR.length; i++){int len = (int)FontUtil.getFontlength(mPaint, WEEK_STR[i]);int x = i * columnWidth + (columnWidth - len)/2;canvas.drawText(WEEK_STR[i], x, titleHeight + FontUtil.getFontLeading(mPaint), mPaint);}
}

③、绘制日期及任务:

private void drawDayAndPre(Canvas canvas){//某行开始绘制的Y坐标,第一行开始的坐标为标题高度+星期部分高度float top = titleHeight+weekHeight;//行for(int line = 0; line < lineNum; line++){if(line == 0){//第一行drawDayAndPre(canvas, top, firstLineNum, 0, firstIndex);}else if(line == lineNum-1){//最后一行top += oneHeight;drawDayAndPre(canvas, top, lastLineNum, firstLineNum+(line-1)*7, 0);}else{//满行top += oneHeight;drawDayAndPre(canvas, top, 7, firstLineNum+(line-1)*7, 0);}}}/*** 绘制某一行的日期* @param canvas* @param top 顶部坐标* @param count 此行需要绘制的日期数量(不一定都是7天)* @param overDay 已经绘制过的日期,从overDay+1开始绘制* @param startIndex 此行第一个日期的星期索引*/private void drawDayAndPre(Canvas canvas, float top,int count, int overDay, int startIndex){
//        Log.e(TAG, "总共"+dayOfMonth+"天  有"+lineNum+"行"+ "  已经画了"+overDay+"天,下面绘制:"+count+"天");//背景float topPre = top + mLineSpac + dayHeight;bgPaint.setColor(mBgDay);RectF rect = new RectF(0, top, getWidth(), topPre);canvas.drawRect(rect, bgPaint);bgPaint.setColor(mBgPre);rect = new RectF(0, topPre, getWidth(), topPre + mTextSpac + dayHeight);canvas.drawRect(rect, bgPaint);mPaint.setTextSize(mTextSizeDay);float dayTextLeading = FontUtil.getFontLeading(mPaint);mPaint.setTextSize(mTextSizePre);float preTextLeading = FontUtil.getFontLeading(mPaint);
//        Log.v(TAG, "当前日期:"+currentDay+"   选择日期:"+selectDay+"  是否为当前月:"+isCurrentMonth);for(int i = 0; i<count; i++){int left = (startIndex + i)*columnWidth;int day = (overDay+i+1);mPaint.setTextSize(mTextSizeDay);//如果是当前月,当天日期需要做处理if(isCurrentMonth && currentDay == day){mPaint.setColor(mTextColorDay);bgPaint.setColor(mCurrentBg);bgPaint.setStyle(Paint.Style.STROKE);  //空心PathEffect effect = new DashPathEffect(mCurrentBgDashPath, 1);bgPaint.setPathEffect(effect);   //设置画笔曲线间隔bgPaint.setStrokeWidth(mCurrentBgStrokeWidth);       //画笔宽度//绘制空心圆背景canvas.drawCircle(left+columnWidth/2, top + mLineSpac +dayHeight/2,mSelectRadius-mCurrentBgStrokeWidth, bgPaint);}//绘制完后将画笔还原,避免脏笔bgPaint.setPathEffect(null);bgPaint.setStrokeWidth(0);bgPaint.setStyle(Paint.Style.FILL);//选中的日期,如果是本月,选中日期正好是当天日期,下面的背景会覆盖上面绘制的虚线背景if(selectDay == day){//选中的日期字体白色,橙色背景mPaint.setColor(mSelectTextColor);bgPaint.setColor(mSelectBg);//绘制橙色圆背景,参数一是中心点的x轴,参数二是中心点的y轴,参数三是半径,参数四是paint对象;canvas.drawCircle(left+columnWidth/2, top + mLineSpac +dayHeight/2, mSelectRadius, bgPaint);}else{mPaint.setColor(mTextColorDay);}int len = (int)FontUtil.getFontlength(mPaint, day+"");int x = left + (columnWidth - len)/2;canvas.drawText(day+"", x, top + mLineSpac + dayTextLeading, mPaint);//绘制次数mPaint.setTextSize(mTextSizePre);MainActivity.DayFinish finish = map.get(day);String preStr = "0/0";if(finish!=null){//区分完成未完成if(finish.finish >= finish.all) {mPaint.setColor(mTextColorPreFinish);}else{mPaint.setColor(mTextColorPreUnFinish);}preStr = finish.finish+"/"+finish.all;}else{mPaint.setColor(mTextColorPreUnFinish);}len = (int)FontUtil.getFontlength(mPaint, preStr);x = left + (columnWidth - len)/2;canvas.drawText(preStr, x, topPre + mTextSpac + preTextLeading, mPaint);}}

 5、事件处理:

事件相关知识点也是自定义控件比较重要的内容,后面有空会详细介绍。下面我们看看这个控件需要处理那些事件。当点击箭头时需要增减月份,点击日期时需要置为选中。控件接受到事件之后,我要怎样知道点击的是箭头还是日期还是其他部位?只能通过事件的坐标计算了,如果在某个范围之内即可,在上面的分析图中,将控件划分成了很多小网格,这些小网格的坐标范围都是确定的(根据宽高等数据),事件发生后,只需要判断事件点坐标是否落入相应区域即可,然后边测试边修改一些细节问题,下面是事件处理先关的代码:

//焦点坐标private PointF focusPoint = new PointF();@Overridepublic boolean onTouchEvent(MotionEvent event) {int action = event.getAction() & MotionEvent.ACTION_MASK;switch (action) {case MotionEvent.ACTION_DOWN:focusPoint.set(event.getX(), event.getY());touchFocusMove(focusPoint, false);break;case MotionEvent.ACTION_MOVE:focusPoint.set(event.getX(), event.getY());touchFocusMove(focusPoint, false);break;case MotionEvent.ACTION_OUTSIDE:case MotionEvent.ACTION_CANCEL:case MotionEvent.ACTION_UP:focusPoint.set(event.getX(), event.getY());touchFocusMove(focusPoint, true);break;}return true;}/**焦点滑动*/public void touchFocusMove(final PointF point, boolean eventEnd) {Log.e(TAG, "点击坐标:("+point.x+" ,"+point.y+"),事件是否结束:"+eventEnd);/**标题和星期只有在事件结束后才响应*/if(point.y<=titleHeight){//事件在标题上if(eventEnd && listener!=null){if(point.x>=rowLStart && point.x<(rowLStart+2*mMonthRowSpac+rowWidth)){Log.w(TAG, "点击左箭头");listener.onLeftRowClick();}else if(point.x>rowRStart && point.x<(rowRStart + 2*mMonthRowSpac+rowWidth)){Log.w(TAG, "点击右箭头");listener.onRightRowClick();}else if(point.x>rowLStart && point.x <rowRStart){listener.onTitleClick(getMonthStr(month), month);}}}else if(point.y<=(titleHeight+weekHeight)){//事件在星期部分if(eventEnd && listener!=null){//根据X坐标找到具体的焦点日期int xIndex = (int)point.x / columnWidth;Log.e(TAG, "列宽:"+columnWidth+"  x坐标余数:"+(point.x / columnWidth));if((point.x / columnWidth-xIndex)>0){xIndex += 1;}if(listener!=null){listener.onWeekClick(xIndex-1, WEEK_STR[xIndex-1]);}}}else{/**日期部分按下和滑动时重绘,只有在事件结束后才响应*/touchDay(point, eventEnd);}}//控制事件是否响应private boolean responseWhenEnd = false;/**事件点在 日期区域 范围内*/private void touchDay(final PointF point, boolean eventEnd){//根据Y坐标找到焦点行boolean availability = false;  //事件是否有效//日期部分float top = titleHeight+weekHeight+oneHeight;int foucsLine = 1;while(foucsLine<=lineNum){if(top>=point.y){availability = true;break;}top += oneHeight;foucsLine ++;}if(availability){//根据X坐标找到具体的焦点日期int xIndex = (int)point.x / columnWidth;if((point.x / columnWidth-xIndex)>0){xIndex += 1;}
//            Log.e(TAG, "列宽:"+columnWidth+"  x坐标余数:"+(point.x / columnWidth));if(xIndex<=0)xIndex = 1;   //避免调到上一行最后一个日期if(xIndex>7)xIndex = 7;   //避免调到下一行第一个日期
//            Log.e(TAG, "事件在日期部分,第"+foucsLine+"/"+lineNum+"行, "+xIndex+"列");if(foucsLine == 1){//第一行if(xIndex<=firstIndex){Log.e(TAG, "点到开始空位了");setSelectedDay(selectDay, true);}else{setSelectedDay(xIndex-firstIndex, eventEnd);}}else if(foucsLine == lineNum){//最后一行if(xIndex>lastLineNum){Log.e(TAG, "点到结尾空位了");setSelectedDay(selectDay, true);}else{setSelectedDay(firstLineNum + (foucsLine-2)*7+ xIndex, eventEnd);}}else{setSelectedDay(firstLineNum + (foucsLine-2)*7+ xIndex, eventEnd);}}else{//超出日期区域后,视为事件结束,响应最后一个选择日期的回调setSelectedDay(selectDay, true);}}/**设置选中的日期*/private void setSelectedDay(int day, boolean eventEnd){Log.w(TAG, "选中:"+day+"  事件是否结束"+eventEnd);selectDay = day;invalidate();if(listener!=null && eventEnd && responseWhenEnd && lastSelectDay!=selectDay) {lastSelectDay = selectDay;listener.onDayClick(selectDay, getMonthStr(month) + selectDay + "日", map.get(selectDay));}responseWhenEnd = !eventEnd;}

 6、最终效果如下:

好了,基本上自定义view实现日历控件就是这些了,感觉是不是没有想想中的那么难呢?

ok,我要休息了,有空再聊!!!

Android自定义view实现日历控件相关推荐

  1. android自定义view圆,Android自定义View圆形百分比控件(一)

    做一个自定义View的小练习,效果如下 只需要画一个圆.一个圆弧.一个百分比文本,添加一个点击事件,传入百分比重绘 1.在res/values文件夹下新建attrs.xml文件,编写自定义属性: 2. ...

  2. Android自定义View 实现窗帘控件

    需求分析 这里只作简单介绍,最后会分享源码地址 窗帘分为三部分,上面的窗帘杆,杆下面的窗帘布,以及布中间的滑块,实现还是蛮简单的,我们可以通过自定义view把这个窗帘画出来 窗帘杆是一个上面是圆角,下 ...

  3. Android开源的精美日历控件,热插拔设计的万能自定义UI

    UI框架应该逻辑与界面实现分离,该日历控件使用了热插拔的设计 ,简单几步即可实现你需要的UI效果,热插拔的思想是你提供你的实现,我提供我的插座接口,与自定义Behavior是一样的思想. 听说第一页无 ...

  4. Android开源的精美日历控件,热插拔设计的万能自定义UI,你值得拥有

    XML用法 如果需要在日历控件下方使用其它控件,使用CalendarLayout控件即可,calendar_content_view_id为其它控件的id,支持任意控件,如RecyclerView.L ...

  5. Android开源的精美日历控件,热插拔设计的万能自定义UI,看完直接怼产品经理

    <attr name="current_month_text_color" format="color" /> <!--当前月份的字体颜色-- ...

  6. android+高仿+日历,Android开源的精美日历控件,热插拔设计的万能自定义UI

    UI框架应该逻辑与界面实现分离,该日历控件使用了热插拔的设计 ,简单几步即可实现你需要的UI效果,热插拔的思想是你提供你的实现,我提供我的插座接口,与自定义Behavior是一样的思想. 听说第一页无 ...

  7. Android开源的精美日历控件,热插拔设计的万能自定义UI,flutter调用原生sdk

    XML用法 如果需要在日历控件下方使用其它控件,使用CalendarLayout控件即可,calendar_content_view_id为其它控件的id,支持任意控件,如RecyclerView.L ...

  8. Android 自定义日期段选择控件,开始日期-结束日期。

    开发中碰到个需求,需要在一个控件中选择完成开始和结束日期.实现的过程走的是程序员开发的老路子,找到轮子后自己改吧改吧就成了.去年做的找不到参考的文章连接了,请原博主见谅. 当时做的时候有几个需求:1. ...

  9. Android自定义一个播放器控件

    介绍 最近要使用播放器做一个简单的视频播放功能,开始学习VideoView,在横竖屏切换的时候碰到了点麻烦,不过在查阅资料后总算是解决了.在写VideoView播放视频时候定义控制的代码全写在Actv ...

  10. Android开源中国客户端学习 (自定义View)左右滑动控件ScrollLayout

    左右滑动的控件我们使用的也是非常多了,但是基本上都是使用的viewpager 等 android基础的控件,那么我们有么有考虑过查看他的源码进行定制呢?当然,如果你自我感觉非常好的话可以自己定制一个, ...

最新文章

  1. DC使用教程系列2-时钟的概念与环境接口面积约束脚本
  2. C++ Primer 5th笔记(chap 16 模板和泛型编程)效率与灵活性
  3. 大牛书单 | 迎金秋,与腾讯技术大咖共读好书
  4. python中long类型_浅谈python 四种数值类型(int,long,float,complex)
  5. C++获取本机所有ip地址,可区分类型是有线无线虚拟机还是回环
  6. C++ STL容器vector篇(三) vector容器大小和数组大小, 插入和删除元素, 存储和读取元素
  7. linux 分区个数限制,Linux分区个数限制[转载]
  8. Xcode:PhoneGap 2.5.0项目创建方法
  9. linux登录vnc的命令行,用VNC远程登陆linux (ZT)
  10. 修改hosts文件并保存
  11. 计算机维修需要什么工具,一种计算机维修工具的制作方法
  12. jconsole是否可以在生产环境使用_使用JCONSOLE远程监控JVM
  13. 机器学习sklearn----通过轮廓系数确定适合的n_clusters
  14. 立创EDA——PCB的布局(四)
  15. 隐式图层动画 (Implicit Layer Animation)
  16. 英语词根记忆法(8)
  17. 移动OA办公——Smobiler第一个开源应用解决方案,快来get吧
  18. 【NLP】Praat库(1) 安装及初步使用
  19. 《红楼梦》金陵十二钗判词及赏析_马立杰_新浪博客
  20. Oracle项目管理系统之预算变更

热门文章

  1. 拯救节日邮件!专属这个节假季的EDM营销方案
  2. bzoj3786: 星系探索 //ETT
  3. STM32 SHT10温湿度传感器的信号采集
  4. 【春节档排片地域可视化分析】
  5. 基于vsftpd搭建ftp服务器
  6. linux kettle命令,Linux下用命令来执行kettle文件资源库的文件ktr与kjb的方法
  7. #微信公众号互联登录-01#
  8. DateTime.ToString()的用法
  9. Chromium Embedded Framework (CEF) 介绍
  10. 初来乍到,请多多指教