最近公司有一個需求是關(guān)于簽到方面的效果静稻,想著自己對自定義View這塊不是很熟悉睹耐,所以就想著自己動手來實現(xiàn)下糊探,以此來學(xué)習(xí)下自定義View。
首先來一張自己實現(xiàn)的效果圖
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 | 沒有簽到圖片時断楷,簽到內(nèi)部勾的顏色 |
check_in_hook_size | dp | 沒有簽到圖片時,簽到內(nèi)部勾的大小 |
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;
}
/**
* 數(shù)量
* @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冬筒,主要需要實現(xiàn)兩個方法:
onMeasure(int widthMeasureSpec, int heightMeasureSpec)
主要用來測量當(dāng)前控件的寬高widthMeasureSpec和heightMeasureSpec這兩個值通常情況下都是由父視圖經(jīng)過計算后傳遞給子視圖的,說明父視圖會在一定程度上決定子視圖的大小茅主。
認(rèn)識 MeasureSpec
在測量自定義view的大小之前舞痰,我們需要認(rèn)識一個類MeasureSpec,它封裝了父布局傳遞給子布局的布局要求暗膜,每個MeasureSpec代表了一組寬度和高度的要求 MeasureSpec由size和mode組成匀奏。
specMode一共有三種類型,如下所示:
1. EXACTLY
表示父視圖希望子視圖的大小應(yīng)該是由specSize的值來決定的学搜,系統(tǒng)默認(rèn)會按照這個規(guī)則來設(shè)置子視圖的大小,簡單的說(當(dāng)設(shè)置width或height為match_parent時论衍,模式為EXACTLY瑞佩,因為子view會占據(jù)剩余容器的空間,所以它大小是確定的)
2. AT_MOST
表示子視圖最多只能是specSize中指定的大小坯台。(當(dāng)設(shè)置為wrap_content時炬丸,模式為AT_MOST, 表示子view的大小最多是多少,這樣子view會根據(jù)這個上限來設(shè)置自己的尺寸)
3. UNSPECIFIED
表示開發(fā)人員可以將視圖按照自己的意愿設(shè)置成任意的大小蜒蕾,沒有任何限制稠炬。這種情況比較少見,不太會用到咪啡。
onDraw
用來繪制View需要顯示的內(nèi)容
下面來看代碼
@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);
//當(dāng)View的高是wrap_content 時首启,高度設(shè)置為實際測量的高度
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內(nèi)部元素繪制
@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);
}
}
}