我尽量不打错别字,用词准确,不造成阅读障碍
本文介绍一个自定义进度流程展示的view,注重易用性和适应性,使用简单且不需要做过多知识点准备,好上手。
效果如下:
根据指定的步骤数可自行绘制,每个步骤的图标可以不一样。
一.自定义属性
<!--流程进度2-->
<declare-styleable name="AuditProgressViewTwo">
<!--所有步骤数,从1开始,不是0,比如5-->
<attr name="all_count" format="integer" />
<!--现在完成了几个步骤,从1开始,不是0,比如4-->
<attr name="current_position" format="integer" />
<!--完成步骤的文字颜色,比如粉红-->
<attr name="complete_color" format="reference|color" />
<!--未完成步骤的文字颜色,比如灰色-->
<attr name="not_complete_color" format="reference|color" />
<!--连线颜色,完成步骤中的连线颜色-->
<attr name="line_complete_color" format="reference|color" />
</declare-styleable>
注释详细,看看就明白,关于连线颜色是指到达状态下的连线颜色,未到达状态下的连线颜色是“未完成步骤的文字颜色”,效果图里面因为都是灰色,所以看不出来,应该改成蓝色好了,可图都截完了,懒~!
二.需求分析
到代码实施部分了,考虑需要什么;我希望使用者只引入一个控件就好了,所以最好把所有数据和资源都给我,我在一个控件里给你处理,这样就打算让使用者给我三个数组资源——1:"未达到"状态下的图标样式数组,2:"达到"状态下的图标样式数组,3:步骤文字描述数组。在Activity里给我就好,像这样:
public class AuditProgressActivity extends AppCompatActivity {
//未完成状态下的图标样式
private int notCompleteImageArray[] = {R.drawable.audit_uncomplete, R.drawable.audit_uncomplete, R.drawable.audit_uncomplete, R.drawable.audit_uncomplete, R.drawable.audit_uncomplete};
//完成状态下的图标样式
private int completeImageArray[] = {R.drawable.audit_complete, R.drawable.audit_complete, R.drawable.audit_complete, R.drawable.audit_complete, R.drawable.audit_complete};
//文字数组
private String text[] = {"一帆风顺", "一帆风顺", "风顺", "帆风顺", "风顺"};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_audit_progress);
AuditProgressViewTwo auditProgressViewTwo = findViewById(R.id.apvt_my);
//三个set设置资源
auditProgressViewTwo.setCompleteImageSource(completeImageArray);
auditProgressViewTwo.setNotCompleteImageSource(notCompleteImageArray);
auditProgressViewTwo.setTextSource(text);
auditProgressViewTwo.build();
}
}
通过三个set来设置资源。
然后是工具分析,要画图片,需要一个画笔。画文字,需要一个画笔。画连接线,需要一个画笔。确定了要画的步骤个数之后,在一个for循环里处理就好。
三.重要代码实施
刚开始的代码是这样的:
public class AuditProgressViewTwo extends View {
private int allCount; //所有进度数目,比如5个
private int currentPosition; //当前进度所在位置,比如在第4个进度
private int lineCompleteColor;
private TextPaint textPaint; //写文字的画笔
private Paint bitmapPaint; //画图片的画笔
private Paint linePaint; //画线的画笔
private int completeColor = Color.parseColor("#FF0000"); //完成颜色,主要用于文字
private int notCompleteColor = Color.parseColor("#757575"); //未完成颜色,主要用于文字
private ArrayList<Bitmap> completeBitmap = new ArrayList<>(); //完成的Bitmap
private ArrayList<Bitmap> notCompleteBitmap = new ArrayList<>(); //未完成的Bitmap
private int notCompleteImageArray[] = {R.drawable.audit_uncomplete, R.drawable.audit_uncomplete, R.drawable.audit_uncomplete, R.drawable.audit_uncomplete, R.drawable.audit_uncomplete};
private int completeImageArray[] = {R.drawable.audit_complete, R.drawable.audit_complete, R.drawable.audit_complete, R.drawable.audit_complete, R.drawable.audit_complete};
private String text[] = {"步骤一", "步骤二", "步骤三", "步骤四", "步骤五"};
public AuditProgressViewTwo(Context context) {
this(context, null);
}
public AuditProgressViewTwo(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public AuditProgressViewTwo(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.AuditProgressViewTwo);
allCount = typedArray.getInteger(R.styleable.AuditProgressViewTwo_all_count, 5);
currentPosition = typedArray.getInteger(R.styleable.AuditProgressViewTwo_current_position, 4);
completeColor = typedArray.getInteger(R.styleable.AuditProgressViewTwo_complete_color, Color.BLACK);
notCompleteColor = typedArray.getInteger(R.styleable.AuditProgressViewTwo_not_complete_color, Color.GRAY);
lineCompleteColor = typedArray.getInteger(R.styleable.AuditProgressViewTwo_line_complete_color, Color.BLACK);
typedArray.recycle();
if (allCount > completeImageArray.length) {
throw new RuntimeException(getClass().getName() + "资源数组图标个数不能小于全部流程个数");
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//设置画笔
textPaint = new TextPaint();
textPaint.setStyle(Paint.Style.FILL);
textPaint.setAntiAlias(true);
textPaint.setColor(notCompleteColor);
textPaint.setTextSize(30);
//设置画笔
bitmapPaint = new Paint();
bitmapPaint.setStyle(Paint.Style.FILL);
bitmapPaint.setAntiAlias(true);
//设置画笔
linePaint = new Paint();
linePaint.setColor(completeColor);
linePaint.setAntiAlias(true);
linePaint.setStyle(Paint.Style.FILL);
linePaint.setStrokeWidth(2.0f);
//单独的list放数据,主要方便后期操作
completeBitmap.clear();
notCompleteBitmap.clear();
for (int i = 0; i < completeImageArray.length; i++) {
completeBitmap.add(i, BitmapFactory.decodeResource(getResources(), completeImageArray[i]));
}
for (int i = 0; i < notCompleteImageArray.length; i++) {
notCompleteBitmap.add(i, BitmapFactory.decodeResource(getResources(), notCompleteImageArray[i]));
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < allCount; i++) {
if (i < currentPosition) {
int bitmapWidthHalf = completeBitmap.get(i).getWidth() / 2;
canvas.drawBitmap(completeBitmap.get(i), i * 2 * 80 - bitmapWidthHalf, 0, bitmapPaint);
linePaint.setColor(lineCompleteColor);
textPaint.setColor(completeColor);
} else {
canvas.drawBitmap(notCompleteBitmap.get(i), i * 2 * 80, 0, bitmapPaint);
linePaint.setColor(notCompleteColor);
textPaint.setColor(notCompleteColor);
}
if (i > 0) {
float startX = i * notCompleteBitmap.get(i-1).getWidth()+(i-1)*160;
float stopX = i * (notCompleteBitmap.get(i-1).getWidth()+160);
float startY = notCompleteBitmap.get(i).getHeight() / 2;
canvas.drawLine(startX, startY, stopX, startY, linePaint);
}
//写文字部分
canvas.drawText(text[i], i * 2 * 80, completeBitmap.get(i).getHeight()+70, textPaint);
}
}
/**
* 设置完成进度的图片数组
*
* @param complete 数据源
*/
public void setCompleteImageSource(int[] complete) {
this.completeImageArray = complete;
}
/**
* 设置未完成进度的图片数组
*
* @param notComplete 数据源
*/
public void setNotCompleteImageSource(int[] notComplete) {
this.notCompleteImageArray = notComplete;
}
/**
* 设置文字描述数组
*
* @param textSource 数据源
*/
public void setTextSource(String[] textSource) {
this.text = textSource;
}
刚开始没想着一步到位,先实现主要效果,那个"i * 2 * 80"是我随便写的边距,160就是2*80嘛,效果很不好:在xml文件中引人:
<com.study.longl.myselfviewdemo.AuditProgress.AuditProgressViewTwo
android:id="@+id/apvt_my"
android:layout_width="match_parent"
android:layout_height="100dp"
android:paddingTop="20dp"
app:all_count="5"
app:complete_color="@color/colorAccent"
app:current_position="4"
app:line_complete_color="@color/white"
app:not_complete_color="@color/gray" />
效果如下:
效果不好,但是无非就是计算的位置有问题,起码图片画的都对,没出什么问题。可是看起来边距也不对,图片应该在文字中间,如果文字比图片宽,是不是图片距边缘就应该有一个距离?反而文字可以顶在边缘。所以干脆我就把整个控件的宽按照步骤个数均分几等分,然后将图片个文字都水平居中,再设置一下两者垂直间距就OK了。
就像这样:
改良后如下:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//设置控件可使用范围的矩形,加入适配padding属性的效果
viewRectF = new RectF(getPaddingLeft(), getPaddingTop(), getRight() - getLeft() - getPaddingRight(), getBottom() - getTop() - getPaddingBottom());
//...省略老代码
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < allCount; i++) {
float smallRectWidth = viewRectF.width() / allCount; //均分
if (i < currentPosition) {
int bitmapWidthHalf = completeBitmap.get(i).getWidth() / 2;
canvas.drawBitmap(completeBitmap.get(i), (i * smallRectWidth + smallRectWidth / 2) - bitmapWidthHalf, getPaddingTop(), bitmapPaint);
linePaint.setColor(lineCompleteColor);
textPaint.setColor(completeColor);
if (i < allCount - 1) {
//画线,之所以写在这里是为了防止每个item图标大小不一样的情况
float startX = (i * smallRectWidth + smallRectWidth / 2) + bitmapWidthHalf;
float stopX = ((i + 1) * smallRectWidth + smallRectWidth / 2) - bitmapWidthHalf;
float startY = completeBitmap.get(i).getHeight() / 2 + getPaddingTop();
canvas.drawLine(startX, startY, stopX, startY, linePaint);
}
} else {
int bitmapWidthHalf = notCompleteBitmap.get(i).getWidth() / 2;
canvas.drawBitmap(notCompleteBitmap.get(i), (i * smallRectWidth + smallRectWidth / 2) - bitmapWidthHalf, getPaddingTop(), bitmapPaint);
linePaint.setColor(notCompleteColor);
textPaint.setColor(notCompleteColor);
if (i < allCount - 1) {
//画线,之所以写在这里是为了防止每个item图标大小不一样的情况
float startX = (i * smallRectWidth + smallRectWidth / 2) + bitmapWidthHalf;
float stopX = ((i + 1) * smallRectWidth + smallRectWidth / 2) - bitmapWidthHalf;
float startY = notCompleteBitmap.get(i).getHeight() / 2 + getPaddingTop();
canvas.drawLine(startX, startY, stopX, startY, linePaint);
}
}
//写文字部分
textPaint.getTextBounds(text[i], 0, text[i].length(), textRect);
int textWidthHalf = textRect.width() / 2;
float x = (i * smallRectWidth + smallRectWidth / 2) - textWidthHalf;
float y = completeBitmap.get(i).getHeight() + getPaddingTop() + 70;
canvas.drawText(text[i], x, y, textPaint);
}
}
上面的写法已经是接近最终版了,兼容了padding并对齐了文字与图片的纵向排列,运行结果与开篇的效果图片一样:
最后简化一下代码,去掉默认数据加上判空处理并增加动态改变当前步骤的能力,完整代码如下:
public class AuditProgressViewTwo extends View {
private int allCount; //所有进度数目,比如5个
private int currentPosition; //当前进度所在位置,比如在第3个进度
private int lineCompleteColor;
private TextPaint textPaint; //写文字的画笔
private Paint bitmapPaint; //画图片的画笔
private Paint linePaint; //画线的画笔
private Rect textRect; //承接文字宽度的矩形
private RectF viewRectF; //整个view的矩形
private int completeColor = Color.parseColor("#FF0000"); //完成颜色,主要用于文字
private int notCompleteColor = Color.parseColor("#757575"); //未完成颜色,主要用于文字
private ArrayList<Bitmap> completeBitmap = new ArrayList<>(); //完成的Bitmap
private ArrayList<Bitmap> notCompleteBitmap = new ArrayList<>(); //未完成的Bitmap
private int notCompleteImageArray[];
private int completeImageArray[];
private String text[];
public AuditProgressViewTwo(Context context) {
this(context, null);
}
public AuditProgressViewTwo(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public AuditProgressViewTwo(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.AuditProgressViewTwo);
allCount = typedArray.getInteger(R.styleable.AuditProgressViewTwo_all_count, 5);
currentPosition = typedArray.getInteger(R.styleable.AuditProgressViewTwo_current_position, 4);
completeColor = typedArray.getInteger(R.styleable.AuditProgressViewTwo_complete_color, Color.BLACK);
notCompleteColor = typedArray.getInteger(R.styleable.AuditProgressViewTwo_not_complete_color, Color.GRAY);
lineCompleteColor = typedArray.getInteger(R.styleable.AuditProgressViewTwo_line_complete_color, Color.BLACK);
typedArray.recycle();
if (completeImageArray != null) {
if (allCount > completeImageArray.length) {
throw new RuntimeException(getClass().getName() + "资源数组图标个数不能小于全部流程个数");
}
}
textRect = new Rect();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
viewRectF = new RectF(getPaddingLeft(), getPaddingTop(), getRight() - getLeft() - getPaddingRight(), getBottom() - getTop() - getPaddingBottom());
textPaint = new TextPaint();
textPaint.setStyle(Paint.Style.FILL);
textPaint.setAntiAlias(true);
textPaint.setColor(notCompleteColor);
textPaint.setTextSize(30);
bitmapPaint = new Paint();
bitmapPaint.setStyle(Paint.Style.FILL);
bitmapPaint.setAntiAlias(true);
linePaint = new Paint();
linePaint.setColor(completeColor);
linePaint.setAntiAlias(true);
linePaint.setStyle(Paint.Style.FILL);
linePaint.setStrokeWidth(2.0f);
completeBitmap.clear();
notCompleteBitmap.clear();
if (completeImageArray != null && notCompleteImageArray != null) {
for (int i = 0; i < completeImageArray.length; i++) {
completeBitmap.add(i, BitmapFactory.decodeResource(getResources(), completeImageArray[i]));
}
for (int i = 0; i < notCompleteImageArray.length; i++) {
notCompleteBitmap.add(i, BitmapFactory.decodeResource(getResources(), notCompleteImageArray[i]));
}
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (completeImageArray != null && notCompleteImageArray != null) {
for (int i = 0; i < allCount; i++) {
float smallRectWidth = viewRectF.width() / allCount;
int bitmapWidthHalf;
float startY = 10f;
if (i < currentPosition) {
bitmapWidthHalf = completeBitmap.get(i).getWidth() / 2;
canvas.drawBitmap(completeBitmap.get(i), (i * smallRectWidth + smallRectWidth / 2) - bitmapWidthHalf, getPaddingTop(), bitmapPaint);
linePaint.setColor(lineCompleteColor);
textPaint.setColor(completeColor);
if (i < allCount - 1) {
startY = notCompleteBitmap.get(i).getHeight() / 2 + getPaddingTop();
}
} else {
bitmapWidthHalf = notCompleteBitmap.get(i).getWidth() / 2;
canvas.drawBitmap(notCompleteBitmap.get(i), (i * smallRectWidth + smallRectWidth / 2) - bitmapWidthHalf, getPaddingTop(), bitmapPaint);
linePaint.setColor(notCompleteColor);
textPaint.setColor(notCompleteColor);
if (i < allCount - 1) {
startY = notCompleteBitmap.get(i).getHeight() / 2 + getPaddingTop();
}
}
if (i < allCount - 1) {
float startX = (i * smallRectWidth + smallRectWidth / 2) + bitmapWidthHalf;
float stopX = ((i + 1) * smallRectWidth + smallRectWidth / 2) - bitmapWidthHalf;
canvas.drawLine(startX, startY, stopX, startY, linePaint);
}
//写文字部分
textPaint.getTextBounds(text[i], 0, text[i].length(), textRect);
int textWidthHalf = textRect.width() / 2;
float x = (i * smallRectWidth + smallRectWidth / 2) - textWidthHalf;
float y = completeBitmap.get(i).getHeight() + getPaddingTop() + 70;
canvas.drawText(text[i], x, y, textPaint);
}
}
}
/**
* 设置完成进度的图片数组
*
* @param complete 数据源
*/
public void setCompleteImageSource(int[] complete) {
this.completeImageArray = complete;
}
/**
* 设置未完成进度的图片数组
*
* @param notComplete 数据源
*/
public void setNotCompleteImageSource(int[] notComplete) {
this.notCompleteImageArray = notComplete;
}
/**
* 设置文字描述数组
*
* @param textSource 数据源
*/
public void setTextSource(String[] textSource) {
this.text = textSource;
}
public void setAllCount(int allCount) {
this.allCount = allCount;
}
public void setCurrentPosition(int currentPosition) {
this.currentPosition = currentPosition;
invalidate();
}
//build启动刷新
public void build() {
invalidate();
}
}
ps:
1.其实这个真的很简单了,简单到本不打算写的,就是距离计算的问题,可是适合初学者学习。
2.开动脑筋,还可以改成垂直方向的,原理一样。
3.这里注意数据源与xml文件中的allCount属性和currentPosition属性要对上,否则很可能会崩,我也想过根据数据源个数进行for循环,这样就不需要在xml设置allCount属性了,但是考虑使用场景多样,比如虽然有五个数据,但是实际我就想显示一个,这种需求也许有呢,所以还是交给使用者设置比较好,你随意设置。