*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
写在前面的话
其实写这个自定义view是有一点故事的,是因为他,我决定写这个view的,由于他的帮助和鼓励,我最终完成了这个view,在此,向他致敬!
ps:故事总是有剧情和结局的,这里不便多讲,送自己句话:且行且珍惜吧!
1.效果图
好了,扯了上面的这些闲话,直接来看效果图吧。
2.实现思路
- 首先是画各步骤点之间的线条
- 接着是画未选中步骤点的图标
- 第三步是画选中步骤点的图标
- 最后画出各步骤点对应的说明文字
3.实现细节
3.1概述
StepView继承自View,通过构造方法初始化一些必要参数,然后在onSizeChanged方法中获取View的宽高以及其他额外计算的数据信息,最后通过onDraw方法绘制出View。
3.2首先通过res/values/attrs定义一些细节参数
3.3通过构造方法初始化
public StepView(Context context) {
this(context, null);
}
public StepView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public StepView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr);
}
private void init(Context context, AttributeSet attrs, int defStyleAttr) {
defaultNormalLineColor = Color.parseColor("#545454");
defaultPassLineColor = Color.WHITE;
defaultTextColor = Color.WHITE;
defaultLineStikeWidth = dp2px(context, 1);
defaultTextSize = sp2px(context, 80);
defaultText2DotMargin = dp2px(context, 15);
defalutMargin = dp2px(context, 100);
defaultLine2TopMargin = dp2px(context, 30);
defaultText2BottomMargin = dp2px(context, 20);
normal_pic = BitmapFactory.decodeResource(getResources(), R.drawable.ic_normal);
target_pic = BitmapFactory.decodeResource(getResources(), R.drawable.ic_target);
passed_pic = BitmapFactory.decodeResource(getResources(), R.drawable.ic_passed);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.StepView, defStyleAttr, 0);
dotCount = a.getInt(R.styleable.StepView_count, defaultDotCount);
if (dotCount < 2) {
throw new IllegalArgumentException("Steps can't be less than 2");
}
stepNum = a.getInt(R.styleable.StepView_step, defaultStepNum);
lineLength = a.getInt(R.styleable.StepView_line_length, defaultLineLength);
maxDotCount = a.getInt(R.styleable.StepView_max_dot_count, defaultMaxDotCount);
if (maxDotCount < dotCount) {//当最多点小于设置点数量时,设置线条长度可变
lineLength = defaultLineLength;
}
textLocation = a.getInt(R.styleable.StepView_text_location, defaultTextLocation);
isTextBelowLine = textLocation == defaultTextLocation;
normalLineColor = a.getColor(R.styleable.StepView_normal_line_color, defaultNormalLineColor);
passLineColor = a.getColor(R.styleable.StepView_passed_line_color, defaultPassLineColor);
lineStikeWidth = a.getDimension(R.styleable.StepView_line_stroke_width, defaultLineStikeWidth);
textColor = a.getColor(R.styleable.StepView_text_color, defaultTextColor);
textSize = a.getDimension(R.styleable.StepView_text_size, defaultTextSize);
text2LineMargin = a.getDimension(R.styleable.StepView_text_to_line_margin, defaultText2DotMargin);
margin = (int) a.getDimension(R.styleable.StepView_margin, defalutMargin);
line2TopMargin = a.getDimension(R.styleable.StepView_line_to_top_margin, defaultLine2TopMargin);
text2BottomMargin = a.getDimension(R.styleable.StepView_text_to_bottom_margin, defaultText2BottomMargin);
clickable = a.getBoolean(R.styleable.StepView_is_view_clickable, defaultViewClickable);
a.recycle();
//当文字在线条上面时,参数倒置
if (!isTextBelowLine) {
line2BottomMargin = line2TopMargin;
text2TopMargin = text2BottomMargin;
}
//线条画笔
linePaint = new Paint();
linePaint.setAntiAlias(true);
linePaint.setStrokeWidth(lineStikeWidth);
//文字画笔
textPaint = new Paint();
textPaint.setAntiAlias(true);
textPaint.setColor(textColor);
textPaint.setTextSize(textSize);
//存放说明文字的矩形
bounds = new Rect();
}
由这段代码可知,通过init方法,不但初始化了上面的细节参数,还额外初始化了bitmap、paint、bounds等参数。由于调用了dp/sp2px方法,所以先贴一下该方法。
private int dp2px(Context context, int value) {
float density = context.getResources().getDisplayMetrics().density;
return (int) (density * value + 0.5f);
}
private int sp2px(Context context, int value) {
float scaledDensity = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (value / scaledDensity + 0.5f);
}
3.4在onSizeChanged方法中,完成宽高等数据计算。
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
width = w - margin * 2;
height = h;
//线条长度是否可变
if (lineLength == defaultLineLength) {//可变
perLineLength = width / (dotCount - 1);
} else {//固定
perLineLength = width / (maxDotCount - 1);
}
passWH = calculateWidthAndHeight(passed_pic);
normalWH = calculateWidthAndHeight(normal_pic);
targetWH = calculateWidthAndHeight(target_pic);
}
此处说明一下,计算bitma宽高的方法,然后把宽高信息存于二维数组中
/*计算bitmap宽高*/
private int[] calculateWidthAndHeight(Bitmap bitmap) {
int[] wh = new int[2];
int width = bitmap.getWidth();
int height = bitmap.getHeight();
wh[0] = width;
wh[1] = height;
return wh;
}
3.5通过onDraw方法绘制View
3.5.1画步骤点之间连线
/*绘制链接步骤点之间的线条*/
private void drawConnectLine(Canvas canvas, int stepNum) {
float startX = 0;
float stopX = 0;
for (int i = 0; i < dotCount - 1; i++) {
/*设置线条起点X轴坐标*/
if (i == stepNum) {
startX = margin + perLineLength * i + targetWH[0] / 2;
} else if (i > stepNum) {
startX = margin + perLineLength * i + normalWH[0] / 2;
} else {
startX = margin + perLineLength * i + passWH[0] / 2;
}
/*设置线条终点X轴坐标*/
if (i + 1 == stepNum) {
stopX = margin + perLineLength * (i + 1) - targetWH[0] / 2;
} else if (i + 1 < stepNum) {
stopX = margin + perLineLength * (i + 1) - passWH[0] / 2;
} else {
stopX = margin + perLineLength * (i + 1) - normalWH[0] / 2;
}
/*当目标步骤超过i时,线条设置为已过颜色,不超过时,设置为普通颜色*/
if (stepNum > i) {
linePaint.setColor(passLineColor);
} else {
linePaint.setColor(normalLineColor);
}
if (isTextBelowLine) {
/*当文字在线条下方时,设置线条y轴的位置并绘制*/
canvas.drawLine(startX, line2TopMargin, stopX, line2TopMargin, linePaint);
} else {
canvas.drawLine(startX, height - line2BottomMargin, stopX, height - line2BottomMargin, linePaint);
}
}
}
3.5.2画普通步骤点
/*绘制一般情况下的步骤点图片*/
private void drawNormalSquar(Canvas canvas, int stepNum) {
for (int i = 0; i < dotCount; i++) {
/*在目标点状态时,普通图片不绘制,跳过,继续下一次循环*/
if (stepNum == i) {
continue;
}
if (stepNum > i) {
float left = margin + perLineLength * i - passWH[0] / 2;
float top = 0;
if (isTextBelowLine) {
top = line2TopMargin - passWH[1] / 2;
} else {
top = height - line2BottomMargin - passWH[1] / 2;
}
canvas.drawBitmap(passed_pic, left, top, null);
} else {
float left = margin + perLineLength * i - normalWH[0] / 2;
float top = 0;
if (isTextBelowLine) {
top = line2TopMargin - normalWH[1] / 2;
} else {
top = height - line2BottomMargin - normalWH[1] / 2;
}
canvas.drawBitmap(normal_pic, left, top, null);
}
}
}
3.5.3画目标步骤点
/*绘制目标步骤图片*/
private void drawTargetSquar(Canvas canvas, int i) {
float left = margin + perLineLength * i - targetWH[0] / 2;
float top = 0;
if (isTextBelowLine) {
top = line2TopMargin - targetWH[1] / 2;
} else {
top = height - line2BottomMargin - targetWH[1] / 2;
}
canvas.drawBitmap(target_pic, left, top, null);
}
3.5.4画步骤点说明文字
/*绘制各步骤说明文字*/
private void drawDescText(Canvas canvas) {
for (int i = 0; i < dotCount; i++) {
String text = texts[i];
textPaint.getTextBounds(text, 0, text.length(), bounds);
int textWidth = bounds.width();
int textHeight = bounds.height();
float x = margin + perLineLength * i - textWidth / 2;
float y;
if (isTextBelowLine) {
y = height - text2BottomMargin;
} else {
y = text2TopMargin + textHeight;
}
canvas.drawText(text, x, y, textPaint);
}
}
通过上面这几个步骤就完成StepView的绘制了。
3.6根据用户设置的是否可点击,给StepView添加点击监听
这里先说明一下思路:当用户点击时,View通过touch事件监听用户点击的x/y值,然后转换为point,再通过判断point是否在几个步骤点区域范围内,返回对应的步骤点值,然后重新绘制View。
3.6.1下面看onTouchEvent方法:
@Override
public boolean onTouchEvent(MotionEvent event) {
if (clickable) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Point point = new Point();
point.x = (int) event.getX();
point.y = (int) event.getY();
int stepInDots = getStepInDots(point);
if (stepInDots != -1) {
stepNum = stepInDots;
invalidate();
}
break;
default:
break;
}
}
return super.onTouchEvent(event);
}
3.6.2获取用户点击点在某个步骤点值:
/*获取手指触摸点为第几个步骤点,异常时返回-1*/
private int getStepInDots(Point point) {
for (int i = 0; i < dotCount; i++) {
Rect rect = getSetpSquarRects()[i];
int x = point.x;
int y = point.y;
if (rect.contains(x, y)) {
return i;
}
}
return -1;
}
3.6.3获取各步骤点矩阵所在区域:
/*获取所有步骤点的矩阵区域*/
private Rect[] getSetpSquarRects() {
Rect[] rects = new Rect[dotCount];
int left, top, right, bottom;
for (int i = 0; i < dotCount; i++) {
/*此处默认所有点的区域范围为被选中图片的区域范围*/
Rect rect = new Rect();
left = margin + perLineLength * i - targetWH[0] / 2;
right = margin + perLineLength * i + targetWH[0] / 2;
if (isTextBelowLine) {
top = (int) (line2TopMargin - targetWH[1] / 2);
bottom = (int) (line2TopMargin + targetWH[1] / 2);
} else {
top = (int) (height - line2BottomMargin - targetWH[1] / 2);
bottom = (int) (height - line2BottomMargin + targetWH[1] / 2);
}
rect.set(left, top, right, bottom);
rects[i] = rect;
}
return rects;
}
至此,StepView的点击事件也完成了。
3.7设置外部调用接口
/*给外部调用接口,设置步骤总数*/
public void setDotCount(int count) {
if (count < 2) {
throw new IllegalArgumentException("dot count can't be less than 2.");
}
dotCount = count;
}
/*给外部调用接口,设置说明文字信息*/
public void setDescription(String[] descs) {
if (descs == null || descs.length < dotCount) {
throw new IllegalArgumentException("Descriptions can't be null or its length must maore than dot count");
}
texts = descs;
}
/*给外部调用接口,设置该view是否可点击*/
public void setClickable(boolean clickable) {
this.clickable = clickable;
}
/*给外部调用接口,设置步骤*/
public void setStep(Step step) {
switch (step) {
case ONE:
stepNum = 0;
break;
case TWO:
stepNum = 1;
break;
case THREE:
stepNum = 2;
break;
case FOUR:
stepNum = 3;
break;
case FIVE:
stepNum = 4;
break;
default:
break;
}
invalidate();
}
/*此处默认最多为5个步骤*/
public enum Step {
ONE, TWO, THREE, FOUR, FIVE
}
通过设置这几个接口,可以很方便的动态设置StepView。
4.部分细节详解
- 详解1
画线条时,由于目标步骤点比普通的大,因此在计算线条长度时应计算目标步骤点两端线条长度,避免线条长度画错,影响美观。
/*设置线条起点X轴坐标*/
if (i == stepNum) {
startX = margin + perLineLength * i + targetWH[0] / 2;
} else if (i > stepNum) {
startX = margin + perLineLength * i + normalWH[0] / 2;
} else {
startX = margin + perLineLength * i + passWH[0] / 2;
}
/*设置线条终点X轴坐标*/
if (i + 1 == stepNum) {
stopX = margin + perLineLength * (i + 1) - targetWH[0] / 2;
} else if (i + 1 < stepNum) {
stopX = margin + perLineLength * (i + 1) - passWH[0] / 2;
} else {
stopX = margin + perLineLength * (i + 1) - normalWH[0] / 2;
}
- 详解2
线条长度是否可变(见git view1/view2/view3/view4/view5),当设置线条长度固定时,线条的长度由view_width/(max_dot-1)决定;当设置线条长度不固定时(view6),由图可知,view6的长度与view5完整的长度一致。 - 详解3
文字是否在线条下面,默认是。当文字在线条上面的时候,这里采取数据倒置设置,即把设置给view之前的线条距顶部、文字距底部的距离分别设置给了线条距底部、文字距顶部的距离。见如下代码:
//当文字在线条上面时,参数倒置
if (!isTextBelowLine) {
line2BottomMargin = line2TopMargin;
text2TopMargin = text2BottomMargin;
}
- 详解4
获取各步骤点的矩形区域,首先是分别对各步骤点区域的左上右下进行计算,然后设置给各步骤点矩形。
/*获取所有步骤点的矩阵区域*/
private Rect[] getSetpSquarRects() {
Rect[] rects = new Rect[dotCount];
int left, top, right, bottom;
for (int i = 0; i < dotCount; i++) {
/*此处默认所有点的区域范围为被选中图片的区域范围*/
Rect rect = new Rect();
left = margin + perLineLength * i - targetWH[0] / 2;
right = margin + perLineLength * i + targetWH[0] / 2;
if (isTextBelowLine) {
top = (int) (line2TopMargin - targetWH[1] / 2);
bottom = (int) (line2TopMargin + targetWH[1] / 2);
} else {
top = (int) (height - line2BottomMargin - targetWH[1] / 2);
bottom = (int) (height - line2BottomMargin + targetWH[1] / 2);
}
rect.set(left, top, right, bottom);
rects[i] = rect;
}
return rects;
}
5.调用
- xml调用
- 代码调用
private StepView step1, step2, step3, step4, step5, step6;
private CheckBox click1, click2, click3, click4, click5, click6;
private String[] texts = {"确认身份信息", "确认入住信息", "选择房型", "支付押金", "完成入住"};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
step1 = (StepView) findViewById(R.id.step1);
step2 = (StepView) findViewById(R.id.step2);
step3 = (StepView) findViewById(R.id.step3);
step4 = (StepView) findViewById(R.id.step4);
step5 = (StepView) findViewById(R.id.step5);
step6 = (StepView) findViewById(R.id.step6);
click1 = (CheckBox) findViewById(R.id.click1);
click2 = (CheckBox) findViewById(R.id.click2);
click3 = (CheckBox) findViewById(R.id.click3);
click4 = (CheckBox) findViewById(R.id.click4);
click5 = (CheckBox) findViewById(R.id.click5);
click6 = (CheckBox) findViewById(R.id.click6);
step1.setDescription(texts);
step2.setDescription(texts);
step3.setDescription(texts);
step4.setDescription(texts);
step5.setDescription(texts);
step6.setDescription(texts);
step1.setStep(StepView.Step.ONE);
step2.setStep(StepView.Step.TWO);
step3.setStep(StepView.Step.THREE);
step4.setStep(StepView.Step.FOUR);
step5.setStep(StepView.Step.FIVE);
step6.setStep(StepView.Step.FOUR);
checkChaged(click1, step1);
checkChaged(click2, step2);
checkChaged(click3, step3);
checkChaged(click4, step4);
checkChaged(click5, step5);
checkChaged(click6, step6);
}
private void checkChaged(CheckBox check, final StepView step) {
check.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
step.setClickable(isChecked);
}
});
}
6.代码托管地址
StepView