无意间打开Github,发现自己的一个自定义控件项目竟然神奇的被Star了,真的是相当惊喜,毕竟这是自己从事代码工作以来收获的第一个Star,于是才有了以下这篇博客。花点时间将代码整理了一下,也配上了一张效果图,粗糙地完成了以下这篇博客,作为一个笔记进行总结整理,同时提供了一个分享途径。好了,话不多说,先上效果图:
以下是主要构造
public DashBoardProgressView(Context context, AttributeSet attrs) {
super(context, attrs);
initPaint();
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
dashBoardView = new DashBoardView(context, attrs);
addView(dashBoardView, layoutParams);
pointView = new PointView(context, attrs);
addView(pointView, layoutParams);
scoreTextView = new ScoreTextView(context, attrs);
addView(scoreTextView, layoutParams);
}
通过以上构造方法,可以看到该控件主要由三个View组成,分别命名为:
- DashBoardView(外边框和信用等级显示,刷新频率最低,只在值满足条件的时候刷新)
- PointView(进度显示的点,一直在刷新)
- ScoreTextView(信用分数,一直在刷新)
由于刷新频率不同,同时根据具体绘制的内容,认为分为三个部分还是较合理的,如果放在一个View中进行onDraw()绘制,会导致大量不必要的部分重复绘制。接下来拆分为四个部分进行讲解,包含以上三个View和一个对外提供的刷新的方法。
1.PointView(容易理解的View)
说其最容易理解是因为它仅仅是一个小圆点围着表盘的中心点不断转动到预设的值,实现原理就是不断改变绘制角度后调用onDraw()方法刷新,见关键代码:
private class PointView extends View {
public PointView(Context context) {
// super(context);
this(context, null);
}
public PointView(Context context, AttributeSet attrs) {
super(context, attrs);
/*
* <p>Causes the Runnable to be added to the message queue.
* The runnable will be run on the user interface thread.</p>
* */
post(new Runnable() {
@Override
public void run() {
initPaint();
}
});
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//转移画布使绘制的内容在所给区域的中央
if (306 / 576F > height / width) { //给出的大小可能是不和我们的控件匹配的
canvas.translate(Math.abs(width - reguSizeX) / 2, 0);
} else {
canvas.translate(0, Math.abs(height - reguSizeY) / 2);
}
//绘制圆点
canvas.save();
canvas.translate(288 / 576f * reguSizeX, 288 / 306f * reguSizeY);
canvas.rotate(rotation);
canvas.drawCircle(0, -278 / 576f * reguSizeX, 6 / 306f * reguSizeY, paint);
canvas.restore();
}
}
以上代码中有两个需要注意的地方:
1.由于要适配大小,所以有些操作要放在onMeasure之后,但是又必须在绘制之前,使用
post(new Runnable() {
@Override
public void run() {
initPaint();
}
});
方法能够解决这个问题,在绘制圆点的时候还不能很好的体现这个方法的作用,后面绘制文字的时候,由于文字大小也要做适配,所以必须先获得文字大小再进行绘制,所以将绘制文字的画笔初始化放在了post中,以设置适配了整体布局大小之后的文字大小(有点描述不清,希望能够理解……)
2.刷新点的位置的时候先将画布中心转移到半圆的中间,然后不断旋转画布,再绘制圆,达到小圆点不断移动的效果
2.ScoreTextView(最容易理解的View)
这个view的代码见最后的总代码,只有一个简单的绘制文字到画布中间,用来显示分数,唯一的难点在上面的PointView中已经做了解释,使用post在绘制之前获取布局大小以用来适配并设置要设置的文字大小,同时简单的使用了一个canvas.clipRect()方法来防止绘制脏布局(经过测试好像并没有什么效果)
3.DashBoardView(相对复杂的View)
相对复杂指的是该View相对于前面分析的两个View来说要复杂一些,其实控件本身并不复杂,只是要绘制的内容多一点,包括线性边框,分段边框,断点组成的边框和相关文字部分,其中相对复杂的就是那个由点组成的边框了,其实也是使用了旋转画布的原理,旋转画布来简化具有一定规则的图形绘制在这篇博客中有详细的例子:
个人认为还有一个比较坑的地方就是根据原型图来测量文字所在的地方,将文字绘制到固定的位置,该部分少不了要进行位置的适配,这些细节工作才是比较花时间的部分。给View对应的具体代码在文章后面给出。
该自定义控件已经写成了一个小项目放在了github上,地址:
4.刷新方法:refreshScore(int refreshToScore)
刷新方法的代码如下:
/**
* 模拟刷新小圆点的位置
*/
public void refreshScore(final int refreshToScore) {
score = 500;
rotation = -90;
new Thread(new Runnable() {
@Override
public void run() {
try {
while (score < refreshToScore) {
Thread.sleep(50);
rotation += 1;
score += 2.222;
if (score >= refreshToScore) { //该判断非常重要,防止每次加2.22超出边界
score = refreshToScore;
}
pointView.postInvalidate();
scoreTextView.postInvalidate();
double tempScore = score + 0.5;
if (tempScore < 600) {
scoreLever = 0;
dashBoardView.postInvalidate();
} else if (tempScore >= 600 && tempScore <= 603) {
scoreLever = 1;
dashBoardView.postInvalidate();
} else if (tempScore >= 700 && tempScore <= 703) {
scoreLever = 2;
dashBoardView.postInvalidate();
} else if (tempScore >= 800 && tempScore <= 803) {
scoreLever = 3;
dashBoardView.postInvalidate();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
我们在方法中开启了一个子线程不断使用postInvalidate()方法已达到小圆点移动和分数刷新的效果,通过循环 while (score < refreshToScore) 来控制刷新的时机,当分数大于或者等于设置的分数的时候停止刷新,这其中有一个需要注意的地方,分数和角度是对应的,不是1:1的比例增加的,比如我这个地方需要用到的就是1:2.22(根据实际需求),如下:
rotation += 1;
score += 2.222;
也就是圆点每旋转一个角度,要增加两分,所以可能会出现如果设置的分数是700,每次加2.22刚好最后一次加到了702.22(只是举例,不一定是这个)才停止的情况,所以我们添加了以下代码进行约束,这也是在代码中注明特别重要的原因,不然会导致结果不准确。
rotation += 1;
score += 2.222;
if (score >= refreshToScore) { //该判断非常重要,防止每次加2.22超出边界
score = refreshToScore;
}
以下是所有代码:
DashBoardProgressView.java
package com.shixia.dashboardprogressview.view;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
/**
* Created by AmosShi on 2016/10/27.
* <p>
* Description:仿支付宝信用分表盘控件,自动更新进度,已做好屏幕适配工作,见Demo
* <p>
* Email:shixiuwen1991@yeah.net
* <p>
* 分析:
* 3个层级:
* 1.无需更新的边框刻度以及分数文字刻度等
* 2.随刻度移动而变化的文字部分
* 3.移动更新的刻度点
*/
public class DashBoardProgressView extends FrameLayout {
private Paint paint;
private Paint textPaint38;
private Paint textPaint20;
private Paint textPaint60;
private RectF rectF;
private RectF rectF2;
private float rotation = -90;
private double score = 500;
private int scoreLever = 0;
private PointView pointView;
private ScoreTextView scoreTextView;
private DashBoardView dashBoardView;
private float reguSizeX = 0; //以此为参考缩放控件以得到合适大小
private float reguSizeY = 0;
private float width;
private float height;
public DashBoardProgressView(Context context) {
// super(context);
this(context, null);
}
public DashBoardProgressView(Context context, AttributeSet attrs) {
super(context, attrs);
initPaint();
Log.i("amos", "init");
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
dashBoardView = new DashBoardView(context, attrs);
addView(dashBoardView, layoutParams);
pointView = new PointView(context, attrs);
addView(pointView, layoutParams);
scoreTextView = new ScoreTextView(context, attrs);
addView(scoreTextView, layoutParams);
}
private void initPaint() {
paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.WHITE);
paint.setStrokeCap(Paint.Cap.ROUND);
}
private void initTextPaint38() {
textPaint38 = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
textPaint38.setStyle(Paint.Style.STROKE);
textPaint38.setColor(Color.WHITE);
textPaint38.setStrokeCap(Paint.Cap.ROUND);
textPaint38.setTextSize(38 / 306f * reguSizeY);
}
private void initTextPaint20() {
textPaint20 = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
textPaint20.setStyle(Paint.Style.STROKE);
textPaint20.setColor(Color.WHITE);
textPaint20.setStrokeCap(Paint.Cap.ROUND);
textPaint20.setTextSize(20 / 306f * reguSizeY);
}
private void initTextPaint60() {
textPaint60 = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
textPaint60.setStyle(Paint.Style.STROKE);
textPaint60.setColor(Color.WHITE);
textPaint60.setStrokeCap(Paint.Cap.ROUND);
textPaint60.setTextSize(60 / 306f * reguSizeY);
}
private void initRectF() {
Rect rect = new Rect((int) (10 / 576f * reguSizeX), (int) (10 / 306f * reguSizeY), (int) (566 / 576f * reguSizeX), (int) (566 / 306f * reguSizeY));
rectF = new RectF(rect);
Rect rect2 = new Rect((int) (42 / 576f * reguSizeX), (int) (42 / 306f * reguSizeY), (int) (534 / 576f * reguSizeX), (int) (534 / 306f * reguSizeY));
rectF2 = new RectF(rect2);
}
/**
* 模拟刷新小圆点的位置
*/
public void refreshScore(final int refreshToScore) {
score = 500;
rotation = -90;
new Thread(new Runnable() {
@Override
public void run() {
try {
while (score < refreshToScore) {
Thread.sleep(50);
rotation += 1;
score += 2.222;
if (score >= refreshToScore) { //该判断非常重要,防止每次加2.22超出边界
score = refreshToScore;
}
pointView.postInvalidate();
scoreTextView.postInvalidate();
double tempScore = score + 0.5;
if (tempScore < 600) {
scoreLever = 0;
dashBoardView.postInvalidate();
} else if (tempScore >= 600 && tempScore <= 603) {
scoreLever = 1;
dashBoardView.postInvalidate();
} else if (tempScore >= 700 && tempScore <= 703) {
scoreLever = 2;
dashBoardView.postInvalidate();
} else if (tempScore >= 800 && tempScore <= 803) {
scoreLever = 3;
dashBoardView.postInvalidate();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
private class DashBoardView extends View {
public DashBoardView(Context context) {
// super(context);
this(context, null);
}
public DashBoardView(Context context, AttributeSet attrs) {
super(context, attrs);
/*
* <p>Causes the Runnable to be added to the message queue.
* The runnable will be run on the user interface thread.</p>
* 使用了post后,run()中的代码会提前加载到message queue,提前于onDraw()方法的
* 执行,以初始化一些数据,有些数据数据是onMeasure()方法中返回的,不这么做的话无法
* 计算比例以适配大小
* */
post(new Runnable() {
@Override
public void run() {
initPaint();
initTextPaint38();
initTextPaint20();
initRectF();
}
});
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//转移画布使绘制的内容在所给区域的中央
if (306 / 576F > height / width) { //给出的大小可能是不和我们的控件匹配的
canvas.translate(Math.abs(width - reguSizeX) / 2, 0);
} else {
canvas.translate(0, Math.abs(height - reguSizeY) / 2);
}
//绘制文字
canvas.save();
canvas.translate(288 / 576f * reguSizeX, 288 / 306f * reguSizeY);
canvas.drawText("700", -15 / 576f * reguSizeX, -168 / 306f * reguSizeY, textPaint20);
canvas.restore();
canvas.save();
canvas.translate(288 / 576f * reguSizeX, 288 / 306f * reguSizeY);
canvas.rotate(45);
canvas.drawText("800", -15 / 576f * reguSizeX, -168 / 306f * reguSizeY, textPaint20);
canvas.restore();
canvas.save();
canvas.translate(288 / 576f * reguSizeX, 288 / 306f * reguSizeY);
canvas.rotate(-45);
canvas.drawText("600", -15 / 576f * reguSizeX, -168 / 306f * reguSizeY, textPaint20);
canvas.restore();
canvas.save();
canvas.translate(288 / 576f * reguSizeX, 288 / 306f * reguSizeY);
canvas.drawText("500", -184 / 576f * reguSizeX, 8 / 306f * reguSizeY, textPaint20);
canvas.restore();
canvas.save();
canvas.translate(288 / 576f * reguSizeX, 288 / 306f * reguSizeY);
canvas.drawText("900", 148 / 576f * reguSizeX, 8 / 306f * reguSizeY, textPaint20);
canvas.restore();
if (scoreLever == 0) {
canvas.drawText("信用较差", 213 / 576f * reguSizeX, 196 / 306f * reguSizeY, textPaint38);
} else if (scoreLever == 1) {
canvas.drawText("信用一般", 213 / 576f * reguSizeX, 196 / 306f * reguSizeY, textPaint38);
} else if (scoreLever == 2) {
canvas.drawText("信用较好", 213 / 576f * reguSizeX, 196 / 306f * reguSizeY, textPaint38);
} else if (scoreLever == 3) {
canvas.drawText("信用极好", 213 / 576f * reguSizeX, 196 / 306f * reguSizeY, textPaint38);
}
//绘制最外框
paint.setStrokeWidth(8 / 306f * reguSizeY);
canvas.drawArc(rectF, 175, 190, false, paint);
//绘制内边框1,2,3,4,5,6分段(带有断点的内边框)
paint.setStrokeCap(Paint.Cap.BUTT);
paint.setStrokeWidth(16 / 306f * reguSizeY);
canvas.drawArc(rectF2, 175, 4, false, paint);
canvas.drawArc(rectF2, 181, 43, false, paint);
canvas.drawArc(rectF2, 226, 43, false, paint);
canvas.drawArc(rectF2, 271, 43, false, paint);
canvas.drawArc(rectF2, 316, 43, false, paint);
canvas.drawArc(rectF2, 1, 4, false, paint);
//绘制大圆点
paint.setStrokeWidth(6 / 306f * reguSizeY);
canvas.save();
canvas.drawLine(288 / 576f * reguSizeX, 96 / 306f * reguSizeY, 288 / 576f * reguSizeX, 76 / 306f * reguSizeY, paint);
canvas.restore();
canvas.save();
canvas.translate(288 / 576f * reguSizeX, 288 / 306f * reguSizeY);
canvas.rotate(45);
canvas.drawLine(0, -192 / 306f * reguSizeY, 0, -212 / 306f * reguSizeY, paint);
canvas.restore();
canvas.save();
canvas.translate(288 / 576f * reguSizeX, 288);
canvas.rotate(90);
canvas.drawLine(0, -192 / 306f * reguSizeY, 0, -212 / 306f * reguSizeY, paint);
canvas.restore();
canvas.save();
canvas.translate(288 / 576f * reguSizeX, 288 / 306f * reguSizeY);
canvas.rotate(-45);
canvas.drawLine(0, -192 / 306f * reguSizeY, 0, -212 / 306f * reguSizeY, paint);
canvas.restore();
canvas.save();
canvas.translate(288 / 576f * reguSizeX, 288);
canvas.rotate(-90);
canvas.drawLine(0, -192 / 306f * reguSizeY, 0, -212 / 306f * reguSizeY, paint);
canvas.restore();
//绘制小圆点
paint.setStrokeCap(Paint.Cap.ROUND);
for (int i = -19; i < 20; i++) {
canvas.save();
canvas.translate(288 / 576f * reguSizeX, 288 / 306f * reguSizeY);
canvas.rotate(4.5f * i);
canvas.drawLine(0, -192 / 306f * reguSizeY, 0, -202 / 306f * reguSizeY, paint);
canvas.restore();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// setMeasuredDimension(576, 306);
width = getMeasuredSize(widthMeasureSpec, true);
height = getMeasuredSize(heightMeasureSpec, false);
setMeasuredDimension((int) width, (int) height);
if (306 / 576F > height / width) { //给出的大小可能是不和我们的控件匹配的
reguSizeX = height * 576 / 306;
reguSizeY = height;
} else {
reguSizeX = width;
reguSizeY = width * 306 / 576;
}
}
}
private class PointView extends View {
public PointView(Context context) {
// super(context);
this(context, null);
}
public PointView(Context context, AttributeSet attrs) {
super(context, attrs);
/*
* <p>Causes the Runnable to be added to the message queue.
* The runnable will be run on the user interface thread.</p>
* */
post(new Runnable() {
@Override
public void run() {
initPaint();
}
});
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//转移画布使绘制的内容在所给区域的中央
if (306 / 576F > height / width) { //给出的大小可能是不和我们的控件匹配的
canvas.translate(Math.abs(width - reguSizeX) / 2, 0);
} else {
canvas.translate(0, Math.abs(height - reguSizeY) / 2);
}
//绘制圆点
canvas.save();
canvas.translate(288 / 576f * reguSizeX, 288 / 306f * reguSizeY);
canvas.rotate(rotation);
canvas.drawCircle(0, -278 / 576f * reguSizeX, 6 / 306f * reguSizeY, paint);
canvas.restore();
}
}
private class ScoreTextView extends View {
public ScoreTextView(Context context) {
// super(context);
this(context, null);
}
public ScoreTextView(Context context, AttributeSet attrs) {
super(context, attrs);
/*
* <p>Causes the Runnable to be added to the message queue.
* The runnable will be run on the user interface thread.</p>
* */
post(new Runnable() {
@Override
public void run() {
initTextPaint60();
}
});
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//转移画布使绘制的内容在所给区域的中央
if (306 / 576F > height / width) { //给出的大小可能是不和我们的控件匹配的
canvas.translate(Math.abs(width - reguSizeX) / 2, 0);
} else {
canvas.translate(0, Math.abs(height - reguSizeY) / 2);
}
//优化,防止绘制脏布局
canvas.clipRect(232 / 576f * reguSizeX, 280 / 306f * reguSizeY - textPaint60.getTextSize()
, 232 / 576f * reguSizeX + textPaint60.measureText(String.valueOf((int) (score + 0.5))), 280 / 306f * reguSizeY);
canvas.drawText(String.valueOf((int) (score + 0.5)), 232 / 576f * reguSizeX, 280 / 306f * reguSizeY, textPaint60);
}
}
private int getMeasuredSize(int length, boolean isWidth) {
int mode = MeasureSpec.getMode(length);
int size = MeasureSpec.getSize(length);
int resSize = 0;
if (mode == MeasureSpec.EXACTLY) {
resSize = size;
} else {
if (mode == MeasureSpec.AT_MOST) {
if (isWidth) {
resSize = 576;
} else {
resSize = 306;
}
}
}
return resSize;
}
}
MainActivity.java
package com.shixia.dashboardprogressview;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import com.shixia.dashboardprogressview.view.DashBoardProgressView;
public class MainActivity extends AppCompatActivity {
private Button btnRefreshTo550;
private Button btnRefreshTo650;
private DashBoardProgressView wpbView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
wpbView = (DashBoardProgressView) findViewById(R.id.wpb_progress_view);
btnRefreshTo550 = (Button) findViewById(R.id.btn_refresh_to550);
btnRefreshTo650 = (Button) findViewById(R.id.btn_refresh_to650);
wpbView.refreshScore(800);
btnRefreshTo550.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
wpbView.refreshScore(550);
}
});
btnRefreshTo650.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
wpbView.refreshScore(650);
}
});
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main"
android:layout_width="match_parent" android:layout_height="match_parent"
tools:context="com.shixia.dashboardprogressview.MainActivity">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#dcdcdc">
<LinearLayout
android:layout_width="1000px"
android:layout_height="800px"
android:layout_centerInParent="true"
android:background="#43c0dc"
android:gravity="center">
<com.shixia.dashboardprogressview.view.DashBoardProgressView
android:layout_width="580px"
android:layout_height="300px"
android:layout_centerInParent="true"
android:id="@+id/wpb_progress_view"/>
</LinearLayout>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/btn_refresh_to550"
android:text="refresh To 550"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="100px"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/btn_refresh_to650"
android:text="refresh To 650"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="300px"/>
</RelativeLayout>
</android.support.constraint.ConstraintLayout>
PS:该自定义控件除了设置刷新分数外没有对外提供相关属性设置方法,只想分享一个解决这种问题的思路,真的要使用的话请自行修改,如有任何问题请留言或者邮箱:shixiuwen1991@yeah.net