之前研究了相关的绘制,单个控件的测量,以及简单的类内部事件处理。我们先自定义个一个圆形进度条作为实践。然后再继续更复杂的自定义控件:
看效果:
有几个要素:
1. 内圈圆的绘制+中心进度的绘制
2. 最外圈的外切范围的计算+进度的绘制
3. 第二层外圈的外切范围的计算+进度比最外层稍慢的绘制处理
4. 当然最麻烦的是就是你需要去兼容内圈半径不能超过控件范围、最外层环不能太大以及覆盖住内圈的问题、第二层外圈(黑色部分)正好在最外层外环和内圈的问题,最后的一系列处理也就是为了做到兼容适配,不管用户怎么设置都没问题。当然初始阶段我觉得可以先做效果,然后进行版本迭代....
我想先把代码、属性、布局贴出来吧:
attrs.xml --- 属性配置
activity_main.xml --- 布局文件
CircleProgressView.java --- 奉上自定义控件文件
package me.heyclock.hl.customcopy;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
/*
*@Description: 自定义圆形进度条
*@Author: hl
*@Time: 2018/10/12 9:37
*/
public class CircleProgressView extends View {
/* 官方文档:
https://developer.android.google.cn/reference/android/graphics/Canvas
https://developer.android.google.cn/reference/android/graphics/Paint
*/
private Context context; ///< 上下文
private Canvas canvas; ///< 画布
private Paint paintInside; ///< 内圈
private Paint paintOutside; ///< 外圈
private Paint paintOutsideS; ///< 外圈第二层
private int outsideThick; ///< 外环厚度
private int outsideThickS; ///< 外环厚度第二层
private Paint paintText; ///< 文本
private int textSize; ///< 文本大小
///< 做红色点击区域限制
private boolean bIsDownInRedRegion = false;
///< 圆圈半径
private int radius;
///< 圆圈颜色
private String inside_color;
private String outside_color;
private String text_color;
///< 控件自定义背景
private Bitmap bgDrawable = null;
///< 控件宽度和高度
private int width = 12;
private int height = 12;
///< 进度百分比
private float mCurPercent = 1;
/**
* 刷新绘制+增量变化
*/
private static final int STEP_RADIUS = 10; ///< 每次半径增加10
public CircleProgressView(Context context) {
this(context, null);
}
public CircleProgressView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CircleProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, 0, 0);
}
public CircleProgressView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
this.context = context;
///< TypedArray的方式
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CircleProgressView);
///< getDimension() getDimensionPixelOffset() getDimensionPixelSize()
/// --这三个方法都是根据DisplayMetrics获取相应的值,不同在于方法1直接保存float型数据,方法2直接对float取整,方法3对float小数先四舍五入后取整。
radius = ta.getDimensionPixelOffset(R.styleable.CircleProgressView_inside_radius, 6);
inside_color = ta.getString(R.styleable.CircleProgressView_inside_color);
outside_color = ta.getString(R.styleable.CircleProgressView_outside_color);
outsideThick = ta.getDimensionPixelOffset(R.styleable.CircleProgressView_outside_width, 18);
text_color = ta.getString(R.styleable.CircleProgressView_text_color);
textSize = ta.getInteger(R.styleable.CircleProgressView_text_size, 38);
Drawable drawable = ta.getDrawable(R.styleable.CircleProgressView_bg_drawable);
if (null != drawable) {
BitmapDrawable bd = (BitmapDrawable) drawable;
bgDrawable = bd.getBitmap();
}
ta.recycle();
///< 1\. 做一些绘制初始化
canvas = new Canvas(); ///< 也可以指定绘制到Bitmap上面 -> Canvas(Bitmap bitmap)
///< 1.1 内环
paintInside = new Paint();
paintInside.setStyle(Paint.Style.FILL); ///< 绘制实心圆
paintInside.setColor(Color.parseColor(inside_color));
paintInside.setAntiAlias(true); ///< 消除锯齿
///< 1.2 中心文本
paintText = new Paint();
paintText.setTextSize(textSize);
paintText.setColor(Color.parseColor(text_color));
paintText.setAntiAlias(true); ///< 消除锯齿
///< 1.3 外环进度
paintOutside = new Paint();
paintOutside.setColor(Color.parseColor(outside_color));
paintOutside.setStrokeWidth(outsideThick);
paintOutside.setStrokeCap(Paint.Cap.ROUND); ///< 圆形头
paintOutside.setStyle(Paint.Style.STROKE); ///< 绘制空心圆
paintOutside.setAntiAlias(true); ///< 消除锯齿
///< 1.4 外环第二层
paintOutsideS = new Paint(paintOutside);
paintOutsideS.setColor(Color.BLACK);
}
@Override
protected void onDraw(Canvas canvas) {
//super.onDraw(canvas);
///< 2.进行绘制
///< 先绘制一个背景 - 如果自定义控件背景存在的情况下,则进行背景绘制
if (null != bgDrawable) {
canvas.drawBitmap(bgDrawable, 0, 0, paintOutside);
}
///< 绘制一个圆圈吧-> drawCircle(float cx, float cy, float radius, Paint paint)
canvas.drawCircle(width / 2, height / 2,
radius / 2, paintInside);
///< 绘制圆圈中心的进度 - 白色
Rect txRect = new Rect();
String percentS = mCurPercent + "%";
paintText.getTextBounds(percentS, 0, percentS.length(), txRect);
///< 关于起点,涉及到基线相关的知识(目前可以姑且暂时理解为绘制起始点是左下角)
canvas.drawText(percentS, width / 2 - txRect.width() / 2,
height / 2 + txRect.height() / 2, paintText);
///< 绘制外环
RectF mCircle = new RectF(); ///< 背景圆的外接矩形
mCircle.set(outsideThick, ///< 外切矩形范围
outsideThick,
getWidth() - outsideThick,
getHeight() - outsideThick);
canvas.drawArc(mCircle, 270,
360 * mCurPercent / 100,
false, paintOutside);
///< 绘制第二层外圈
RectF mCircleSecond = new RectF(); ///< 背景圆的外接矩形
///< 第二层的外切矩形范围左上角相当于第一层的左上角+第二层外圈的厚度,相应的右下角需要多减去一个厚度
mCircleSecond.set(outsideThick + outsideThickS, ///< 外切矩形范围
outsideThick + outsideThickS,
getWidth() - outsideThick - outsideThickS,
getHeight() - outsideThick - outsideThickS);
///< 让第二层外圈稍微慢点走
float realPercent = mCurPercent > 3 ? mCurPercent - 3 : mCurPercent;
canvas.drawArc(mCircleSecond, 270,
360 * realPercent / 100,
false, paintOutsideS);
///< TODO 只有会专门研究相关的绘制以及相关参数的示意图
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
///< 采用默认的onMeasure看看
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
///< 自己进行相关测量
int defaultW = 12;
int defaultH = 12;
int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int wSize = MeasureSpec.getSize(widthMeasureSpec);
int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int hSize = MeasureSpec.getSize(heightMeasureSpec);
///< 在wrap_content的情况下默认长度为默认宽/高与背景图片对比后的最大值,假设背景图片是200*200,则显示200*200
int minWSize = Math.max(dp2px(context, defaultW), getSuggestedMinimumWidth());
int minHSize = Math.max(dp2px(context, defaultH), getSuggestedMinimumHeight());
///< 如果设置了半径的情况下,再跟半径做一次取值,保证用户设置wrap_content的,半径设置的情况下以半径为主
minWSize = Math.max(minWSize, radius);
minHSize = Math.max(minHSize, radius);
///< wrap_content的specMode是AT_MOST模式,这种情况下宽/高等同于specSize
// 查表得这种情况下specSize等同于parentSize,也就是父容器当前剩余的大小
// 在wrap_content的情况下如果不特殊处理,效果等同martch_parent
if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
width = minWSize;
height = minHSize;
setMeasuredDimension(minWSize, minHSize);
} else if (wSpecMode == MeasureSpec.EXACTLY && hSpecMode == MeasureSpec.EXACTLY) {
width = wSize;
height = hSize;
setMeasuredDimension(wSize, hSize);
} else if (wSpecMode == MeasureSpec.AT_MOST) {
width = minWSize;
height = hSize;
setMeasuredDimension(minWSize, hSize);
} else if (hSpecMode == MeasureSpec.AT_MOST) {
width = wSize;
height = minHSize;
setMeasuredDimension(wSize, minHSize);
}
///< 做一个兼容,如果半径超过了控件宽或者高
int minWH = width;
if (width > height) {
minWH = height;
}
///< 1\. 兼容半径
if ((radius * 2) > minWH) {
radius = minWH / 2;
Log.e("attrs", "纠正一下 " + radius);
}
///< 2\. 兼容外圈厚度*2 不超过控件范围,同时不能覆盖内圈; 另外多减了radius/2作为中空部分占据
if (outsideThick >= radius/4){
outsideThick = radius/4;
paintOutside.setStrokeWidth(outsideThick);
}
///< 3\. 外圈第二层
outsideThickS = (width - outsideThick * 2 - radius)/3;
paintOutsideS.setStrokeWidth(outsideThickS);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
///< 点击区域坐标范围
int minX = (width - radius * 2) / 2;
int maxX = width / 2 + radius;
int minY = (height - radius * 2) / 2;
int maxY = height / 2 + radius;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (x >= minX && x <= maxX &&
y >= minY && y <= maxY) {
bIsDownInRedRegion = true;
}
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
if (bIsDownInRedRegion) {
bIsDownInRedRegion = false;
if (x >= minX && x <= maxX &&
y >= minY && y <= maxY) {
///< 抬手时我们就可以启动定时器进行绘制刷新了
Log.e("test", "进度条区域点击了呀,sb");
}
}
break;
}
return true;
}
/**
* 刷新绘制+增量变化
*/
public void updateDraw(int perCent) {
///< 更新进度条
mCurPercent = perCent;
invalidate();
}
/**
* dp转px
*
* @param dp
* @return
*/
public static int dp2px(Context context, int dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics());
}
}
然后调用方式,自定义了一个定时器不停的进行(调用部分代码):
private CircleProgressView circleProgressView;
private int perCent = 1;
Then, call
{
circleProgressView = findViewById(R.id.am_ciclePro);
final Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
runOnUiThread(new Runnable() {
@Override
public void run() {
circleProgressView.updateDraw(perCent++);
if (perCent > 100){
perCent = 1;
//timer.cancel();
}
}
});
}
}, 0, 100);
}
注意我们的内圈绘制时半径是: **radius **/ 2 - radius是宽高最小值的一半哟!
调试过程中的比较麻烦的就是两个外环(记住内圈半径是**radius **/ 2):
这样我们再来计算两个外圈半径厚度才能比较好处理....如下部分是厚度的处理:
或许我们需要这么分割一下才能便于我们进行计算:
在测量最外层环半径是可以这样: (minWH - radius/2 * 2 ) / 2(假设宽高一样, 就是控件宽度 - 内圈直径, 然后除以2,但是贴近红圈,这样黑色圈就不能放了。) 所以为了好看,我们再除以2空出一个内环的位置. (minWH - radius/2 * 2 ) / 2 / 2 = (minWH - radius/2 * 2 ) / 4 = (2*radius- radius/2 * 2 ) / 4 = radius/4;
目前就是大概就这样做了一个计算,总感觉不是很精准的样子。可能还有bug啥的。 后面具体深入paint的时候我们再回味吧....流程先搞了再说!