这篇文章没有什么可看性,主要是源码注释太多,推荐自己看源码,更容易理解些,在这里主要介绍,其运作流程,贴代码片段。
先贴源码地址:点此进入源码地址。
自定义View要重写三个方法:onMeasure,onLayout,onDraw,这三个方法各有个的作用,onMeasure是对组件的宽高进行测量,onLayout是对子控件的位置进行摆放,onDraw是对自定义控件进行绘制,在《仿探探头像编辑解析》这篇文章中,已经对onMeasure,onLayout方法进行了运用,那个源码注释也很多,如果有兴趣的可以去看看,本章是对onDraw方法进行使用,顺带使用Path对象。
好了,先谈谈为什么我要重复造轮子,要做一个有签到功能的日历,由于自己对自定义的组件ondraw方法还没怎么用过,所以重复造轮子咯,是不是理由不是很充分,没关系,开心就好。
先来张效果图
这个CalendarView的API
String clickLeftMonth();
String clickRightMonth();
Surface getSurface();
String getYearAndmonth();
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() {
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;
monthHeight = 0;
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);
boxPath = new Path();
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;
curStartIndex = monthStart;
date[monthStart] = 1;
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();
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;
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;
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) {
if (downDate != null) {
drawCellBg(canvas, downIndex, surface.cellDownBgColor);
}
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);="" }="" }<="" code=""/>
后面就是开始绘制日期,即将画出来的表格填充数字。
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);
}
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 = 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.save();
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);
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,大概流程讲完了。详细的可以去看源码,里面注释多多,你一定能看懂的。(*^__^*)。