一,实现流程
1、自定义控件,继承View,设置控件宽度与高度
2、定义画笔,绘制完成填充圆圈和未完成空心圆圈
3、在圆圈中添加文本序列,表示完成到第几步
4、在圆圈下面绘制描述文本,通过计算让圆圈位于文本宽度的水平中间
5、在描述文本下面绘制描述文本所对应的时间
二、话不多说,效果展示如下
三、案例代码
1、在资源文件下声明自定义控件的属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="TimeLineView">
<!--小球下面的文本字体大小-->
<attr name="textSize" format="dimension"/>
<!--文本到小球间距-->
<attr name="txMarginTop" format="dimension"/>
<!--小球的半径-->
<attr name="tlradius" format="dimension"/>
<!--线条粗细-->
<attr name="lineWidth" format="dimension"/>
<!--小球底部文本颜色-->
<attr name="CompleteColor" format="color"/>
<!--完成状态小球的颜色-->
<attr name="textColor" format="color"/>
<!--未完成状态小球的颜色-->
<attr name="NoCompleteColor" format="color"/>
<!--完成状态小球内文本颜色-->
<attr name="completeTextColor" format="color"/>
<!--未完成状态小球内文本颜色-->
<attr name="noCompleteTextColor" format="color"/>
<!--小球内文本大小-->
<attr name="inRadiusTextSize" format="dimension"/>
</declare-styleable>
</resources>
2、继承View,自定义 TimeLineView类,实现代码如下
public class TimeLineView extends View {
/*
定义画笔
*/
private Paint mPaint;
/*
定义线条画笔
*/
private Paint mLinePaint;
/*
延长线专用画笔
*/
private Paint exPaint;
/*
字体画笔
*/
private Paint mTxtPaint;
/*
字体大小
*/
private float mTextSize;
/*
圆形半径
*/
private float mRadius;
/*
线的粗度
*/
private float mLineWidth;
/*
完成的颜色
*/
private int mCompleteColor;
/*
未完成的颜色
*/
private int mNoCompleteColor;
/*
当前执行到的步数 从0开始计算
*/
private float mStep = 0;
/*
* 传入的文字的list
*/
private List<String> pointStringList;
/*
* 传入的文字所对应时间的list
*/
private List<String> timeStringList;
/*
* 每个点的X坐标
*/
private Float[] pointXArray;
/*
* 文字高度
*/
private float mTextHeight;
/*
自定义的节点点击事件
*/
/*
存放圆心的列表
*/
List<CircleCenter> circleCenterList;
List<CircleCenter> mCenterXY;
/*
* 点与点之间的阶段长度
*/
float sectionLength;
/*
计算保留两位小数用的工具类
*/
BigDecimal bigDecimal;
private float mMarginTop;
private int mCompleteTextColor;
private int mNoCompleteTextColor;
private float inRadiusTextSize;
private int mTextColor;
public TimeLineView(Context context) {
this(context, null);
}
public TimeLineView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public TimeLineView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initAttr(attrs);
init();
}
/**
* 初始化自定义属性的
*
* @param attrs attrs.xml里定义的属性
*/
private void initAttr(AttributeSet attrs) {
if (attrs != null) {
TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.TimeLineView);
mTextSize = typedArray.getDimension(R.styleable.TimeLineView_textSize, 20);
mRadius = typedArray.getDimension(R.styleable.TimeLineView_tlradius, 20);
inRadiusTextSize = typedArray.getDimension(R.styleable.TimeLineView_inRadiusTextSize, 14);
mLineWidth = typedArray.getDimension(R.styleable.TimeLineView_lineWidth, 5);
mCompleteColor = typedArray.getColor(R.styleable.TimeLineView_CompleteColor, Color.GREEN);
mTextColor = typedArray.getColor(R.styleable.TimeLineView_textColor, Color.DKGRAY);
mNoCompleteColor = typedArray.getColor(R.styleable.TimeLineView_NoCompleteColor, Color.GRAY);
mCompleteTextColor = typedArray.getColor(R.styleable.TimeLineView_completeTextColor, Color.BLACK);
mNoCompleteTextColor = typedArray.getColor(R.styleable.TimeLineView_noCompleteTextColor, Color.BLACK);
mMarginTop = typedArray.getDimension(R.styleable.TimeLineView_txMarginTop, DensityUtil.dp2px(5));
//切记回收防止内存泄漏
typedArray.recycle();
}
}
/**
* 初始化画笔、字体高度、传入步数文字内容的列表以及圆心的列表
*/
private void init() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setTextSize(inRadiusTextSize);
mPaint.setStrokeWidth(mRadius/3);
mTxtPaint = new Paint();
mTxtPaint.setAntiAlias(true);
mTxtPaint.setStrokeWidth(mLineWidth);
mTxtPaint.setTextSize(mTextSize);
mTxtPaint.setColor(mTextColor);
mTxtPaint.setStyle(Paint.Style.FILL);
mTextHeight = mTxtPaint.descent() - mTxtPaint.ascent();
mLinePaint = new Paint();
mLinePaint.setAntiAlias(true);
mLinePaint.setStrokeWidth(mLineWidth);
exPaint = new Paint();
pointStringList = new ArrayList<>();
timeStringList = new ArrayList<>();
circleCenterList = new ArrayList<>();
mCenterXY = new ArrayList<>();
}
/**
* 自定义的测量方式 主要处理的是wrap_content模式下的一些默认值
* 宽度是不管如何设置都是占用的整个屏幕的宽度,这里可以自行调整
* 高度是两倍字体的高度加上圆的直径 作为默认高度,如果有需求也可以自行调整
*
* @param widthMeasureSpec 宽度尺
* @param heightMeasureSpec 高度尺
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int minimumWidth = getSuggestedMinimumWidth();
final int minimumHeight = getSuggestedMinimumHeight();
int width = measureWidth(minimumWidth, widthMeasureSpec);
int height = measureHeight(minimumHeight, heightMeasureSpec);
setMeasuredDimension(width, height);
}
/**
* 测量宽度
*
* @param defaultWidth 默认宽度
* @param measureSpec 宽度尺
* @return 计算后的宽度
*/
private int measureWidth(int defaultWidth, int measureSpec) {
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.AT_MOST:
defaultWidth = getWidth();
break;
case MeasureSpec.EXACTLY:
defaultWidth = specSize;
break;
case MeasureSpec.UNSPECIFIED:
defaultWidth = Math.max(defaultWidth, specSize);
}
return defaultWidth;
}
/**
* 高度测量
*
* @param defaultHeight 默认高度
* @param measureSpec 高度尺
* @return 计算后的高度
*/
private int measureHeight(int defaultHeight, int measureSpec) {
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.AT_MOST:
/*文本高度+小球大小+文本到小球的高度*/
defaultHeight = (int) (2 * mTextHeight + (mRadius * 2) + mMarginTop * 2) + getPaddingTop() + getPaddingBottom();
break;
case MeasureSpec.EXACTLY:
defaultHeight = specSize;
break;
case MeasureSpec.UNSPECIFIED:
defaultHeight = Math.max(defaultHeight, specSize);
break;
}
return defaultHeight;
}
/**
* 绘制思路:
* 1.确定第一步骤的圆形和文字的宽度,哪个宽就用哪个作为第一步绘制的左边距离
* 2.确定最后步骤的圆形和文字的宽度,那个宽就用哪个最为最后一个步骤绘制的右边距离
* 3.用自定义组件整体的宽度减去左边距和右边距再加上两个半径的距离,剩下的就是要平分剩下点的距离
* 4.平分剩下的距离,画圆后连线。
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//先判断传入的步数是不是大于0,如果传入一个空数据的List则不调用显示步数的方法
if (pointStringList.size() > 0) {
//先设置成完成后的颜色,因为绘制过程肯定是先绘制完成后的颜色且传入的步数至少有一步是完成的
mLinePaint.setColor(mCompleteColor);
mLinePaint.setStyle(Paint.Style.STROKE);
initViewsPos();
//当前绘制到那一步
int currentStep;
//是否绘制半截进程的线 如2.3就绘制第三步到第四步的百分之30的完成线
boolean isDrawExtrasLine = false;
//循环X轴的点进行绘制
for (int i = 0; i < mCenterXY.size(); i++) {
currentStep = i;
//当前的步骤大于设置的步骤了就需要将线和圆改成未完成色,如果要绘制未完成色就说明可能要绘制延长线,那就将绘制延长线的开关打开。
if (currentStep > mStep) {
mLinePaint.setColor(mNoCompleteColor);
isDrawExtrasLine = true;
}
//当i大于1说明有起码两个点时才有必要画线
if (i >= 1) {
drawLine(canvas, i);
}
}
//绘制延长线,主要是处理步数为类似2.3时,这百分之30的第三个圆到第四个圆之间的完成颜色的线
if (isDrawExtrasLine) {
bigDecimal = new BigDecimal(mStep);
float mStepTwoPoint = bigDecimal.setScale(2, BigDecimal.ROUND_HALF_UP).floatValue();
//取小数点后的数字
float littleCountFloat = mStepTwoPoint - (int) mStepTwoPoint;
drawExtrasLine(canvas, littleCountFloat, (int) mStep);
}
//绘制圆圈 currentStep > mStep 绘制为空心圆颜色为mNoCompleteColor 否则绘制为实心圆,颜色为mCompleteColor
for (int i = 0; i < pointXArray.length; i++) {
currentStep = i;
//当前的步骤大于设置的步骤了就需要将线和圆改成未完成色,如果要绘制未完成色就说明可能要绘制延长线,那就将绘制延长线的开关打开。
if (currentStep >= mStep) {
drawCircleFill(canvas, i);
} else {
drawCircleStroke(canvas, i);
}
}
for (int i = 0; i < pointXArray.length; i++) {
currentStep = i;
/*设置未完成字体颜色*/
/*if (currentStep > mStep) {
mTxtPaint.setColor(mNoCompleteColor);
}*/
/**绘制文本*/
drawCirlceAndText(canvas, i);
}
} else {
mPaint.setColor(Color.RED);
String noStepsWarnText = "传入步数为0,请重新传入数据";
canvas.drawText(noStepsWarnText, (getWidth() / 2) - mPaint.measureText(noStepsWarnText) / 2, getHeight() - this.mRadius - 1, mPaint);
}
}
/**
* 初始化组件的位置
* 主要是计算各个圆形的位置
* 并将其圆心位置记录到数组中去
*/
private void initViewsPos() {
mCenterXY.clear();
//传入的步数
int pointCount = pointStringList.size();
pointXArray = new Float[pointCount];
//如果文字的长度大于半径 就用文字的长度来计算 开始的点和结束的点同理
float startDistance = Math.max(mRadius, Math.max(mTxtPaint.measureText(pointStringList.get(0)) / 2, mTxtPaint.measureText(timeStringList.get(0)) / 2));
float endDistance = Math.max(mRadius, Math.max(mTxtPaint.measureText(pointStringList.get(pointStringList.size() - 1)) / 2, mTxtPaint.measureText(timeStringList.get(timeStringList.size() - 1)) / 2));
//每段线的距离 宽度减去左留白和右留白后除以点数减一即可 帮助思考的图例:*---*---*---*
sectionLength = (getWidth() - startDistance - endDistance) / (pointCount - 1);
//循环将起始点、结束点和中间的点的X的值放入数组中
//用来存放起始点的 X 值
/*绘制圆形的坐标 Y值*/
float pointY = getPaddingTop() + mRadius;
for (int i = 0; i < pointCount; i++) {
if (i == 0) {
pointXArray[i] = startDistance;
mCenterXY.add(new CircleCenter(startDistance, pointY));
} else if (i == pointCount - 1) {
pointXArray[i] = getWidth() - endDistance;
mCenterXY.add(new CircleCenter(getWidth() - endDistance, pointY));
} else {
pointXArray[i] = startDistance + sectionLength * i;
mCenterXY.add(new CircleCenter(startDistance + sectionLength * i, pointY));
}
}
}
/**
* 绘制圆形和下面的文字
*
* @param canvas 画布
* @param i x轴的值
*/
private void drawCirlceAndText(Canvas canvas, int i) {
//获取中心点的坐标
CircleCenter center = mCenterXY.get(i);
//要显示的文本值
String text = pointStringList.get(i);
if (TextUtils.isEmpty(text)){
return;
}
mTxtPaint.setFakeBoldText(true);
Rect rect = new Rect();
mTxtPaint.getTextBounds(text, 0, text.length(), rect);
//文本的宽度和高度
int txtWidth = rect.width();
int txtHeight = rect.height();
canvas.drawText(text, center.x - txtWidth / 2, center.y + mRadius + mMarginTop + txtHeight / 2, mTxtPaint);
mTxtPaint.setFakeBoldText(false);
//显示的时间进度值
String time = timeStringList.get(i);
if (!TextUtils.isEmpty(time)){
Rect rect2 = new Rect();
mTxtPaint.getTextBounds(time, 0, time.length(), rect2);
//文本的宽度
int txtWidth2 = rect2.width();
int txtHeight2 = rect2.height();
canvas.drawText(time, center.x - txtWidth2 / 2, center.y + mRadius + txtHeight + mMarginTop * 2 + txtHeight2 / 2, mTxtPaint);
}
}
/**
* 画空心
*
* @param canvas
* @param i
*/
private void drawCircleFill(Canvas canvas, int i) {
mPaint.setColor(mNoCompleteColor);
mPaint.setStyle(Paint.Style.STROKE);
CircleCenter center = mCenterXY.get(i);
canvas.drawCircle(center.x, center.y, mRadius - mLineWidth / 2, mPaint);
mPaint.setColor(mNoCompleteTextColor);
mPaint.setStyle(Paint.Style.FILL);
Rect rect = new Rect();
String text = String.valueOf(i + 1);
mPaint.getTextBounds(text, 0, text.length(), rect);
canvas.drawText(text, center.x - rect.width() / 2, center.y + rect.height() / 2, mPaint);
Log.e("mTextHeight:", mTextHeight + "");
}
/*画实心*/
private void drawCircleStroke(Canvas canvas, int i) {
mPaint.setColor(mCompleteColor);
mPaint.setStyle(Paint.Style.FILL);
CircleCenter center = mCenterXY.get(i);
canvas.drawCircle(center.x, center.y, mRadius, mPaint);
mPaint.setColor(mCompleteTextColor);
Rect rect = new Rect();
String text = String.valueOf((i + 1));
mPaint.getTextBounds(text, 0, text.length(), rect);
canvas.drawText(text, center.x - rect.width() / 2, center.y +rect.height() / 2, mPaint);
}
/**
* 绘制两圆之间的连接线 总数为点数-1
*
* @param canvas 画布
* @param i 画线条到当前位置 线开始的XY轴的值
*/
private void drawLine(Canvas canvas, int i) {
canvas.drawLine(mCenterXY.get(i - 1).x + mRadius - mLineWidth / 2, mCenterXY.get(i - 1).y, mCenterXY.get(i).x - mRadius + mLineWidth / 2, mCenterXY.get(i).y, mLinePaint);
}
/**
* 绘制多余的小数线段
* 这里的绘制原理是 计算百分比并找到完成步数的值 并在其之后 绘制一条完成颜色的线 该线的长度是一个线段乘以完成的百分比
*
* @param canvas 画布
* @param percent 完成的百分比
*/
private void drawExtrasLine(Canvas canvas, float percent, int i) {
//设置延长线专用Paint的颜色和宽度
exPaint.setColor(mCompleteColor);
exPaint.setStrokeWidth(mLineWidth);
canvas.drawLine(mCenterXY.get(i).x + mRadius - mLineWidth / 2, mCenterXY.get(i).y, (mCenterXY.get(i).x + mRadius) + sectionLength * percent, mCenterXY.get(i).y, exPaint);
}
/**
* 传参的方法,对外开放
*
* @param mpointStringArray 传入的步骤的数组(这里之所以用数组是因为可以使用@Size注解)
* @param step 传入的完成步骤数
*/
public void setPointStrings(@Size(min = 2) String[] mpointStringArray, @Size(min = 2) String[] timeStringArray, @FloatRange(from = 1.0) float step) {
if (mpointStringArray.length == 0) {
pointStringList.clear();
circleCenterList.clear();
timeStringList.clear();
mStep = 0;
} else {
pointStringList = Arrays.asList(mpointStringArray);
timeStringList = Arrays.asList(timeStringArray);
mStep = Math.min(step, pointStringList.size());
invalidate();
}
}
/**
* 动态设置步数的方法
*
* @param step 步数
*/
public void setStep(@FloatRange(from = 1.0) float step) {
mStep = Math.min(step, pointStringList.size());
invalidate();
}
/**
* 手势方法重写 返回true来消费此方法
* 原理是当手指抬起时 计算点击的点是不是在步骤圆形中的某一个范围内,如果在就触发自定义点击事件
*
* @param event 点击事件
* @return true 消费事件
*/
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
if (x + getLeft() < getRight() && y + getTop() < getBottom()) {
}
break;
}
return true;
}
/**
* 圆心的内部实体类
* 主要为是判断点击事件是否在圆上提供的Bean
*/
class CircleCenter {
float x;
float y;
private CircleCenter(float x, float y) {
this.x = x;
this.y = y;
}
private float getX() {
return x;
}
public void setX(float x) {
this.x = x;
}
private float getY() {
return y;
}
public void setY(float y) {
this.y = y;
}
}
}
3、在布局中使用
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<us.mifeng.view.TimeLineView
android:id="@+id/timeline1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="20dp"
app:txMarginTop="8dp"
app:CompleteColor="@color/colorAccent"
app:NoCompleteColor="@color/colorPrimaryDark"
app:completeTextColor="@color/color_ffffff"
app:noCompleteTextColor="@color/color_666666"
app:textColor="@color/color_666666"
app:inRadiusTextSize="12sp"
app:textSize="12sp"
app:lineWidth="4dp"
app:tlradius="12dp" />
<us.mifeng.view.TimeLineView
android:id="@+id/timeline2"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_margin="20dp"
app:textSize="12sp"
app:tlradius="5dp" />
</LinearLayout>
4、查找组件,设置显示数据
TimeLineView timeline1 = findViewById(R.id.timeline1);
TimeLineView timeline2 = findViewById(R.id.timeline2);
String[] times = new String[]{"2018-12-23","2018-12-24","2018-12-25"};
String[] steps = new String[]{"发起售后","商家收货","商家维修"};
timeline1.setPointStrings(steps,times, 1.54f);
String[] times2 = new String[]{"2018-12-23","2018-12-24","2018-12-25","2018-12-26"};
String[] steps2 = new String[]{"第一步哟第一步","第二步","第三部哟第三步","第四步"};
timeline2.setPointStrings(steps2,times2,2.423f);