本文比较详细的介绍了绘制圆环及圆弧的基础知识,为实现钉钉运动步数打下基础,实现了下面的效果,实现钉钉运动就灰常简单了,本文实现的初步效果如下:
如果想直接看钉钉运动的最终效果,请戳:Android进阶之自定义控件(2)高仿钉钉运动步数实现可动的进度圆环(下)
![img_e846d6fd2f1477a28685e0ca588310f3.gif](https://yqfile.alicdn.com/img_e846d6fd2f1477a28685e0ca588310f3.gif?x-oss-process=image/resize,w_1400/format,webp)
Animation.gif
1、圆环的绘制
2、绘制背景圆环和进度圆环
3、绘制中间的文字
(1)使用drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)绘制圆环:
![img_d093e0d8f03bba697e61b48648c9f60f.png](https://yqfile.alicdn.com/img_d093e0d8f03bba697e61b48648c9f60f.png?x-oss-process=image/resize,w_1400/format,webp)
image.png
public class SportStepView extends View {
private Paint mPaint;
//圆环绘制的宽度
private int mRoundWidth = 40;
public SportStepView(Context context) {
this(context, null);
}
public SportStepView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public SportStepView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取宽的模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//获取宽的尺寸
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//对wrap_content这种模式进行处理
if (heightMode == MeasureSpec.AT_MOST) {
heightSize = widthSize;
}
//绘制圆环以宽度为标准,保存丈量结果
setMeasuredDimension(widthSize, heightSize);
}
private void init() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(mRoundWidth);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//绘制圆,设置画笔的Style为Paint.Style.STROKE,则绘制出来的为圆环,否则绘制出来的为圆
//canvas.drawCircle(getWidth() / 2, getHeight() / 2, getWidth() / 2 - mRoundWidth / 2, mPaint);
RectF oval = new RectF(0 , 0, getWidth(), getWidth());
//画圆弧 useCenter:是否显示圆内的横线 下面的绘制0,360的圆弧,也可以实现绘制圆环的效果
canvas.drawArc(oval, 0, 360, false, mPaint);
}
}
![img_bc56fc212710b235922a872b0d41e758.png](https://yqfile.alicdn.com/img_bc56fc212710b235922a872b0d41e758.png?x-oss-process=image/resize,w_1400/format,webp)
image.png
会发现显示不全,绘制超出边界了,因为圆的宽度是在当前半径向两边展开的。如下图分析得知,圆所在的矩形区域不是rect(为屏幕的矩形区域),而是real Rect所在的区域:
![img_35ad1d27d9f604baf5a47e96ce1818b0.png](https://yqfile.alicdn.com/img_35ad1d27d9f604baf5a47e96ce1818b0.png?x-oss-process=image/resize,w_1400/format,webp)
image.png
因此只需要修改如下即可。
RectF oval = new RectF(0 + mRoundWidth / 2, 0 + mRoundWidth / 2, getWidth() - mRoundWidth / 2, getWidth() - mRoundWidth / 2);
// RectF oval = new RectF(0 , 0, getWidth(), getWidth());
//画圆弧 useCenter:是否显示圆内的横线 下面的绘制0,360的圆弧,也可以实现绘制圆环的效果
canvas.drawArc(oval, 0, 360, false, mPaint);
修改后达到我们的预期效果:
![img_7049025487a8d2a977d848813310a1f4.png](https://yqfile.alicdn.com/img_7049025487a8d2a977d848813310a1f4.png?x-oss-process=image/resize,w_1400/format,webp)
image.png
(2)如果只是绘制上面的圆环效果,还可以使用: canvas.drawCircle()的方式实现,这种方法更简单:
//绘制圆,设置画笔的Style为Paint.Style.STROKE,则绘制出来的为圆环,否则绘制出来的为圆
//由于圆环本身有宽度,所以半径要减去圆环宽度的一半,不然一部分圆会在view外面。
canvas.drawCircle(getWidth() / 2, getHeight() / 2, getWidth() / 2 - mRoundWidth / 2, mPaint);
(3)接下来我们来实现背景圆环+进度圆环的效果了,利用drawCircle绘制背景圆环,drawArc()绘制进度圆环,预期效果如下:
![img_ea8e49fd6f4d511baf2cf4beeea53ce4.png](https://yqfile.alicdn.com/img_ea8e49fd6f4d511baf2cf4beeea53ce4.png?x-oss-process=image/resize,w_1400/format,webp)
image.png
这个很简单,再画个圆弧即可:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//绘制背景圆环,设置画笔的Style为Paint.Style.STROKE,则绘制出来的为圆环,否则绘制出来的为圆
//正常情况下
canvas.drawCircle(getWidth() / 2, getHeight() / 2, getWidth() / 2 - mRoundWidth / 2, mPaint);
//正常情况下,绘制进度圆环
RectF oval = new RectF(0 + mProgressRoundWidth / 2, 0 + mProgressRoundWidth / 2, getWidth() - mProgressRoundWidth / 2, getWidth() - mProgressRoundWidth / 2);
//画圆弧 useCenter:是否显示圆内的横线 下面的绘制0,360的圆弧,也可以实现绘制圆环的效果
canvas.drawArc(oval, 0, 300, false, mProgressPaint);
}
但是光这样处理,会有个小问题,就是当背景圆环和进度圆环宽度不一致时,会出现下面的问题。
![img_89b3beb3d5af1c96eec93ef216c81808.png](https://yqfile.alicdn.com/img_89b3beb3d5af1c96eec93ef216c81808.png?x-oss-process=image/resize,w_1400/format,webp)
image.png
解决方法:以宽度较大为准即可。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
/**
* 如果背景圆环和进度圆环宽度不一致,都以较大的宽度为准绘制。避免出现两者显示不居中的问题
*/
//绘制背景圆环,设置画笔的Style为Paint.Style.STROKE,则绘制出来的为圆环,否则绘制出来的为圆
// canvas.drawCircle(getWidth() / 2, getHeight() / 2, getWidth() / 2, mPaint);
if (mRoundWidth < mProgressRoundWidth) {
canvas.drawCircle(getWidth() / 2, getHeight() / 2, getWidth() / 2 - mProgressRoundWidth / 2, mPaint);
} else {
//正常情况下
canvas.drawCircle(getWidth() / 2, getHeight() / 2, getWidth() / 2 - mRoundWidth / 2, mPaint);
}
if (mRoundWidth < mProgressRoundWidth) {
// //正常情况下,绘制进度圆环
RectF oval = new RectF(0 + mProgressRoundWidth / 2, 0 + mProgressRoundWidth / 2, getWidth() - mProgressRoundWidth / 2, getWidth() - mProgressRoundWidth / 2);
// RectF oval = new RectF(0 , 0, getWidth(), getWidth());
//画圆弧 useCenter:是否显示圆内的横线 下面的绘制0,360的圆弧,也可以实现绘制圆环的效果
canvas.drawArc(oval, 0, 300, false, mProgressPaint);
} else {
//绘制进度圆环
RectF oval = new RectF(0 + mRoundWidth / 2, 0 + mRoundWidth / 2, getWidth() - mRoundWidth / 2, getWidth() - mRoundWidth / 2);
//画圆弧 useCenter:是否显示圆内的横线 下面的绘制0,360的圆弧,也可以实现绘制圆环的效果
canvas.drawArc(oval, 0, 300, false, mProgressPaint);
}
}
(4)绘制居中的进度文字
![img_33877873086c24770fbf490c806dcb34.png](https://yqfile.alicdn.com/img_33877873086c24770fbf490c806dcb34.png?x-oss-process=image/resize,w_1400/format,webp)
image.png
代码实现:
//绘制中间的文字
Rect textRect = new Rect();
//进度百分比
int progressPercent = (int) (mCurrentProgress * 1f / mMaxProgress * 100);
String mShowText = progressPercent + "%";
mTextPaint.getTextBounds(mShowText, 0, mShowText.length(), textRect);
canvas.drawText(mShowText, getWidth() / 2 - textRect.width() / 2, getHeight() / 2 + textRect.height() / 2, mTextPaint);
![img_f84f2366e0fc9010e48b4c28fbd83336.png](https://yqfile.alicdn.com/img_f84f2366e0fc9010e48b4c28fbd83336.png?x-oss-process=image/resize,w_1400/format,webp)
image.png
(5)处理圆环进度和文字进度的动态显示
方法一:开一个分线程,动态改变进度的值,不断绘制达到进度变化的效果。
方法二:下篇会介绍到_
在测试的Activity中使用:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// mytextview = findViewById(R.id.mytextview);
final SportStepView sportStepView = findViewById(R.id.sportstepview);
mCurrentProgress = 80;
//速度,值越大,变化速度越快
rate = 1;
//开一个分线程,动态改变进度的值,不断绘制达到进度变化的效果
new Thread(new Runnable() {
@Override
public void run() {
sportStepView.setCurrentProgress(0);
for (int i = 0; i < mCurrentProgress / rate; i++) {
sportStepView.setCurrentProgress(sportStepView.getCurrentProgress() + rate);
SystemClock.sleep(20);
// pb_progress.invalidate();//invalidate()必须在主线程中执行,此处不能使用
sportStepView.postInvalidate();//强制重绘,postInvalidate()可以在主线程也可以在分线程中执行
}
}
}).start();
}
完整代码:
package com.example.jojo.learn.customview;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import com.example.jojo.learn.R;
/**
* Created by JoJo on 2018/7/31.
* wechat:18510829974
* description: 仿钉钉运动步数
*/
public class SportStepView extends View {
//绘制背景圆环的画笔
private Paint mPaint;
//绘制外面进度的圆环的画笔
private Paint mProgressPaint;
//绘制外面进度的圆环的画笔
private Paint mTextPaint;
//背景圆弧的绘制的宽度
private int mRoundWidth = 40;
//进度圆环的宽度
private float mProgressRoundWidth = 60;
private int mTextSize = 40;//单位 sp
//圆环最大进度
private int mMaxProgress = 100;
//圆环当前进度
private int mCurrentProgress = 0;
public SportStepView(Context context) {
this(context, null);
}
public SportStepView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public SportStepView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取宽的模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//获取宽的尺寸
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//对wrap_content这种模式进行处理
if (heightMode == MeasureSpec.AT_MOST) {
heightSize = widthSize;
}
//以宽度为标准保存丈量结果
setMeasuredDimension(widthSize, heightSize);
}
private void init() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(mRoundWidth);
mProgressPaint = new Paint();
mProgressPaint.setAntiAlias(true);// 抗锯齿效果
mProgressPaint.setStyle(Paint.Style.STROKE);
mProgressPaint.setColor(Color.YELLOW);
mProgressPaint.setStrokeCap(Paint.Cap.ROUND);// 圆形笔头
mProgressPaint.setStrokeWidth(mProgressRoundWidth);
mTextPaint = new Paint();
mTextPaint.setAntiAlias(true);// 抗锯齿效果
mTextPaint.setStyle(Paint.Style.STROKE);
mTextPaint.setColor(Color.BLACK);
mTextPaint.setTextSize(sp2px(mTextSize));
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
/**
* 如果背景圆环和进度圆环宽度不一致,都以较大的宽度为准绘制。避免出现两者显示不居中的问题
*/
//绘制背景圆环,设置画笔的Style为Paint.Style.STROKE,则绘制出来的为圆环,否则绘制出来的为圆
// canvas.drawCircle(getWidth() / 2, getHeight() / 2, getWidth() / 2, mPaint);
if (mRoundWidth < mProgressRoundWidth) {
canvas.drawCircle(getWidth() / 2, getHeight() / 2, getWidth() / 2 - mProgressRoundWidth / 2, mPaint);
} else {
//正常情况下
canvas.drawCircle(getWidth() / 2, getHeight() / 2, getWidth() / 2 - mRoundWidth / 2, mPaint);
}
if (mRoundWidth < mProgressRoundWidth) {
// //正常情况下,绘制进度圆环
RectF oval = new RectF(0 + mProgressRoundWidth / 2, 0 + mProgressRoundWidth / 2, getWidth() - mProgressRoundWidth / 2, getWidth() - mProgressRoundWidth / 2);
// RectF oval = new RectF(0 , 0, getWidth(), getWidth());
//画圆弧 useCenter:是否显示圆内的横线 下面的绘制0,360的圆弧,也可以实现绘制圆环的效果
canvas.drawArc(oval, 0 + 90, mCurrentProgress * 1f / mMaxProgress * 360, false, mProgressPaint);
} else {
//绘制进度圆环
RectF oval = new RectF(0 + mRoundWidth / 2, 0 + mRoundWidth / 2, getWidth() - mRoundWidth / 2, getWidth() - mRoundWidth / 2);
//画圆弧 useCenter:是否显示圆内的横线 下面的绘制0,360的圆弧,也可以实现绘制圆环的效果
canvas.drawArc(oval, 0 + 90, mCurrentProgress * 1f / mMaxProgress * 360, false, mProgressPaint);
}
//绘制中间的文字
Rect textRect = new Rect();
//进度百分比
int progressPercent = (int) (mCurrentProgress * 1f / mMaxProgress * 100);
String mShowText = progressPercent + "%";
mTextPaint.getTextBounds(mShowText, 0, mShowText.length(), textRect);
canvas.drawText(mShowText, getWidth() / 2 - textRect.width() / 2, getHeight() / 2 + textRect.height() / 2, mTextPaint);
}
public void setCurrentProgress(int currentProgress) {
this.mCurrentProgress = currentProgress;
}
public void setMaxProgress(int maxProgress) {
this.mMaxProgress = maxProgress;
}
public int getMaxProgress() {
return mMaxProgress;
}
public int getCurrentProgress() {
return mCurrentProgress;
}
/**
* 将sp转换成px
*
* @param sp
* @return
*/
private int sp2px(int sp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp,
getResources().getDisplayMetrics());
}
}
涉及到的自定义属性
<!--SportStepView-->
<declare-styleable name="SportStepView">
<!--圆环半径-->
<attr name="radius" format="dimension"></attr>
<attr name="outerRoundColor" format="color"></attr>
<attr name="innerRoundColor" format="color"></attr>
</declare-styleable>