前言
这篇文章没有什么可看性,主要是源码注释太多,推荐自己看源码,更容易理解些,在这里主要介绍,其运作流程,贴代码片段。
自定义View要重写三个方法:onMeasure,onLayout,onDraw,这三个方法各有个的作用,onMeasure是对组件的宽高进行测量,onLayout是对子控件的位置进行摆放,onDraw是对自定义控件进行绘制,已经对onMeasure,onLayout方法进行了运用,那个源码注释也很多,如果有兴趣的可以去看看,本章是对onDraw方法进行使用,顺带使用Path对象。
好了,先谈谈为什么我要重复造轮子,要做一个有签到功能的日历,由于自己对自定义的组件ondraw方法还没怎么用过,所以重复造轮子咯,是不是理由不是很充分,没关系,开心就好。
先来张效果图
这个CalendarView的API
String clickLeftMonth(); //上一个月 return String(年-月)
String clickRightMonth(); //下一个月 return String(年-月)
Surface getSurface(); //获取整个组件画图对象,可进行设置字体颜色等 return Surface
String getYearAndmonth(); // 获得当前应该显示的年月 return String(年-月)
boolean isSelectMore(); //返回是否多选
setSelectMore(boolean flag);//设置是否多选
setFlagData(String[] flags);//设置要进行标记的数据
setOnItemClickListener(OnItemClickListener); //点击一个日期的回调事件
setWritingFlag(String str); //设置标记字符,默认为签到
OK,先来简述下这个组件跑起来的流程,
1.初始化数据。
2.测量组件大小,即调用了OnMeasure方法
3.调用onDraw方法。
步骤是不是很简单呀?OK,通过源码简单的跑一下流程。
初始化数据
public CalendarView(Context context) {
super(context);
// 初始化数据
init();
}
public CalendarView(Context context, AttributeSet attrs) {
super(context, attrs);
// 初始化数据
init();
}
/**
* 初始化数据 ,初始化事件对象 ,初始化日期格式类对象 ,Surface布局对象初始化 ,获取屏幕密度比例 ,设置View背景 ,设置触摸事件
*/
private void init() {
// 创建一个Date对象并将引用给显示的月,选择开始,选择结束,今天的日期
curDate = selectedStartDate = selectedEndDate = today = new Date();
// 获取一个日期类对象
calendar = Calendar.getInstance();
// 设置日期
calendar.setTime(curDate);
// 创建一个布局路径
surface = new Surface(this);
// 获取屏幕密度比例
surface.density = getResources().getDisplayMetrics().density;
// 给整个控件设置触摸事件
setOnTouchListener(this);
}
这一块看出,在组件进行实例化的时候调用了init方法,然后看见了new Surface() 创建了一个Surface对象。ok来看下这个Surface类,其他的应该都知道是什么。(像我注释这么密的看不懂才怪(*^__^*))。
public void init() {
float temp = height / 7f;// 将整个视图分成了7份,每份所占的高度
monthHeight = 0;// (float) ((temp + temp * 0.3f) * 0.6);
weekHeight = (float) ((temp + temp * 0.3f) * 0.5);
cellHeight = (height - monthHeight - weekHeight) / 6f;
cellWidth = width / 7f;
// 创建一个边框的画笔并设置其属性
borderPaint = new Paint();
borderPaint.setColor(cellBorderColor);
borderPaint.setStyle(Paint.Style.STROKE);
// 边框的宽度
borderWidth = (float) (0.5 * density);
borderWidth = borderWidth < 1 ? 1 : borderWidth;
borderPaint.setStrokeWidth(borderWidth);
// 创建星期画笔并设置其属性
weekPaint = new Paint();
weekPaint.setColor(textWeekColor);
weekPaint.setAntiAlias(true);
float weekTextSize = weekHeight * 0.6f;
weekPaint.setTextSize(weekTextSize);
weekPaint.setTypeface(Typeface.DEFAULT_BOLD);
// 创建时间画笔并设置其属性
datePaint = new Paint();
datePaint.setAntiAlias(true);
float cellTextSize = cellHeight * 0.3f;
datePaint.setTextSize(cellTextSize);
datePaint.setTypeface(Typeface.DEFAULT_BOLD);
// 创建一个Path对象用于记录画笔所画的路径
boxPath = new Path();
// 画第一行,现在起点是0,0
boxPath.rLineTo(width, 0);
// 将起点向下移动一个星期格子的高度
boxPath.moveTo(0, monthHeight + weekHeight);
// 画第二行
boxPath.rLineTo(width, 0);
// 循环画纵线和号数的横线
for (int i = 1; i < 7; i++) {
// 纵线
boxPath.moveTo(i * cellWidth, monthHeight);
boxPath.rLineTo(0, height - monthHeight);
// 横线
boxPath.moveTo(0, monthHeight + weekHeight + i * cellHeight);
boxPath.rLineTo(width, 0);
}
// 表格被选择后使用的画笔
cellBgPaint = new Paint();
cellBgPaint.setAntiAlias(true);
cellBgPaint.setStyle(Paint.Style.FILL);
cellBgPaint.setColor(cellSelectBgColor);
}
其实这个类也没做什么,就一个init方法就是初始化各种画笔,然后动态计算各种高度和宽度。这里面的那个for循环里面的boxPath就是通过path对象记录绘制的表格路径。
ok回到CalendarView类,这个组件被实例化了,就开始进行调用onMeasure方法了。这方法里面没啥可说的就是测量这个组件的大小,确定这个组件需要的宽高是多少如果
onMeasure和onLayout会被执行两次,然后才执行onDraw方法,看下这个onDraw方法。
首先调用了这个calculateDate方法。这个方法是动态计算日期的。
/**
* 计算日期,计算出上月,这月下月的日期装入到一个数组里面进行保存
*/
private void calculateDate() {
calendar.setTime(curDate);
calendar.set(Calendar.DAY_OF_MONTH, 1);
int dayInWeek = calendar.get(Calendar.DAY_OF_WEEK);
Log.d(TAG, "day in week:" + dayInWeek);
int monthStart = dayInWeek;
monthStart -= 1; // 以日为开头-1,以星期一为开头-2
curStartIndex = monthStart;
date[monthStart] = 1;
// last month
if (monthStart > 0) {
calendar.set(Calendar.DAY_OF_MONTH, 0);
int dayInmonth = calendar.get(Calendar.DAY_OF_MONTH);
for (int i = monthStart - 1; i >= 0; i--) {
date[i] = dayInmonth;
dayInmonth--;
}
calendar.set(Calendar.DAY_OF_MONTH, date[0]);
}
showFirstDate = calendar.getTime();
// this month
calendar.setTime(curDate);
calendar.add(Calendar.MONTH, 1);
calendar.set(Calendar.DAY_OF_MONTH, 0);
int monthDay = calendar.get(Calendar.DAY_OF_MONTH);
for (int i = 1; i < monthDay; i++) {
date[monthStart + i] = i + 1;
}
curEndIndex = monthStart + monthDay;
// next month
for (int i = monthStart + monthDay; i < 42; i++) {
date[i] = i - (monthStart + monthDay) + 1;
}
if (curEndIndex < 42) {
// 显示了下一月的
calendar.add(Calendar.DAY_OF_MONTH, 1);
}
calendar.set(Calendar.DAY_OF_MONTH, date[41]);
showLastDate = calendar.getTime();
}
这个方法动态计算日期,显示计算上个月所剩下的日期装入数组date里面,然后装当前月份的,最后装下个月开头部分日期。
为什么会在这个onDraw方法里面调用呢,因为如果在构造方法里面执行一次就没法执行了,如果我点击下一个月那数据就不变了,onMeasure和onLayout都执行两遍所以不行。因此只能在onDraw方法绘制一次,计算一下。
往下看,这段代码是绘制星期天的。
// 画用于分隔显示号数的表格框
canvas.drawPath(surface.boxPath, surface.borderPaint);
// 星期计算
float weekTextY = surface.monthHeight + surface.weekHeight * 3 / 4f;
// 绘制星期1.2.3等字体
for (int i = 0; i < surface.weekText.length; i++) {
float weekTextX = i
* surface.cellWidth
+ (surface.cellWidth - surface.weekPaint
.measureText(surface.weekText[i])) / 2f;
canvas.drawText(surface.weekText[i], weekTextX, weekTextY,
surface.weekPaint);
}
动态计算星期1-7的位置然后在所处位置绘制文字。
再下面就是绘制选择格子的背景颜色,默认是当前月的当前号数。
/**
* @param canvas
*/
private void drawDownOrSelectedBg(Canvas canvas) {
// down and not up
if (downDate != null) {
drawCellBg(canvas, downIndex, surface.cellDownBgColor);
}
// selected bg color
if (!selectedEndDate.before(showFirstDate)
&& !selectedStartDate.after(showLastDate)) {
int[] section = new int[]{-1, -1};
calendar.setTime(curDate);
calendar.add(Calendar.MONTH, -1);
findSelectedIndex(0, curStartIndex, calendar, section);
if (section[1] == -1) {
calendar.setTime(curDate);
findSelectedIndex(curStartIndex, curEndIndex, calendar, section);
}
if (section[1] == -1) {
calendar.setTime(curDate);
calendar.add(Calendar.MONTH, 1);
findSelectedIndex(curEndIndex, 42, calendar, section);
}
if (section[0] == -1) {
section[0] = 0;
}
if (section[1] == -1) {
section[1] = 41;
}
for (int i = section[0]; i <= section[1]; i++) {
drawCellBg(canvas, i, surface.cellSelectBgColor);
}
}
}
后面就是开始绘制日期,即将画出来的表格填充数字。
for (int i = 0; i < num; i++) {
// 这个月的字体颜色
int color = surface.textInstantColor;
if (isLastMonth(i)) {
// 上个月字体颜色
color = surface.textOtherColor;
} else if (isNextMonth(i)) {
// 下个月字体颜色
color = surface.textOtherColor;
} else if (todayIndex != -1) {
// 循环为签到的日期加标记
int flagLen = flagData == null ? 0 : flagData.length;
for (int j = 0; j < flagLen; j++) {
if ((date[i] + "").equals(flagData[j]))
drawCellFlag(canvas, i, surface.textFlagBgColor,
surface.textFlagColor);
}
// 如果todayIndex不等于-1且等于今天
if (i == todayIndex) {
// 今天字体颜色
color = surface.textTodayColor;
}
}
drawCellText(canvas, i, date[i] + "", color);
}
在这值得一提的就是这个添加签到标签的方法drawCellFlag。
/**
* 在格子的右上角进行绘制标签
*
* @param canvas 画布
* @param index 下标
* @param bgcolor 背景颜色
* @param textcolor 字体颜色
*/
private void drawCellFlag(Canvas canvas, int index, int bgcolor,
int textcolor) {
int x = getXByIndex(index);
int y = getYByIndex(index);
// 计算一个方格子的上下左右距离组件边框的距离,以此来推出其坐标
float left = surface.cellWidth * (x - 1) + surface.borderWidth;
float top = surface.monthHeight + surface.weekHeight + (y - 1)
* surface.cellHeight - surface.borderWidth;
float right = left + surface.cellWidth + surface.borderWidth;
float botton = top + surface.cellHeight - surface.borderWidth;
surface.cellBgPaint.setColor(bgcolor);
// 通过Path来记录路径,画一个梯形图
Path path = new Path();
path.moveTo(right - surface.cellWidth * 2 / 3, top);
path.lineTo(right - surface.cellWidth / 4, top);
path.lineTo(right, botton - surface.cellHeight * 3 / 4);
path.lineTo(right, botton - surface.cellHeight / 3);
canvas.drawPath(path, surface.cellBgPaint);
// 因为下面的绘制的文字将要进行旋转因此我将以上Canvas绘制的图案进行保存,这样就不会被旋转给影响到了
canvas.save();
// 将字体进行旋转40度,以文字开始绘制的坐标点进行旋转
canvas.rotate((float) 45, right - surface.cellWidth * 3 / 7, botton
- surface.cellHeight * 5 / 6);
surface.cellBgPaint.setColor(textcolor);
// 动态的计算字体大小
float a = surface.cellWidth / 4;
float b = surface.cellHeight / 4;
float c = (float) Math.sqrt(a * a + b * b);
surface.cellBgPaint.setTextSize(c * 3 / 5);
surface.cellBgPaint.setTypeface(Typeface.DEFAULT_BOLD);
// 绘制文字
canvas.drawText(writingFlag, right - surface.cellWidth * 3 / 7, botton
- surface.cellHeight * 5 / 6, surface.cellBgPaint);
// 释放旋转状态,恢复sava时的状态
canvas.restore();
}
这个方法里面能计算出每个表格的left,right,top,botton的位置,即就可以动态计算梯形四个点,这四个点就是
A(right - surface.cellWidth * 2 / 3, top)
B(right - surface.cellWidth / 4, top)
C(right, botton - surface.cellHeight * 3 / 4)
D(right, botton - surface.cellHeight / 3)
通过Path对象记录这四个点串起来的路径然后canvas绘制就ok了。
而这个标签“签到”的位置也是这样给算出来的。
ok,大概流程讲完了。详细的可以去看源码,里面注释多多,你一定能看懂的。(*^__^*)。