效果图
代码实现
首先讲一下自定义View的一个大致的步骤有哪些:
- 自定义属性的声明与获取
- 测量onMeasure
- 布局onLayout(ViewGroup)
- 绘制onDraw
- onTouchEvent
onInterceptTouchEvent(ViewGroup)
实现上图所示的效果,我们只需要用到1,2,4这三个步骤,接下来就带大家一步步实现。
自定义属性的声明与获取
首先,在当前项目的values文件夹下新建attrs.xml文件,里面编写我们的自定义属性,文件内容如下,我们总结了如下的属性:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleProgressView">
<!--圆的半径-->
<attr name="circleRadius" format="dimension"/>
<!--圆的颜色-->
<attr name="circleColor" format="color"/>
<!--文字颜色-->
<attr name="textColor" format="color"/>
<!--弧线颜色-->
<attr name="arcColor" format="color"/>
<!--弧线宽度-->
<attr name="arcWidth" format="dimension"/>
<!--文字大小-->
<attr name="textSize" format="dimension"/>
<!--弧线与圆直间的间距-->
<attr name="arcPadding" format="dimension"/>
</declare-styleable>
</resources>
自定义属性声明好之后,我们在代码中来进行获取,写一个类CircleProgressView继承自ProgressBar,在构造方法中我们进行自定义属性的获取,
public class CircleProgressView extends ProgressBar {
/*默认圆形颜色*/
public static final int DEFAULT_CIRCLE_COLOR = 0XFF0000;
/*弧线默认颜色*/
public static final int DEFAULT_ARC_COLOR = 0X00FF00;
/*文字默认颜色*/
public static final int DEFAULT_TEXT_COLOR = 0X000000;
/*圆形默认半径*/
public static final int DEFAULT_CIRCLE_RADIUS = 100;
/*弧线默认宽度*/
public static final int DEFAULT_ARC_WIDTH = 40;
/*默认文字大小*/
public static final int DEFAULT_TEXT_SIZE = 60;
/*弧线和圆之间默认的间距*/
public static final int DEFAULT_ARC_PADDING = 30;
private int circleColor;
private float circleRadius;
private int arcColor;
private int textColor;
private float arcWidth;
private float textSize;
private float arcPadding;
private int mRealWidth;
public CircleProgressView(Context context) {
this(context,null);//一个参数的构造方法调用两个参数的构造方法
}
public CircleProgressView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleProgressView);
circleColor = typedArray.getColor(R.styleable.CircleProgressView_circleColor, DEFAULT_CIRCLE_COLOR);
circleRadius = typedArray.getDimension(R.styleable.CircleProgressView_circleRadius,DEFAULT_CIRCLE_RADIUS);
arcColor = typedArray.getColor(R.styleable.CircleProgressView_arcColor, DEFAULT_ARC_COLOR);
textColor = typedArray.getColor(R.styleable.CircleProgressView_textColor, DEFAULT_TEXT_COLOR);
arcWidth = typedArray.getDimension(R.styleable.CircleProgressView_arcWidth, DEFAULT_ARC_WIDTH);
textSize = typedArray.getDimension(R.styleable.CircleProgressView_textSize, DEFAULT_TEXT_SIZE);
arcPadding = typedArray.getDimension(R.styleable.CircleProgressView_arcPadding, DEFAULT_ARC_PADDING);
}
}
测量onMeasure
接下来是要重写onMeasure方法来对自定义的进度条进行测量,为何要重写onMeasure方法呢?直接开始绘制不就完了吗?这里我说一下重写onMeasure方法的意义何在:
简单一句话,重写onMeasure就是为了给View一个wrap_content时的默认大小,如果我们在自定义View时不重写onMeasure,那么当你指定View的宽高为wrap_content时,此时的效果是充满整个父控件,所以如果需要使用wrap_content属性(根据内容确定控件大小),必须重写该方法。
先看一张图
很明显,这个进度条的宽度等于三部分之和:
(圆半径+圆与弧线之间的间距+弧宽度)*2
然后我们需要设置View最终的尺寸,下面给出onMeasure方法的实现
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//期望的宽度
int expectSize = (int) ((circleRadius+arcPadding+arcWidth)*2+getPaddingLeft()+getPaddingRight());
int width = resolveSize(expectSize,widthMeasureSpec);
int height = resolveSize(expectSize,heightMeasureSpec);
mRealWidth = Math.min(width,height);//取宽和高中较小的一个值作为View的尺寸
setMeasuredDimension(mRealWidth, mRealWidth);
}
其中用到了一个比较有用的方法resolveSize,这是系统已经为我们封装好的,简单说一下,这个方法的主要作用就是根据你提供的大小和传过来的MeasureSpec,返回最终的大小值,这个里面根据传入模式的不同来做相应的处理。最后调不要忘了调用setMeasuredDimension方法保存设置的值。
绘制onDraw
这里我们分三部分进行绘制,圆形,弧线以及圆形中间的文字
画圆
画圆的重点在于圆心位置和半径的确定,很明显,圆心位置在当前View的中心,半径由我们的自定义属性进行设置,废话不多说,直接上代码
//创建画笔
Paint circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
//设置画笔颜色
circlePaint.setColor(circleColor);
//画圆 canvas.drawCircle(mRealWidth/2,mRealWidth/2,circleRadius,circlePaint);
画弧线
画弧线我们首先要确定弧线所在矩形的位置,因为在Android中,弧线是由一个矩形来规定的,如图所示
//画弧线
Paint arcPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
arcPaint.setColor(arcColor);
arcPaint.setStrokeWidth(arcWidth);
arcPaint.setStyle(Paint.Style.STROKE);
RectF rectF = new RectF(arcWidth/2,arcWidth/2,mRealWidth-arcWidth/2,mRealWidth-arcWidth/2);
Log.d(TAG, "onDraw: "+rectF.toString());
float sweepValue = getProgress()*1.0f/getMax()*360;
canvas.drawArc(rectF,-90,sweepValue,true,arcPaint);
//画文字
Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
textPaint.setTextAlign(Paint.Align.CENTER);
textPaint.setTextSize(textSize);
textPaint.setColor(textColor);
String tvContent = getProgress()+"%";
float height = getTextHeight(tvContent,textPaint);
canvas.drawText(tvContent,mRealWidth/2,mRealWidth/2+height/2,textPaint);
获取文字高度的方法如下
public float getTextHeight(String text,Paint paint){
Rect bounds = new Rect();
paint.getTextBounds(text,0,text.length(),bounds);
return bounds.height();
}
布局文件如下
<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"
android:gravity="center"
>
<com.example.personcenter.CircleProgressView
android:id="@+id/circleView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:circleColor="@android:color/holo_green_light"
app:circleRadius="150px"
app:textColor="@android:color/holo_red_light"
app:arcColor="@android:color/holo_blue_light"
app:arcWidth="50px"
app:textSize="16sp"
app:arcPadding="50px"
android:max="100"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="update"
android:text="特么的动起来"
android:layout_marginTop="20dp"
/>
</LinearLayout>
最后我们在代码中控制进入条的变化。
public class MainActivity extends AppCompatActivity {
public static final int MSG_UPDATE = 0X11;
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
int currentProgress = circleProgressView.getProgress();
circleProgressView.setProgress(++currentProgress);
if(currentProgress >= circleProgressView.getMax()){
handler.removeMessages(MSG_UPDATE);
}else{
handler.sendEmptyMessageDelayed(MSG_UPDATE,200);
}
}
};
private CircleProgressView circleProgressView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
circleProgressView = (CircleProgressView) findViewById(R.id.circleView);
}
public void update(View view){
handler.sendEmptyMessage(MSG_UPDATE);
}
}
效果二
public class CircleProgressView extends ProgressBar {
/*默认圆形颜色*/
public static final int DEFAULT_CIRCLE_COLOR = 0XFF0000;
/*弧线默认颜色*/
public static final int DEFAULT_ARC_COLOR = 0X00FF00;
/*文字默认颜色*/
public static final int DEFAULT_TEXT_COLOR = 0X000000;
/*圆形默认半径*/
public static final int DEFAULT_CIRCLE_RADIUS = 100;
/*弧线默认宽度*/
public static final int DEFAULT_ARC_WIDTH = 40;
/*默认文字大小*/
public static final int DEFAULT_TEXT_SIZE = 60;
/*弧线和圆之间默认的间距*/
public static final int DEFAULT_ARC_PADDING = 30;
private float circleRadius;
private int arcColor;
private int textColor;
private float arcWidth;
private float textSize;
private float arcPadding;
private int mRealSize;
private Paint mPaintCircle;
private Path mPath;
private Paint mPathPaint;
private int width;
private int height;
private int count = 0;
private Canvas bitmapCanvas;//定义Bitmap的画布
private Bitmap bitmap;//定义Bitmap的画布
//设置进度
private int maxProgress = 100;
private int currentProgress = 0;
private static final int NEED_INVALIDATE = 0X6666;
//操作UI主线程
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case NEED_INVALIDATE:
//更新时间
count += 5;
if (count > 80) {
count = 0;
}
invalidate();
sendEmptyMessageDelayed(NEED_INVALIDATE, 50);
break;
}
}
};
public CircleProgressView(Context context) {
this(context, null);//一个参数的构造方法调用两个参数的构造方法
}
public CircleProgressView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleProgressView);
circleRadius = typedArray.getDimension(R.styleable.CircleProgressView_circleRadius, DEFAULT_CIRCLE_RADIUS);
arcColor = typedArray.getColor(R.styleable.CircleProgressView_arcColor, DEFAULT_ARC_COLOR);
textColor = typedArray.getColor(R.styleable.CircleProgressView_textColor, DEFAULT_TEXT_COLOR);
arcWidth = typedArray.getDimension(R.styleable.CircleProgressView_arcWidth, DEFAULT_ARC_WIDTH);
textSize = typedArray.getDimension(R.styleable.CircleProgressView_textSize, DEFAULT_TEXT_SIZE);
arcPadding = typedArray.getDimension(R.styleable.CircleProgressView_arcPadding, DEFAULT_ARC_PADDING);
//初始化绘制路径的画笔
mPath = new Path();
mPathPaint = new Paint();
mPathPaint.setAntiAlias(true);
mPathPaint.setColor(Color.argb(0xff, 0xff, 0x69, 0x5a));
mPathPaint.setStyle(Paint.Style.FILL);//设置为填充,默认为填充,这里我们还是定义下
mPathPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
mPaintCircle = new Paint();
mPaintCircle.setAntiAlias(true);
mPaintCircle.setColor(Color.argb(0xff, 0xf8, 0x8e, 0x8b));
handler.sendEmptyMessageDelayed(NEED_INVALIDATE, 50);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//期望的宽度
int expectSize = (int) ((circleRadius+arcPadding+arcWidth)*2+getPaddingLeft()+getPaddingRight());
width = resolveSize(expectSize,widthMeasureSpec);
height = resolveSize(expectSize,heightMeasureSpec);
mRealSize = Math.min(width, height);//取宽和高中较小的一个值作为View的尺寸
setMeasuredDimension(width, mRealSize);
bitmap = Bitmap.createBitmap(width, mRealSize, Bitmap.Config.ARGB_8888);
bitmapCanvas = new Canvas(bitmap);
}
@Override
protected synchronized void onDraw(Canvas canvas) {
super.onDraw(canvas);
//绘制Bitmap上的圆形
bitmapCanvas.drawCircle(mRealSize / 2, height / 2, 150, mPaintCircle);
//通过Path绘制贝塞尔曲线
mPath.reset();
mPath.moveTo(mRealSize, (height / 2 + 150) - (currentProgress * 300f / maxProgress));
mPath.lineTo(mRealSize, height / 2 + 200);
mPath.lineTo(count, height / 2 + 200);
mPath.lineTo(count, (height / 2 + 150) - (currentProgress * 300f / maxProgress));
for (int i = 0; i < 10; i++) {
mPath.rQuadTo(20, 5, 40, 0);
mPath.rQuadTo(20, -5, 40, 0);
}
mPath.close();
//将贝塞尔曲线绘制到Bitmap的Canvas上
bitmapCanvas.drawPath(mPath, mPathPaint);
//
//画弧线
Paint arcPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
arcPaint.setColor(arcColor);
arcPaint.setStrokeWidth(arcWidth);
arcPaint.setStyle(Paint.Style.STROKE);
RectF rectF = new RectF(arcWidth/2,arcWidth/2, mRealSize -arcWidth/2, mRealSize -arcWidth/2);
float sweepValue = currentProgress*1.0f/maxProgress*360;
bitmapCanvas.drawArc(rectF,-90,sweepValue,false,arcPaint);
//画文字
Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
textPaint.setTextAlign(Paint.Align.CENTER);
textPaint.setTextSize(textSize);
textPaint.setColor(textColor);
String tvContent = currentProgress+"%";
float height = getTextHeight(tvContent,textPaint);
bitmapCanvas.drawText(tvContent, mRealSize /2, mRealSize /2+height/2,textPaint);
canvas.drawBitmap(bitmap, 0, 0, null);
}
public float getTextHeight(String text,Paint paint){
Rect bounds = new Rect();
paint.getTextBounds(text,0,text.length(),bounds);
return bounds.height();
}
public void setCurrentProgress(int currentProgress) {
this.currentProgress = currentProgress;
invalidate();//实时更新进度
}
public int getMaxProgress() {
return maxProgress;
}
public void setMaxProgress(int maxProgress) {
this.maxProgress = maxProgress;
}
public int getCurrentProgress() {
return currentProgress;
}
}
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private Button mButton;
private CircleProgressView circleProgressView;
public static final int MSG_UPDATE = 0X11;
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
int currentProgress = circleProgressView.getCurrentProgress();
circleProgressView.setCurrentProgress(++currentProgress);
if (currentProgress >= circleProgressView.getMaxProgress()) {
handler.removeMessages(MSG_UPDATE);
} else {
sendEmptyMessageDelayed(MSG_UPDATE,100);
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = (Button) findViewById(R.id.btn);
circleProgressView = (CircleProgressView) findViewById(R.id.circleView);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (circleProgressView.getCurrentProgress() < circleProgressView.getMaxProgress()) {
handler.sendEmptyMessage(MSG_UPDATE);
}
}
});
}
}