最近公司有一个需求是关于签到方面的效果,想着自己对自定义View这块不是很熟悉,所以就想着自己动手来实现下,以此来学习下自定义View。
首先来一张自己实现的效果图
![](https://img.haomeiwen.com/i14983899/a91339908a5b434d.jpg)
GitHub
引用
implementation 'com.lishang:checkInProgress:1.0.1'
属性 | 类型 | 描述 |
---|---|---|
text_date_size | sp | 日期文字大小 |
text_date_color | color | 日期文字颜色 |
radius | dp | 签到圆半径 |
circle_color | color | 圆的背景色 |
line_height | dp | 线高 |
line_color | color | 线的颜色 |
text_score_size | sp | 签到积分字体大小 |
text_score_color | color | 签到积分文字颜色 |
check_in_bitmap | drawble | 签到后的图片 |
check_in_color | color | 没有签到图片时,签到的颜色 |
check_in_hook_color | color | 没有签到图片时,签到内部勾的颜色 |
check_in_hook_size | dp | 没有签到图片时,签到内部勾的大小 |
circle_margin | dp | 签到圆顶部与日期字体距离 |
circle_stroke_width | dp | 签到圆描边宽度 |
circle_stroke_color | color | 签到圆边描颜色 |
check_in_progress_show | boolean | 是否显示签到进度 |
check_in_progress_color | color | 签到进度颜色 |
check_in_leak_show | boolean | 是否支持补签 |
circle_style | enum | 签到圆样式 fill 填充 stroke描边(circle_stroke_width、circle_stroke_color生效) |
align | enum | 位置 top/center/bottom |
使用
<com.lishang.checkin.CheckInProgress
android:id="@+id/checkIn_1"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"
android:background="#408ce2"
app:align="center"
app:check_in_color="#ceebfd"
app:check_in_hook_color="#2d66d9"
app:check_in_leak_show="true"
app:circle_color="#2d66d9"
app:circle_margin="5dp"
app:circle_stroke_color="#ceebfd"
app:circle_stroke_width="1dp"
app:circle_style="stroke"
app:line_color="#2d66d9"
app:line_height="1dp"
app:radius="10dp"
app:text_date_color="#edffff"
app:text_date_size="12sp"
app:text_score_color="#9bccff"
app:text_score_size="12sp" />
checkIn.setAdapter(new CheckInProgress.Adapter() {
/**
* 日期
* @param position
* @return
*/
@Override
public String getDateText(int position) {
CheckIn in = list.get(position);
return in.date;
}
/**
* 积分
* @param position
* @return
*/
@Override
public String getScoreText(int position) {
CheckIn in = list.get(position);
return in.score;
}
/**
* 是否签到
* @param position
* @return
*/
@Override
public boolean isCheckIn(int position) {
CheckIn in = list.get(position);
return in.isCheckIn;
}
/**
* 数量
* @return
*/
@Override
public int size() {
return list.size();
}
/**
* 是否支持补签
* @param position
* @return
*/
@Override
public boolean isLeakCheckIn(int position) {
CheckIn in = list.get(position);
return in.isLeakChekIn;
}
});
checkIn.setOnClickCheckInListener(new
OnClickCheckInListener() {
@Override
public void OnClick(int position) {
CheckIn checkIn = list1.get(position);
if (checkIn.isCheckIn) {
Toast.makeText(getApplicationContext(), "已签到", Toast.LENGTH_SHORT).show();
} else {
if (checkIn.isLeakChekIn) {
Toast.makeText(getApplicationContext(), "补卡", Toast.LENGTH_SHORT).show();
checkIn.isLeakChekIn = false;
checkIn.isCheckIn = true;
Log.e("CheckIn", Arrays.toString(list1.toArray()));
checkIn.getAdapter().notifyDataSetChanged();
} else {
Toast.makeText(getApplicationContext(), "签到", Toast.LENGTH_SHORT).show();
}
}
}
});
代码简要概括
自定义View,主要需要实现两个方法:
onMeasure(int widthMeasureSpec, int heightMeasureSpec)
主要用来测量当前控件的宽高widthMeasureSpec和heightMeasureSpec这两个值通常情况下都是由父视图经过计算后传递给子视图的,说明父视图会在一定程度上决定子视图的大小。
认识 MeasureSpec
在测量自定义view的大小之前,我们需要认识一个类MeasureSpec,它封装了父布局传递给子布局的布局要求,每个MeasureSpec代表了一组宽度和高度的要求 MeasureSpec由size和mode组成。
specMode一共有三种类型,如下所示:
1. EXACTLY
表示父视图希望子视图的大小应该是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小,简单的说(当设置width或height为match_parent时,模式为EXACTLY,因为子view会占据剩余容器的空间,所以它大小是确定的)
2. AT_MOST
表示子视图最多只能是specSize中指定的大小。(当设置为wrap_content时,模式为AT_MOST, 表示子view的大小最多是多少,这样子view会根据这个上限来设置自己的尺寸)
3. UNSPECIFIED
表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到。
onDraw
用来绘制View需要显示的内容
下面来看代码
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//计算元素位置
onCalculation();
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
//当View的高是wrap_content 时,高度设置为实际测量的高度
if (heightMode == MeasureSpec.AT_MOST && verticalHeight != 0) {
heightSize = verticalHeight;
}
setMeasuredDimension(widthSize, heightSize);
}
onCalculation()方法用了测量View上各个元素的位置,并保存下来
/**
* 先计算好画布上每个元素的位置
*/
private void onCalculation() {
if (adapter == null) return;
calculationDate();
calculationScore();
//元素垂直高度
int total = datePointPool.get(0).y - getPaddingTop(); //日期的高度
total += (circleMargin); // + 间距
total += (radius) * 2; //+积分圆的直径
verticalHeight = total;
}
/**
* 日期元素位置
*/
private void calculationDate() {
int left = getPaddingLeft();
int right = getPaddingRight();
int top = getPaddingTop();
int width = getMeasuredWidth() - left - right;
int margin = width / (adapter.size());
//日期位置
int cy = 0;
for (int i = 0; i < adapter.size(); i++) {
String str = adapter.getDateText(i);
Rect rect = new Rect();
datePaint.getTextBounds(str, 0, str.length(), rect);
int y = top + rect.height();
if (cy < y) {
cy = y;
}
}
for (int i = 0; i < adapter.size(); i++) {
int cx = left + margin / 2 + i * margin;
Point point = new Point(cx, cy);
datePointPool.put(i, point);
}
}
/**
* 积分元素位置
*/
private void calculationScore() {
int radiusPx = (radius);
int left = datePointPool.get(0).x;
int right = datePointPool.get(datePointPool.size() - 1).x;
int top = datePointPool.get(0).y;
int width = right - left;
int cy = top + radiusPx + (circleMargin);
int margin = width / (adapter.size() - 1);
for (int i = 0; i < adapter.size(); i++) {
int cx = left + i * margin;
Point p = new Point(cx, cy);
circlePointPool.put(i, p);
scorePaint.setTextSize((textScoreSize));
scorePaint.setStyle(Paint.Style.FILL);
scorePaint.setColor(textScoreColor);
scorePaint.setTextAlign(Paint.Align.CENTER);
String str = "+" + adapter.getScoreText(i);
if (adapter.isLeakCheckIn(i) && checkInLeakShow) {
str = "补";
}
Rect rect = new Rect();
scorePaint.getTextBounds(str, 0, str.length(), rect);
Paint.FontMetricsInt fontMetrics = scorePaint.getFontMetricsInt();
Point point = new Point(p.x, p.y + (fontMetrics.descent - fontMetrics.ascent) / 2 - fontMetrics.descent);
scorePointPool.put(i, point);
}
}
onDraw(Canvas canvas) 进行View内部元素绘制
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawDate(canvas);
drawBgLine(canvas);
drawScore(canvas);
}
/**
* 画日期
*
* @param canvas
*/
private void drawDate(Canvas canvas) {
int margin = calculationAlign();
if (datePointPool.size() != 0) {
for (int i = 0; i < adapter.size(); i++) {
String str = adapter.getDateText(i);
datePaint.setColor(textDateColor);
Point point = datePointPool.get(i);
canvas.drawText(str, point.x, point.y + margin, datePaint);
}
}
}
private void drawBgLine(Canvas canvas) {
int margin = calculationAlign();
int startX = datePointPool.get(0).x;
int startY = datePointPool.get(0).y + (radius) + (circleMargin) + margin;
int stopX = datePointPool.get(datePointPool.size() - 1).x;
int stopY = startY;
canvas.drawLine(startX, startY, stopX, stopY, linePaint);
}
private void drawScore(Canvas canvas) {
int radiusPx = (radius);
int margin = calculationAlign();
for (int i = 0; i < adapter.size(); i++) {
Point p = circlePointPool.get(i);
if (adapter.isCheckIn(i)) {
if (checkInProgressShow && i + 1 < adapter.size()) {
//进度
scorePaint.setStyle(Paint.Style.FILL);
scorePaint.setColor(checkInProgressColor);
scorePaint.setStrokeWidth((lineHeight));
Point p1 = circlePointPool.get(i + 1);
canvas.drawLine(p.x, p.y + margin, p1.x, p1.y + margin, scorePaint);
}
if (checkIn != null) {
float scale = radiusPx * 2.0f / checkIn.getWidth();
Matrix matrix = new Matrix();
matrix.postScale(scale, scale);
canvas.save();
canvas.translate(p.x - radiusPx, p.y + margin - radiusPx);
canvas.drawBitmap(checkIn, matrix, scorePaint);
canvas.restore();
} else {
scorePaint.setColor(checkInColor);
scorePaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(p.x, p.y + margin, radiusPx, scorePaint);
//画勾
scorePaint.setStyle(Paint.Style.FILL);
scorePaint.setColor(checkInHookColor);
scorePaint.setStrokeWidth((checkInHookSize));
int startX = p.x - radiusPx / 4 * 3;
int startY = p.y + margin;
int stopX = p.x - radiusPx / 4;
int stopY = p.y + margin + radiusPx / 2;
canvas.drawLine(startX, startY, stopX, stopY, scorePaint);
startX = stopX;
startY = stopY;
stopX = p.x + radiusPx / 4 * 3;
stopY = p.y + margin - radiusPx / 2;
canvas.drawLine(startX, startY, stopX, stopY, scorePaint);
canvas.drawCircle(startX, startY, checkInHookSize / 2.0f, scorePaint);
}
} else {
scorePaint.setStyle(Paint.Style.FILL);
scorePaint.setColor(circleColor);
canvas.drawCircle(p.x, p.y + margin, radiusPx, scorePaint);
if (circleStyle == Paint.Style.STROKE) {
scorePaint.setColor(circleStrokeColor);
scorePaint.setStrokeWidth((circleStrokeWidth));
scorePaint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(p.x, p.y + margin, radiusPx, scorePaint);
}
scorePaint.setTextSize((textScoreSize));
scorePaint.setStyle(Paint.Style.FILL);
scorePaint.setColor(textScoreColor);
scorePaint.setTextAlign(Paint.Align.CENTER);
String str = "+" + adapter.getScoreText(i);
if (adapter.isLeakCheckIn(i) && checkInLeakShow) {
str = "补";
}
Point point = scorePointPool.get(i);
canvas.drawText(str, point.x, point.y + margin, scorePaint);
}
}
}
网友评论