按照规矩先上图
Android关于自定义View的文章不再新鲜,实现方式也是各有千秋,在这里自己写的一个进度指示器实现方式是通过继承自View
实现,内部所有的圆圈、线条和文字都是直接绘制,没有以ViewGroup
的addView()
方式去做。
以最终效果图为参考,我们要分4步走
- 绘制空心的圆圈
- 绘制实心的圆
- 绘制圆圈之间的线条
- 绘制每一个步骤的说明文字
根据需求来,哪些东西是可以用户自定义的,是通过属性控制还是暴露setter
出去自己决定。这里我准备的attribute
如下,用户可自定义的就是圆圈大小和色值等
xml
然后是在View
中定义的一些变量
private Paint mStrokePaint = new Paint(Paint.ANTI_ALIAS_FLAG);//空心圆圈画笔
private Paint mCenterPaint = new Paint(Paint.ANTI_ALIAS_FLAG);//实心圆圈画笔
private Paint mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);//文字画笔
private Paint mLinePaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);//线条画笔
private RectF mLineRect = new RectF();//线条
private Rect mTextRect = new Rect();//进度文字
private float mGap;//线条与圆圈之间的间隔
private float mMargin;//文字与圆圈垂直方向的间距
private float mStepTextSize;//进度文字大小
private float mLineHeight;//线条高度
private float mStrokeWidth;//外圈的宽度
private float mCenterRadius;//内圈半径
private float mStrokeRadius;//外圈半径
private float mCircleHeight;//绘制圆圈的高度
private float mTextY;//绘制文案的Y值
private int mMode;//文字相对于圆圈的位置,上下
private int mProgress;
private int mMaxCount;
private int mDefaultColor;
private int mReachedColor;
private int mTextColor;
private int mLineColor;
private int mWidth;
private int mHeight;
初始化的自定义View代码是需要套公式的,在构造中通过TypedArray
获取View的各种属性值、为一些属性提供默认值、定义好属性变量、根据View的需要提供默认的宽高等,然后继续。
onMeasure()
在onMeasure
方法中一定要为你的View添加好宽高限制,关键点是View的测量要避免在MeasureSpec.AT_MOST
和MeasureSpec.UNSPECIFIED
的情况下获取不到想要的效果,最好提供给一个默认值。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//measure()方法为自己定义的测量方法
setMeasuredDimension(measure(widthMeasureSpec, true), measure(heightMeasureSpec, false));
}
private int measure(int measureSpec, boolean isWidth) {
int result;
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
int padding = isWidth ? getPaddingLeft() + getPaddingRight() : getPaddingTop() + getPaddingBottom();
if (mode == MeasureSpec.EXACTLY) {
result = size;
} else {
result = isWidth ? getSuggestedMinimumWidth() : getSuggestedMinimumHeight();
result += padding;
if (mode == MeasureSpec.AT_MOST) {
if (isWidth) {
result = Math.max(result, size);
} else {
result = Math.min(result, size);
}
}
}
return result;
}
这里给View提供的最小宽高分别是一个圆圈的宽和一个200的高。
@Override
protected int getSuggestedMinimumWidth() {
return (int) (mStrokeRadius * 2);
}
@Override
protected int getSuggestedMinimumHeight() {
return DEFAULT_HEIGHT;
}
onDraw()
绘制到现在为止我们已经控制好View的宽高了,其实也可以先绘制一番看看效果了,接着在onDraw
里面根据最大的mMaxCount和mProgress绘制圆圈。
- (这里View绘制的效果是居中显示的)首先考虑绘制最外层的空心圆圈,如果
mMaxCount
为1的时候圆圈的位置就是屏幕的中心点了,如果mMaxCount
为2的时候呢?我的想法是把屏幕可用的宽度(除去padding)2等分,两个圆点之间的宽度权重占一,两点距离左右的边距各自权重为0.5,以此类推。- 绘制实心圆圈就是和空心圆的位置完全一样,只需要传入不同的画笔和半径就OK了。
- 最后绘制圆圈之间的连接线,两个圆圈之间的间隔
g
的长度是可以计算出来的,然后在这个基础上减去圆圈的半径就OK了,当然友好一点的做法还是要通过一个属性gap
去控制线条与圆圈之间的margin
值。- 为了让线条的高度可以自定义,这里的线条就通过
Rect
去绘制了。
int height = getHeight() - getPaddingBottom() - getPaddingTop();
int usefulWidth = mWidth - getPaddingLeft() - getPaddingRight();//可用宽度
float g = (usefulWidth / (mMaxCount));
//起始点
float startX = getPaddingLeft() + mStrokeRadius;
startX += g / 2.0f;//这里绘制不能超出paddingRight
for (int iLoop = 0; iLoop < mMaxCount; iLoop++) {
float drawX = startX + (iLoop * g);
//已完成进度的色值
mStrokePaint.setColor(iLoop <= mProgress - 1 ? mReachedColor : mDefaultColor);
canvas.drawCircle(drawX, height / 2, mStrokeRadius, mStrokePaint);
//绘制进度位置
if (iLoop <= mProgress - 1) {
canvas.drawCircle(drawX, height / 2, mCenterRadius, mCenterPaint);
}
}
//绘制连接线
if (mDrawLine) {//mMaxCount大于1的时候就绘制连接线
int lineCount = mMaxCount - 1;
for (int lLoop = 0; lLoop < lineCount; lLoop++) {
float drawXStart = startX + mStrokeRadius + dp2px(mGap) + (lLoop * g);
float drawXEnd = startX + g - mStrokeRadius - dp2px(mGap) + (lLoop * g);
mLineRect.left = drawXStart;
mLineRect.right = drawXEnd;
canvas.drawRect(mLineRect, mLinePaint);
}
}
看下效果,初步就长这个样子,当然具体的各个值是要经过计算和调试的。
接下来就要绘制文字了,文字的绘制位置需要谨慎操作,每一个步骤圆心的位置也就是你每一步文案说明的X轴中心,而Y值需要另行计算,以圆圈在文案上方为例子,圆圈的最底部和文案的最上方应该是Y值相等的,但是首先我们需要保证的是圆圈和文字的绘制位置Y轴一致,然后再去做加减法控制文字的位移,这里控制文字的Y轴是不能通过简单的textSize
控制的,关于Paint
的decent
等方法文末附上链接,于是有了下面这段代码控制绘制文案的Y轴。
private void calcSize() {
int usefulHeight = mHeight - getPaddingBottom() - getPaddingTop();
if (checkSteps()) {
mDrawText = true;
mTextPaint.getTextBounds(String.valueOf(mSteps.get(0)), 0, String.valueOf(mSteps.get(0)).length(), mTextRect);
float center = (mTextPaint.ascent() + mTextPaint.descent()) / 2.0f;
if (mMode == MODE_BOTTOM) {
mCircleHeight = usefulHeight / 2.0f - mTextSize;
mTextY = mCircleHeight - center;
} else {
mCircleHeight = usefulHeight / 2.0f + mTextSize;
mTextY = mCircleHeight - center;
}
} else {
mDrawText = false;
mCircleHeight = usefulHeight / 2.0f;
}
......
}
OK,现在文字和圆圈的Y轴一致了,然后我们根据mode
来控制文字上下方的位移,如果文字在圆圈上方,那么mTextY
的值就需要在原来的基础上减去圆圈半径mStrokeRadius
,然后再减去mTextSize
的1/2才能正好使得文字的最下方和圆圈的正上方是同一个Y轴。看代码。
......
if (checkSteps()) {
mDrawText = true;
mTextPaint.getTextBounds(String.valueOf(mSteps.get(0)), 0, String.valueOf(mSteps.get(0)).length(), mTextRect);//这里假设所有的文案字体大小是一样的,忽略SpannableString带来的影响,严谨一点的做法可以在onDraw()里自行测量
float center = (mTextPaint.ascent() + mTextPaint.descent()) / 2.0f;
if (mMode == MODE_BOTTOM) {
mCircleHeight = usefulHeight / 2.0f - mTextSize;
mTextY = mCircleHeight - center + mStrokeRadius + mTextSize / 2.0f + dp2px(mMargin);
} else {
mCircleHeight = usefulHeight / 2.0f + mTextSize;
mTextY = mCircleHeight - center - mStrokeRadius - mTextSize / 2.0f - dp2px(mMargin);
}
}
然后再来看一下效果
貌似文案的绘制也OK了,接下来就需要修饰一番了。
最后,我加了textMargin
属性控制文案和圆圈之间的间隔,indicator_mode
属性来控制文案和圆圈的相对位置.最终的结果就是效果图了,代码链接附上。
Android FontMetrics