1. 前言
1.最近笔者在实习,需要做一个炫酷的自定义日历来在满足实际项目的需求,当让这里不能把实际需求放在这里,不然我就那啥了,你懂的。
2.所以自己又重新设计了需求(内容完全不一样了,但UI有些类似),写了一个新的、十分炫酷的个性化日历,来记录您最近两个月的学习情况(记录您预期学习计划,以及实际完成的情况)。
3.这篇博客我会尽可能的详细的去创作,所以这对于新手来说提升会很大,包括我自己,我也是刚刚学习Android不久,其实刚拿到需求的时候,我根本不知道该如何下手,不怕你们笑话,我当时连自定义View都不太会,业务逻辑编写能力也是弱的不行,但最后我还是完成了,所以我在这里会穿插的分享一下我学习的方法以及学习的思路,希望感兴趣的朋友耐心的阅读,同时也希望您能够喜欢。
PS:由于篇幅和知识重点分布的特点,我把它拆分成了三个章节,一点一滴记录它是如何完成的,本Demo的推荐阅读顺序是:
【 ① —> ② —> ③ 】或者【 ② —> ③】,本章编号为“②”
相关参考链接:
2. 最终的效果图如下:
3. 本章简介
这一章节呢,我们主要是在原来的Demo的框架之上,手把手分享如何写一个极具个性化的【自定义日历】,这个自定义日历是【英式】的,换句话说就是【“星期的标识”是从星期日开始排列(日 一 二 三 四 五 六)显示的】,说到这里有的读者肯定会有疑惑或者不太爽了,为啥?你可能会想,作为一个高素质的爱国公民对吧,为什么不写成中式的呢,“星期的标识”从星期一到星期天多好看多和谐啊。你说的没错,那是也必须的,所以不必担心,最后会写成中式的日历,你仔细看我的最终版本就是中式的:“星期的标识”是从星期一开始排列(一 二 三 四 五 六 日)显示的,其实先写【英式】的主要原因是:【中式】的日历比【英式】的日历的要复杂很多,如果你理解了【英式】日历的编写核心,再由它转化为【中式】日历就相对容易多了,这个你在阅读完博客的时候你就会体会到的。
这个日历Demo主要是用来记录你的【预期的学习计划】和【具体的完成情况】,首先显示的就是你的【当月的学习记录】和【上一个月的学习记录】的总览,点击任意一个你想查看的且有记录的那个日期,就会进入到那日全天的【学习记录详情】,包括预期设定的学习时间,具体完成的状态,实际完成的时间,具体学习的科目,每个学科的具体学习内容,和每个学习内容的具体时间分配等等。
PS:这个Demo的框架不清楚、不了解也不影响这篇博文的阅读和理解,因为这个Demo的框架仅仅只是一个ViewPager+Fragment 个性化经典写法而已,很简单,如果有对ViewPager+fragment不会使用或者对它感兴趣的可以参考我的这篇博客:
本章成果图
4. 极具个性化【自定义日历】的具体实现
(1)简述自定义View
为什么要自定义控件?(有以下几种原因,或者说情况)
- 特定的显示风格
我们的App肯定是有特殊的效果的,别人是没有的,是唯一的,这个效果呢,必须通过自定义才能显示出来。- 处理特有的用户交互
就是我们与用户交互的方式比较特殊,比如说我们的本博文介绍的自定义炫酷日历,既有特殊显示风格,又有特殊的交互需求,那么此时没办法,我们就需要去自定义控件。- 优化我们的布局
我们可以通过各种嵌套的方式去实现一个布局,但这样我们大家都非常的清楚,我们测量,绘制的过程是很慢的,那么此时我们肯定会想办法,能不能自己用自定义的方法去实现一个布局,从而提升我们的一个效率。- 为了封装,提高开发效率
比如说我们实际开发中,有一些地方的控件啊,经常要复用,看着很是不舒服所以把它封装一下,变成一个自定义控件。
如何自定义控件的?(或者说自定义的步骤)简述:
- 自定义View实际需要的属性
- 在View的构造方法中获取我们的需要的自定义属性
- 重写onMesure () 方法
- 重写onDraw()方法
- 重写onTouchEvent()方法
(2)设计的整体思路
写在前面:作为一个android新手,接到这样一个需求,真的是一脸懵逼,根本不知道该如何下手,真不怕你们见笑,我当时连自定义View都不会,业务逻辑的编写能力也是弱的可以,但最后还是按照需求完成了,那么这是如何做到的呢,这很有必要分享一下,核心是四个字【模仿创作】,这对于一个新手来说非常重要,因为作为新手缺乏经验,自己写又写不出来,你想在网上直接找到现成的代码直接用在实际项目中吧,可以这么说:几乎是不可能的;所以【模仿创作】无疑是最好的方法了。分享给大家我的【模仿创作】的三部曲:
- 网上找相关的类似的demo,博客,挑几个感觉自己需求差不多的,把代码拷贝下来,好好阅读和体会里面的具体思路和实现。
- 看的差不多了之后,新建一个android工程,一行一行的、慢慢的去敲,边敲边运行,先不修改也不扩展,敲一个一模一样的demo出来即可。
- 完成上面两步之后,对自己要做的东西,肯定有了一定的核心知识技术储备,对实际项目需求的编写思路、以及对参考demo修改扩展的想法,接下来就是【模仿创作】的时候了,根据自己实际需求去编写的代码,这样既有思路又有底气,成功率大大的增高。
PS:说到这里,有的读者可能觉得第二个步骤是多余的,看懂之后直接复制到自己工程上,然后在修改不更快更高效吗?千万不要觉得这是多余没必要的。是!不否认直接新建工程之后一通复制粘贴,再在上面进行修改挺爽的,但是这样做有一些弊端:(1)你不一定真正看懂了参考demo的代码,不知道每一行代码为什么要这样写,(2)正因为如此,你可能改不动代码,就算能改一点点,改到最后可能改不下去了,为啥,bug一堆,程序与你一同崩溃,最终是完不成自己的需求的。如果认真的完成了第二步的话,你会有很多收获,(1)不仅仅是案例当中的核心技术知识,还有编写代码的风格与思想,都是很值得学习的,(2)还能大大的提高完成任务需求的概率,何乐而不为呢,对吧?
本Demo是【核心思想】和【开发模式】是参考网上适合自己实际需求案例上的 ,【核心思想】是“面向对象“:把日历当中的每一个日期当作一个对象,所以整个日历就是由一只”画笔“按照一定的计算方法循环把一个一个的日期画出来,从而构成的;【开发模式】采用的是”MVC模式“:模型(model)-视图(view)-控制器(controller),这样思路会很清晰,详情马上就会揭晓。
(3)具体实现步骤
PS:先分享【自定义日历】的基本实现,再说【学习记录】的编写过程,因为知道【自定义日历】的基本实现,就很轻松的能把【学习记录】搞定
(1)创建单个日期的模型类(DayModel)
建立模型之前呢,首先我们得了解一下本Demo中的自定义日历控件的一些基本情况,以便于建立模型,如下图:
PS:如图我们把一个日期看做一个对象,想要准确对这个日期对象的进行显示,我们需要确定以下几个关键点:
- 首先要确定的是这个日期对象所在的【行】和【列】。
- 这样我们就要知道每个对象的宽和高,这样我们才可以方便的确定一个日期对象具体的位置。
- 然后在是看日期对象的里面包含着哪些内容,比如说包含具体的日期,本demo还包含了【学习记录】等等。
- 其次要注意日期对象背景类型,当日期对象被选中和没有选中是不同的背景,这样一个日期对象基本就可以确定下来了。
public class DayModel {
public int width;//单个日期格子的宽度
private int height;//单个日期格子的高度
public String text;//日期的文本
public int textColor;//日期文本字体的颜色
public float textSize;//日期文本字体的大小
/**
* 日期背景的类型,
* 0:代表无任何背景,1:代表默认情况下的日期,2:代表选中的日期
*/
public int backgroundStyle;
public int locationX;//日期在第几列,从第 0 行开始计算
public int locationY;//日期在第几行,从第 0 列开始计算
//所以第一个对象在(0,0)坐标上,注意记一下
/**
* 创建单个日期模型对象的构造函数
*
* @param width 每个日期的宽度
* @param height 每个日期的高度
*/
public DayModel(int width, int height) {
this.width = width;
this.height = height;
}
定义完我们所需要的属性之后,在给DayModel添加一个可以画它的方法,这样一个较为完整的Day Model就差不多了
PS:要注意一定要是【先画背景边框】,【再画数字】,否者当选中的日期的时候,日期文本就会被遮住,等到有【学习记录】的时候,【学习记录要最后画】,不然也会被遮住的哦(●’◡’●)
/**
* 画我们的单个日期模型对象
*/
public void drawDayModel(Context context, Canvas canvas, Paint paint) {
/**
* PS: 先画背景,在画数字
*/
//画背景
drawBackground(canvas, paint);
//画日期文本
drawText(canvas, paint);
}
接下就是编写【画日期背景】和【画日期文本】具体实现的时候了:按顺序首先是画我们的【日期背景】
【画日期的背景】要注意两个要点:
- 背景如何绘画
- 画的是一个矩形(图形)
- 背景位置的计算
- 因为绘画的是一个矩形,所以我们只需要知道所画图形的左上角和右下角就行了,主要是左上角坐标计算,(右下角计算就是把它坐标的横纵坐标各加上日期对象的宽度和高度就行了)
详情请看下图:
具体代码如下:
/**
* 画日期的背景
* @param canvas 画布
* @param paint 画笔
*/
private void drawBackground(Canvas canvas, Paint paint) {
//画背景 根据背景状态设置画笔类型
if (backgroundStyle == 0) {
return;
}
switch (backgroundStyle) {
//一般情况
case 1:
paint.setColor(0xFFECF1F4); // 浅灰
paint.setStyle(Paint.Style.STROKE);// 空心
paint.setStrokeWidth(3);
break;
//日期被选中的情况
case 2:
paint.setColor(0xAA2BCEA3); // 绿
paint.setStyle(Paint.Style.FILL); // 填充
break;
}
//计算位置(矩形边框的左上角坐标)
float rx = locationX * width;//(矩形边框的左上角横坐标)
float ry = locationY * height;//(矩形边框的左上角纵坐标)
//左上角坐标,右下角坐标
canvas.drawRect(rx, ry, rx + width, ry + height, paint);
}
其次是画我们的日期文本
- 日期文本如何画
- 画的是一个文本(文字)
- 日期文本位置的计算
- 和背景计算类似
- 【画文本】和【画图形】是不同的:
- 【画图形】是从图形的【left和top】的位置开始往右下方向画,
- 【画文字】是从文字的【left和文字的baseline】往右上方画
详情请看下图:
具体代码如下:
/**
* 画日期文本
*
* @param canvas 画布
* @param paint 画笔
*/
private void drawText(Canvas canvas, Paint paint) {
//根据单个日期宽度设置字体的大小
textSize = (float) (width / 3.5);
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 = locationX * width + (width - w) / 2; //ok居中
float y = locationY * height + textSize; //就让字体贴着背景的顶部
canvas.drawText(text, x, y, paint);
}
(2)创建日期的管理类(DayController)
DayController:日期管理类主要的功能是创建创建日期模型对象集合,包括以下主要计算如下关键变量:
- 日期对象的宽度
- 日期对象的高度
- 日期对象的具体位置(具体在第几行,第几列)
- 日期显示的具体状态
PS:还有一些小细节,不太好表述,在注释中写的很清楚哦,超详细,绝对能看懂噢 O(∩_∩)O
public class DayController {
public static String currentTime;//记录当前的时间
public static int current = -1;//当前日期
public static int tempCurrent = -1;//储存当前的日期
private static int select = -1;//储存选中的日期
//日期的静态数据块
static 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 calendar 日历类
* @param width 自定义控件的宽度
* @param height 自定义控件的高度
* @return DayModel的集合
*/
public static List<DayModel> createDayModelByCalendar(Calendar calendar, int width, int height) {
List<DayModel> mDayModels = new ArrayList<>();//创建日期对象集合
DayModel mDayModel = null;//创建日期对象
int dayModelWidth = width / 7;//日期对象的宽度
//获取当前月份最多有几个星期
// calendar.getActualMaximum(Calendar.WEEK_OF_MONTH) = 5 (2017年6月7日22:42:36)
int dayModelHeight = height / (calendar.getActualMaximum(Calendar.WEEK_OF_MONTH));//日期对象的高度
// 获取当前月份有多少天!!!
int count = calendar.getActualMaximum(Calendar.DAY_OF_MONTH); // 值为:30,(2017年6月7日22:42:36)
/**
* 生成每一个日期的对象,其中第 i 次,创建的是第 i+1 天
*/
for (int i = 0; i < count; i++) {
mDayModel = new DayModel(dayModelWidth, dayModelHeight);
//从一号开是赋值
mDayModel.text = dayArray[i];
//也是从当月的一号开始设置
calendar.set(Calendar.DAY_OF_MONTH, i + 1);
/**
* 设置每个日期的位置,即在哪一行,哪一列
*/
//当前周在这一月的第几周,值为1,就是第一周(范围是1~5/1~6)
int weekOfMonth = calendar.get(Calendar.WEEK_OF_MONTH);
//当前日期是这一周的第几天,注意默认星期天是第一天,值为1,表示星期天;值为5,表示星期四 (范围是1~7)
int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
//行号
mDayModel.locationY = weekOfMonth-1; //循环从0开始,又因为第0行被星期标识占据了第几周就是第几行,
//列号
/**
* 注意“ -1 ”的含义。。。
* 由于星期标识是:日,一,二,三,四,五,六,所以
* 默认星期天是第一天,值为 1,表示星期天;值为 5,表示星期四,刚好与星期标识对上,
* 所以-1的含义千万不要理解错了,以为是错位了(这只是巧合)
*
* “ -1 ”表示的是:可以理解为从第 0 列开始画,因为(0,0)的才能画到第一个上嘛,
* 星期四就是第n行,第4列(n,4),可是星期四的值为5,所以要减去1
*/
mDayModel.locationX = dayOfWeek - 1;
/**
* 设置日期显示的状态
*/
//设置当前日期的显示状态
if (i == current - 1 && i != select - 1) {//加上【i != select - 1】是为了当前日期也能点击变色,否则进不了【设置你选中的日期显示状态】这个if里面去
mDayModel.backgroundStyle = 1;
mDayModel.textColor = 0xFF2BCEA3;// 绿
}
//设置你选中的日期显示状态
else if (i == select - 1) {
mDayModel.backgroundStyle = 2;
mDayModel.textColor = 0xFFFAFBFE; //白
}
//设置默认的日期显示状态
else {
//周末
if (dayOfWeek == 1 || dayOfWeek == 7) {
mDayModel.backgroundStyle = 1;
mDayModel.textColor = 0xFFB3B3B3;// 浅灰
}
//一般情况
else {
mDayModel.backgroundStyle = 1;
mDayModel.textColor = 0xFF5E5E5E;// 深灰
}
}
mDayModels.add(mDayModel);
}
return mDayModels;
}
... ... (省略Getter和Setter方法)
}
(3)创建自定义View(CalendarView)
CalendarView :接下来就是重头戏了,开始我们自定义View了(自定义日历),由于本Demo给【日期的控制管理类赋值】的【宽度】和【高度】,就是在【布局文件】中为【自定义日历控件】设置的【宽度】和【高度】,所以我们编写这个类的时候,就【省略了onMesure( )】方法,所以这里主要重写了【onDraw( )】和【onTouchEvent( )】方法,重点介绍【onTouchEvent( )】方法。
具体代码如下:
public class CalendarView extends View {
Context context;//上下文环境
Paint paint;//画笔
Calendar calendar;//日历类可以获取到日期相关有用数值
private SimpleDateFormat formatter;//格式化工具
private String selectTime;//记录选择的日期
public CalendarView(Context context) {
super(context);
this.context = context;
//初始化控件
initView();
}
public CalendarView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
//初始化控件
initView();
}
public CalendarView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
//初始化控件
initView();
}
/**
* 初始化控件
*/
public void initView() {
//初始化画笔
paint = new Paint();
//消除图片锯齿
/**
* ① 当我们用Canvas绘制位图的时候,如果对位图进行了选择,则位图会出现锯齿。
* ② 在用View的RotateAnimation做动画时候,如果View当中包含有大量的图形,也会出现锯齿。
* paint.setAntiAlias(true);
* paint.setBitmapFilter(true)
*/
paint.setAntiAlias(true);
//初始化日历
calendar = Calendar.getInstance();
//设置“当前的时间” int
DayController.setCurrent(calendar.get(Calendar.DAY_OF_MONTH));
//设置“储存当前的日期”为多少 int
DayController.setTempCurrent(calendar.get(Calendar.DAY_OF_MONTH));
//设置“记录当前的时间” string
DayController.setCurrentTime(calendar.get(Calendar.MONTH) + "" + calendar.get(Calendar.YEAR));
}
/**
* 重写onDraw()方法
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//为日期集合赋值(给【日期的控制管理类赋值】的【宽度】和【高度】,就是在【布局文件】中为【自定义日历控件】设置的【宽度】和【高度】)
List<DayModel> dayModels = DayController.createDayModelByCalendar(calendar, getMeasuredWidth(), getMeasuredHeight());
//用循环把日期一个一个画出来,组成一个完整的日历
for (DayModel dayModel : dayModels) {
dayModel.drawDayModel(context, canvas, paint);
}
}
onTouchEvent( ): 这个方法很重要,为你的自定义控件添加点击事件,感觉就让它“活”起来了,瞬间用户体验就来了,马上就为你所编写的App在用户心中加分,所以一定要重视这一环节:这个环节呢,本Demo中自定义日历,主要是点击有学习记录的日期跳转到【单日学习记录详情页】查看选中日期【当天具体的学习计划与完成情况】,当然有几个要求:
- 点击自己的自定义日历控件中的任何一个位置都会获取到一个有效坐标,坐标的最小单位就是日期对象的位置坐标。例如:(0,0),(2,6)等等。
- 点击日期对象意外空白的位置不能有点击响应事件,应该屏蔽
- 点击没有学习记录的日期对象同样也是不能有点击响应事件的,也应该屏蔽
获取到有效坐标相对较简单,之前也类似的说过了,这里就不赘述了,这里主要介绍一下介绍如何屏蔽日期对象以外空白位置的点击事件,如下图:
PS:下面这段代码先介绍如何屏蔽日期对象以外空白位置的点击事件,也不跳转,就弹出一个Toast显示点击日期对象的具体日期,慢慢来写看的更清楚,因为马上就会介绍到它们
/**
* 重写点击事件
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
//计算点击的是屏幕的哪一个坐标
float x = event.getX();
float y = event.getY();
//计算点击的是日历中的哪一个位置
int locationX = (int) (x * 7 / getMeasuredWidth());
int locationY = (int) ((calendar.getActualMaximum(Calendar.WEEK_OF_MONTH)) * y / getMeasuredHeight());
// KLog.d("X:" + x);
// KLog.d("Y:" + y);
// KLog.d("locationX:" + locationX);
// KLog.d("locationY:" + locationY);
//选中的日历第一行的时候,前面有几个空格点击的时候肯定不能做相应的操作
if (locationY == 0) {
//先设置当前日期为当月的一号,待会要获得当月一号在一个星期的第几天,然后与locationX比较
calendar.set(Calendar.DAY_OF_MONTH, 1);
if (locationX < calendar.get(Calendar.DAY_OF_WEEK) - 1) {
Toast.makeText(context, "无效点击", Toast.LENGTH_SHORT).show();
return false;
}
}
//选中的日历第最后一行的时候,后面有几个空格点击的时候肯定也是不能做相应的操作
else if (locationY == calendar.getActualMaximum(Calendar.WEEK_OF_MONTH) - 1) {
//先设置当前日期为当月的最后一号,待会要获得当月最后一号在一个星期的第几天,然后与locationX比较
calendar.set(Calendar.DAY_OF_MONTH, calendar.getActualMaximum(Calendar.DAY_OF_MONTH));
if (locationX > calendar.get(Calendar.DAY_OF_WEEK) - 1) {
Toast.makeText(context, "无效点击", Toast.LENGTH_SHORT).show();
return false;
}
}
/**
* 设置选中的日期,由于:
* 1.本日历的【Calendar.DAY_OF_WEEK】,【Calendar.WEEK_OF_MONTH】的值都是从"1"开始的
* 2.【locationX】和【locationY】的值是从"0"开始的
* 所以要正确获得选中的日期,locationX/Y都得加上"1"
*
* PS:不清楚的看一眼【Calendar.DAY_OF_WEEK】:日 一 二 三 四 五 六
* 1 2 3 4 5 6 7
*/
calendar.set(Calendar.WEEK_OF_MONTH, (locationY + 1));
calendar.set(Calendar.DAY_OF_WEEK, (locationX + 1));
}
if (event.getAction() == MotionEvent.ACTION_UP) {
DayController.setSelect(calendar.get(Calendar.DAY_OF_MONTH));
formatter = new SimpleDateFormat("yyyy-MM-dd");
Date time = calendar.getTime();
selectTime = formatter.format(time);
Toast.makeText(context, selectTime, Toast.LENGTH_SHORT).show();
invalidate();
}
return true;
}
写到这里一个自定义日历的基本显示就算是写完了,接下来就是到布局文件里面去引用一下就好啦,很简单,【包名】点【类名】就好了,在两个的Fragment(LastMonthFragment和CurrentMonthFragment)的布局文件中都要写哦:
<cn.dragoliu.ld_calendarview_en.calendar.CalendarView
android:id="@+id/calendarCurrent"
android:layout_width="match_parent"
android:layout_height="400dp"
android:background="#FFFFFF" />
到这还没写完,这样向左(向右)滑动我们viewpager,指示器虽然切换到了上一个月(当前月),但实际上两个Fragment上显示的总是当前月,怎么办呢,这时我们需要在【CalendarView类】中添加一个【改变日期的方法】,是它可以正确的在当月和上月进行切换,代码如下:
/**
* 改变日期,并更改当前状态,由于绘图是在calendar基础上进行绘制的,所以改变calendar就可以改变日历的月份
*
* @param calendar
*/
public void setCalendar(Calendar calendar) {
this.calendar = calendar;
//我要显示的日历时间和系统获取的时间一样的时候,设置系统的即可
if ((calendar.get(Calendar.MONTH) + "" + calendar.get(Calendar.YEAR)).equals(DayController.getCurrentTime())) {
DayController.setCurrent(DayController.getTempCurrent());
}
//我要显示的日历时间和系统获得时间不一样的时候,设置我要想要设置的
else {
DayController.setCurrent(-1);
}
invalidate();
}
接下来:① 在LastMonthFragment添加一个【切换到上一个月】的方法:
/**
* 切换到上一个月
* @param calendar
*/
public void changeData(Calendar calendar) {
calendar.add(Calendar.MONTH, -1);
calendarLast.setCalendar(calendar);
}
② 在CurrentMonthFragment添加一个【更新到当前月份】的方法:
/**
* 更新到当前月份
* @param calendar
*/
public void upDateData(Calendar calendar) {
calendar.add(Calendar.MONTH, +1);
calendarCurrent.setCalendar(calendar);
}
最后在Viewpager监听器中的onPageSelected()方法中调用,如下图所示:
写到这里,这个Demo的核心部分基本就完成了,效果图如下:
(4)添加学习记录
(1)添加月学习记录总览(自定义日历中添加)
思路分析:
有了之前的知识基础作为铺垫,想要画学习记录,是非常简单的。因为从图中你可以看出【一条学习记录】,是由【记录的背景】和【时间文本】构成,简言之就是【背景+文本】,有没有觉得很熟悉呢?对没错,这和【单个日期对象画法】一模一样,只不过日期对象的背景大一些,且为空心,记录对象的背景小一些,且为实心;想要多条记录,放到一个日期对象里,当然就是循环啦,就好比:多个日期对象放到一个日历当中一样。是不是一模一样,很神奇吧!
好的,说到这里【单条的记录】已经没问题了,那么怎么样把一条一条的记录精准显示到自定义日历上呢,详细计算方式,如下图解析:
具体实现:
(1)在DayModel中修改:
首先添加以下属性:
public int recordNumber;//学习记录的个数
public List<String> recordTimes;//学习计划开始的的时间
/**
* 学习记录的状态,
* 0为不画;1:已完成;2:未完成;3:无记录
*/
public List<Integer> recordStates;
然后到“画DayModel中单个日期模型对象”中添加画学习记录的方法:
最后详细设计画学习记录的方法:
/**
* 画学习记录,从背景的底边开始往上画,容易计算一点
* <p>
* 状态(背景)的位置参考上面的 drawBackground()
* 记录时间(文字)的位置参考上面的 drawText()
*
* @param canvas 画布
* @param paint 画笔
*/
private void drawRecord(Canvas canvas, Paint paint) {
//当没有记录的时候,将不调用该方法
if (recordNumber == 0) {
return;
}
//计算位置,从背景的底部开始的画,学习记录的高度为背景高度的1/8
//状态(背景)
//状态(背景)---左上角坐标
float rx = locationX * width;
float ry = locationY * height + height * 7/ 8;
//状态(背景)---右下角坐标
float rx1 = locationX * width + width;
float ry1 = locationY * height + height;
//时间记录(文本)
float x = rx + (width) / 2; //水平居中(差一点)
float y = ry1-2;//竖直居中(由于记录宽度不大,向上移动一点点就可以居中,粗略计算它即可)
for (int i = recordNumber; i > 0; i--) {
switch (recordStates.get(i - 1)) {
//已完成学习计划
case 1:
paint.setColor(0xFF2BCEA3);//绿
paint.setStyle(Paint.Style.FILL);//填充
break;
//未完成学习计划
case 2:
paint.setColor(0xFFFF5543);//红
paint.setStyle(Paint.Style.FILL);//填充
break;
//无记录
case 3:
paint.setColor(0xFFC1C1C1);//浅灰
paint.setStyle(Paint.Style.FILL);//填充
break;
}
//画学习记录的状态(背景)
canvas.drawRect(rx, ry, rx1, ry1, paint);
//画学习记录的时间
textSize = (float) (width / 5);
paint.setTextSize(textSize);
paint.setColor(0xFFFFFFFF); //白色
paint.setStyle(Paint.Style.FILL);
//计算文字的宽度
text = recordTimes.get(i - 1);
Rect rect = new Rect();
paint.getTextBounds(text, 0, text.length(), rect);
int w = rect.width();
canvas.drawText(text, x - w / 2, y, paint);//文字居中了
//画的记录不能重复在一个位置,要往上移动一段距离,这个距离就是记录的高度
//状态(背景)
ry = ry - height * 1 / 8;
ry1 = ry1 - height * 1 / 8;
//时间记录(文字)
y = y - height * 1 / 8;
}
}
(2)在DayController中修改:
首先添加以下属性:(包括数据的模拟)
static int temp = 0;//储存recordDay(有记录的日期)有多少个
private static Gson gson;//Gson对象
static List<MonthRecordBean> monthRecordBeans;//学习记录
static List<Integer> recordDays;//获取有记录的日期集合
static String record ="[{"recordeDate":1,"recordTime":["08 : 30","12 : 30","16 : 30","18 : 30"],"recordStatus":[2,3,1,2]},{"recordeDate":6,"recordTime":["08 : 30","12 : 30","16 : 30"],"recordStatus":[3,2,1]},{"recordeDate":7,"recordTime":["08 : 30","12 : 30","16 : 30"],"recordStatus":[3,2,1]},{"recordeDate":13,"recordTime":["08 : 30","12 : 30","16 : 30"],"recordStatus":[1,2,3]},{"recordeDate":17,"recordTime":["08 : 30","12 : 30","16 : 30","18 : 30"],"recordStatus":[1,2,3,1]},{"recordeDate":20,"recordTime":["08 : 30","12 : 30"],"recordStatus":[1,2]},{"recordeDate":23,"recordTime":["08 : 30","12 : 30","16 : 30","18 : 30"],"recordStatus":[1,2,3,1]},{"recordeDate":25,"recordTime":["08 : 30","12 : 30","16 : 30","18 : 30","20 : 30"],"recordStatus":[1,2,3,1,2]},{"recordeDate":27,"recordTime":["08 : 30","12 : 30","16 : 30","18 : 30","20 : 30","22 : 30"],"recordStatus":[1,2,3,1,2,3]}]";
在生成日期对象之前获取我们模拟的学习记录:
获取学习记录的具体方法如下:
/**
* 获取学习记录数据
*/
public static void initRecordData() {
gson = new Gson();
monthRecordBeans = new ArrayList<>();
monthRecordBeans = gson.fromJson(record, new TypeToken<List<MonthRecordBean>>() {
}.getType());
recordDays = new ArrayList<>();
for (int i = 0; i < monthRecordBeans.size(); i++) {
int recordDate = monthRecordBeans.get(i).getRecordeDate();
recordDays.add(recordDate);
}
}
再到生成日期对象的方法中,设置日期显示状态之后,添加设置学习记录的方法
设置学习记录的代码如下:
/**
* 设置学习记录,当那一天有记录的时候,把学习计划的记录在当前日期里面
*/
if (recordDays.contains(i + 1)) {
KLog.d("recordDay:" + (i + 1));
KLog.d("temp: " + temp);
//学习记录的个数
mDayModel.recordNumber = monthRecordBeans.get(temp).getRecordTime().size();
KLog.d("recordNumber:" + mDayModel.recordNumber);
//学习计划开始时间的集合
mDayModel.recordTimes = monthRecordBeans.get(temp).getRecordTime();
KLog.e(mDayModel.recordTimes);
//学习计划完成状态的集合
mDayModel.recordStates = monthRecordBeans.get(temp).getRecordStatus();
KLog.e(mDayModel.recordStates);
//复位一下
if (temp >= monthRecordBeans.size() - 1) {
temp = 0;
} else {
temp++;
}
} else {
mDayModel.recordNumber = 0;
}
到此添加月学习记录就完成了,效果图如下:
(2)添加单日学习记录详情(点击跳转到另一个界面进行显示)
这个部分其实就是一个Activity的跳转+界面的显示,比较简单,也不是本Demo的重点,所以不详细赘述,跳转的代码就再弹土司的地方写就好了:
显示的话就用RecyclerView显示就可以了,如果没有用过RecyclerView的 可以参考【hyman】的:RecyclerView教学视频,很快就会了,和ListView显示效果差不多,不过用起来更方便快捷哦。
下面来看看显示具体内容有哪一些吧:
搞定了之后,本章的预期目标就算完成了,再来看看本章成果图吧:
5. 本章小结:
这是篇博文详细的记录了一个【个性化自定义日历】(英式) 的编写过程,之前也说了我会尽可能的详细的去创作,由于篇幅和知识重点分布的特点,我把它拆分成了三个章节,一点一滴记录它是如何完成的,中间会扩展一些东西,包括一些常用到的android开发技术,和我自己开发过程中遇到的问题,我都会提到,因为这样创作的话对于新手来说提升会很大,因为我自己也是新手,刚刚学习Android不久,没有太多的经验,所以大神勿喷噢O(∩_∩)O
PS:本Demo的推荐阅读顺序是:
【 ① —> ② —> ③ 】或者【 ② —> ③】,本章编号为“②”