为了增强对自定义 View 的认知以及开发能力,我计划这段时间陆续来完成几个难度从易到难的自定义 View,并简单的写几篇博客来进行介绍,全部的代码也都会开源,也但愿读者能给个 star 哈 GitHub 地址:github.com/leavesC/Cus… 也能够下载 Apk 来体验下:www.pgyer.com/CustomViewjava
先看下效果图:git
这是一个相似于课程表的自定义 View,横向和纵向均是以时间做为计量单位,经过设置当前计划处于哪一个星期数下以及跨度时间,在该范围内绘制出相应的背景以及文本github
PlanBean 中有两个比较重要字段,一个是该计划的绘制范围,即坐标系 rectF,另外一个字段 isEllipsis 是用于标记当前计划的文本是否以省略的形式出现canvas
public class PlanBean {
private String planId;
private String planName;
private String planStartTime;
private String planEndTime;
private String color;
private int dayIndex;
//计划的坐标
private RectF rectF;
//文本是否被省略
private boolean isEllipsis;
}
复制代码
边框以及时间文本的绘制比较简单,只须要计算出各个起始点和终点的坐标系便可bash
@Override
protected void onDraw(Canvas canvas) {
//先画背景
bgPaint.setStyle(Paint.Style.FILL);
bgPaint.setColor(Color.WHITE);
canvas.drawRect(0, 0, width, realHeight, bgPaint);
//画左边和上边的边框
bgPaint.setColor(rectColor);
bgPaint.setStyle(Paint.Style.FILL);
canvas.drawRect(0, 0, leftTimeWidth, height, bgPaint);
canvas.drawRect(leftTimeWidth, 0, width, headerHeight, bgPaint);
//画线
canvas.save();
canvas.translate(leftTimeWidth, 0);
bgPaint.setColor(lineColor);
bgPaint.setStrokeWidth(getResources().getDisplayMetrics().density);
for (int i = 0; i < 7; i++) {
canvas.drawLine(itemWidth * i, 0, itemWidth * i, height, bgPaint);
}
canvas.translate(0, headerHeight);
for (int i = 0; i < 20; i++) {
canvas.drawLine(0, i * itemHeight, width - leftTimeWidth + 2, i * itemHeight, bgPaint);
}
canvas.restore();
//画星期数
canvas.save();
canvas.translate(leftTimeWidth, 0);
bgPaint.setTextSize(sp2px(DAY_TEXT_SIZE));
bgPaint.setColor(Color.BLACK);
bgPaint.setTextAlign(Paint.Align.CENTER);
for (String day : DAYS) {
bgPaint.getTextBounds(day, 0, day.length(), textBounds);
float offSet = (textBounds.top + textBounds.bottom) >> 1;
canvas.drawText(day, itemWidth / 2, headerHeight / 2 - offSet, bgPaint);
canvas.translate(itemWidth, 0);
}
canvas.restore();
//画时间
for (int i = 0; i < TIMES.length; i++) {
String time = TIMES[i];
bgPaint.getTextBounds(time, 0, time.length(), textBounds);
float offSet = (textBounds.top + textBounds.bottom) >> 1;
canvas.drawText(time, leftTimeWidth / 2, headerHeight + itemHeight * i - offSet, bgPaint);
}
···
}
复制代码
难点在于须要判断计划名的文本高度是否超出了其自己的高度,若是超出了则截断文本,并用省略号结尾 。但是 canvas.drawText()
方法自己是没法获取到文本高度以及自动换行的,此时就须要用到 StaticLayout
了,能够设置最大文本宽度,其内部实现了文本自动换行的功能,而且能够获取到文本换行后的总体高度,经过这个就能够完成想要的效果ide
if (planListBeanList != null && planListBeanList.size() > 0) {
for (PlanBean bean : planListBeanList) {
bgPaint.setColor(Color.parseColor(bean.getColor()));
measurePlanBound(bean, planRectF);
canvas.drawRect(planRectF, bgPaint);
String planName = bean.getPlanName();
if (TextUtils.isEmpty(planName)) {
continue;
}
float planItemHeight = planRectF.bottom - planRectF.top;
StaticLayout staticLayout = null;
for (int length = planName.length(); length > 0; length--) {
staticLayout = new StaticLayout(planName, planTextPaint, (int) (itemWidth - 4 * getResources().getDisplayMetrics().density),
Layout.Alignment.ALIGN_CENTER, 1.1f, 1.1f, true);
if (staticLayout.getHeight() > planItemHeight) {
planName = planName.substring(0, length) + "...";
bean.setEllipsis(true);
}
}
if (staticLayout == null) {
staticLayout = new StaticLayout(planName, planTextPaint, (int) (itemWidth - 4 * getResources().getDisplayMetrics().density),
Layout.Alignment.ALIGN_CENTER, 1.1f, 1.1f, true);
}
if (staticLayout.getHeight() > planItemHeight) {
continue;
}
canvas.save();
canvas.translate(planRectF.left + (itemWidth - staticLayout.getWidth()) / 2, planRectF.top + (planItemHeight - staticLayout.getHeight()) / 2);
staticLayout.draw(canvas);
canvas.restore();
}
}
复制代码
因为 StaticLayout
并无向外提供设置总体最大高度的 API ,因此须要本身来循环判断文本的总体高度是否已经超出最大高度,是的话则对文本进行截取。若是最大高度过小,没法容纳一行文本,则直接不绘制文本ui
for (int length = planName.length(); length > 0; length--) {
staticLayout = new StaticLayout(planName, planTextPaint, (int) (itemWidth - 4 * getResources().getDisplayMetrics().density),
Layout.Alignment.ALIGN_CENTER, 1.1f, 1.1f, true);
if (staticLayout.getHeight() > planItemHeight) {
planName = planName.substring(0, length) + "...";
bean.setEllipsis(true);
}
}
if (staticLayout == null) {
staticLayout = new StaticLayout(planName, planTextPaint, (int) (itemWidth - 4 * getResources().getDisplayMetrics().density),
Layout.Alignment.ALIGN_CENTER, 1.1f, 1.1f, true);
}
if (staticLayout.getHeight() > planItemHeight) {
continue;
}
复制代码
另一个比较重要的点是须要经过计划的时间跨度来计算其坐标系,并将坐标系存储下来,方便判断点击事件spa
private void measurePlanBound(PlanBean bean, RectF rect) {
measurePlanBound(bean.getDayIndex(), bean.getPlanStartTime(), bean.getPlanEndTime(), rect);
RectF rectF = new RectF(rect);
bean.setRectF(rectF);
}
private void measurePlanBound(int day, String startTime, String endTime, RectF rect) {
try {
float left = leftTimeWidth + itemWidth * (day - 1);
float right = left + itemWidth;
String[] split = startTime.split(":");
int startHour = Integer.parseInt(split[0]);
int startMinute = Integer.parseInt(split[1]);
float top = ((startHour - START_TIME) * 60 + startMinute) * singleMinuteHeight + headerHeight;
split = endTime.split(":");
int endHour = Integer.parseInt(split[0]);
int endMinute = Integer.parseInt(split[1]);
float bottom = ((endHour - START_TIME) * 60 + endMinute) * singleMinuteHeight + headerHeight;
float offset = 1;
rect.set(left + offset, top + offset, right - offset, bottom - offset);
} catch (Exception e) {
e.printStackTrace();
}
}
复制代码