Andorid自定义拖动弧形进度条

时间:2021-04-12 05:43:18

在android自定义控件中,我们使用进度显示,为了好看,一般都不会用系统自带的。原因大家都懂的。而现在各种自定义进度条显示也是层出不穷,在实际开发中遇到的情况是需要带拖动效果的弧形进度条。下面我们先看看这个效果图:Andorid自定义拖动弧形进度条Andorid自定义拖动弧形进度条
开始自定义之前,我们就要把要用到的东西先定义出来,初始化

//进度边框画笔
private Paint bgpaint;
//进度底色画笔
private Paint colorpaint;
//文本画笔
private Paint txtpaint;
//进度颜色画笔
private Paint propaint;
//控件中心位置坐标
private int cx, cy;
//控件宽高
private int width, height;
//弧形体宽高
private int whsize;
//判断是否按下
private boolean isdown;
//分配圆弧数的比例
private float angle = 180 / 13;

private Paint cicpaint;
//当前点
private int currentdit;

在构造方法中初始化相关参数

 private void init()
{
bgpaint = new Paint();
colorpaint = new Paint();
txtpaint = new Paint();
propaint = new Paint();
cicpaint = new Paint();
cicwpaint = new Paint();
//抗锯齿
bgpaint.setAntiAlias(true);
//设置颜色
bgpaint.setColor(Color.GREEN);
//设置画笔风格 空心
bgpaint.setStyle(Paint.Style.STROKE);
//画笔宽度
bgpaint.setStrokeWidth(35);
//设置画笔尾部圆滑
bgpaint.setStrokeCap(Paint.Cap.ROUND);

colorpaint.setAntiAlias(true);
colorpaint.setColor(Color.WHITE);
colorpaint.setStyle(Paint.Style.STROKE);
colorpaint.setStrokeWidth(33);
colorpaint.setStrokeCap(Paint.Cap.ROUND);

propaint.setAntiAlias(true);
propaint.setColor(Color.GREEN);
propaint.setStyle(Paint.Style.STROKE);
propaint.setStrokeWidth(33);
propaint.setStrokeCap(Paint.Cap.ROUND);

txtpaint.setAntiAlias(true);
txtpaint.setColor(Color.BLACK);
txtpaint.setStyle(Paint.Style.FILL);
txtpaint.setStrokeWidth(1);
txtpaint.setTextSize(30);

cicpaint.setAntiAlias(true);
cicpaint.setColor(Color.GREEN);
cicpaint.setStyle(Paint.Style.FILL);
cicpaint.setStrokeWidth(1);
cicpaint.setTextSize(20);

cicwpaint.setAntiAlias(true);
cicwpaint.setColor(Color.WHITE);
cicwpaint.setStyle(Paint.Style.FILL);
cicwpaint.setStrokeWidth(1);
cicwpaint.setTextSize(20);
//保存点的位置
point.x = (int) Dp2Px(getContext(), 45);
point.y = (int) (whsize - Dp2Px(getContext(), 45));

}

将初始化方法,放入构造方法中

 public MyView(Context context)
{
super(context);
init();
}

public MyView(Context context, AttributeSet attrs)
{
super(context, attrs);
init();
}

我们这个自定义控件没有设置自有属性,大家可以*扩展,好了接下来,就是要获得尺寸。那么有两种方式,一种是在onMeasure()方法中得到控件的宽高。还有就是在onSizeChanged()方法中获得View的高宽。这次我们在onSizeChanged()方法中获得,如果对控件的测量有不熟悉的,可以看看我上一篇blog,android控件测量与布局
onsizechange()方法中怎么获得呢,请看:

 @Override
protected void onSizeChanged(int w, int h, int oldw, int oldh)
{
width = w;
height = h;
whsize = w > h ? h : w;
cx = width / 2;
cy = height / 2;
super.onSizeChanged(w, h, oldw, oldh);
}

尺寸得到之后,我们就开始显示进度了。当然通过onDraw()方法。分别画进度背景,线条,等

 @Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
drawarc(canvas);
Log.d("renk", "invar");
if (isdown)
{
drawmap(canvas);
} else
{
drawbit(canvas);
}
}

先画,进度背景及边框

private void drawarc(Canvas canvas)
{
RectF oval = new RectF(Dp2Px(getContext(), 45),
Dp2Px(getContext(), 45), whsize - Dp2Px(getContext(), 45),
whsize - Dp2Px(getContext(), 45));
//画圆弧
canvas.drawArc(oval, -180, 180, false, bgpaint);
canvas.drawArc(oval, -180, 180, false, colorpaint);
// 中心圆点
// canvas.drawCircle(cx, (int) ry + 32, 10, txtpaint);
}

下面分别讲一下, drawmap(canvas);和 drawbit(canvas);的功能,这里有个判断。isdown表示,是否按下的判断,是配合后面要用的重写ontuchevent()方法。这里先建一个点类,用于保存位置坐标

class Point{
float x;
float y;
}

开始根据touch点的坐标位置画进度,重写Touch事件,在按下,移动,放开,三个事件中,得到点的坐标,再根据点的坐标计算对应到圆弧上的坐标位置,让进度达到对应圆弧位置,完成进度的显示,在按下时,会给isdown赋值true,表示已经按下,定义了x,y,来获得touch的坐标点x轴和y轴。
同时,我们要判断,当按下时,是否在圆弧之上,考虑到有一定误差,我们会给个范围,只要touch在圆环附近15px或者其他,就判定为在圆弧之上。这样就让进度到达点击的圆弧相对应位置。这里我们写了一个方法来判断即isNearBy(x,y);

 @Override
public boolean onTouchEvent(MotionEvent event)
{
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
x = event.getX();
y = event.getY();
if (isNearBy(x, y))
{
isdown = true;
move(getx(x, y), gety(x, y));
} else
{
isdown = false;
}
break;

case MotionEvent.ACTION_MOVE:
x = event.getX();
y = event.getY();
if (isdown)
{
move(getx(x, y), gety(x, y));
}
break;
case MotionEvent.ACTION_UP:
x = event.getX();
y = event.getY();
if (isdown)
{
isdown = false;
getposition(getx(x, y), gety(x, y));
}
break;
}
return true;
}

/** 判断是否在圆环边上>15 */
private boolean isNearBy(Float x, Float y)
{
boolean is = Math.abs(whsize / 2 - Dp2Px(getContext(), 45)
- Math.sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy))) < 15 ? true
: false;
return is;
}

我们在手指移动的时候,就不断的得到坐标(x,y)然后,调用重绘方法invalidate();,让其看起来是在拖动一样,当touch事件up时,我们,就让isdown变为false。同时记录下,传递出坐标,重绘,下面是move方法。同样要判断是否在进度环上。不然,是不会拖动进度走的。

 /** 移动,进度 */
private void move(float x, float y)
{
point.x = (int) x;
point.y = (int) y;
Log.d("renk", "point:" + point.toString());
if (x >= cx * 2 - Dp2Px(getContext(), 45)
&& y >= whsize - Dp2Px(getContext(), 45))
{
return;
}
invalidate();
}

在ontouchevent事件中return,返回指,是一个boolean类型,这里我们可以写false,true,意思是,该事件被当前view消费了,不会上传父view。如果这里使用了,super.onTouchEvent(event);则表明,本view,不处理touch事件,交由上级处理。很明显,这里我们要自己处理,该事件。
我们在代码中写的数字类尺寸,单位都是px,而大家知道,android是要屏幕适配的,官方建议不要用px,代替的是dp。这样我们就要写一个转换方法。

//dp转px
public float Dp2Px(Context context, float dp)
{
final float scale = context.getResources().getDisplayMetrics().density;
return (dp * scale + 0.5f);
}
//px转dp
public float Px2Dp(Context context, float px)
{
final float scale = context.getResources().getDisplayMetrics().density;
return (px / scale + 0.5f);
}

前面,提到触摸到圆弧边缘时(进度头点的范围要比进度圆弧要宽)。怎样得到对应的圆弧上的点的坐标,然后让进度更新到位置上。这里,就调用的两个方法分别的到x,y轴坐标。
下面三个方法通过一张图来看,,当点击到C点时线判断C点离圆弧,距离,如果大于15px,这里判断,是由于手指有大小粗细,如果小于15,那就表明点在了进度弧上,就要得到,相应(cx,cy)的坐标,通过数学公式,我们可以得到∠a的度数方法getxy()可获得。然后,相似三角形得到(cx,cy)的坐标。这样,就可以在这点上画圆点在里面写数字。
Andorid自定义拖动弧形进度条

/** 保存点的角度 **/
private float temp = 0;
/** 获取角度 */
private float getxy(float x, float y)
{
float ao = (float) (Math.atan((cy - y) / (cx - x)) / Math.PI * 180);
return ao;
}

private float getx(float x, float y)
{
temp = getxy(x, y);
if (getxy(x, y) < 0)
{
return (float) (cx + (whsize / 2 - Dp2Px(getContext(), 45))
* Math.cos(getxy(x, y) * Math.PI / 180));
}
return (float) (cx - (whsize / 2 - Dp2Px(getContext(), 45))
* Math.cos(getxy(x, y) * Math.PI / 180));
}

private float gety(float x, float y)
{
if (getxy(x, y) < 0)
{
return (float) ((whsize / 2 - Dp2Px(getContext(), 45))
* Math.sin(getxy(x, y) * Math.PI / 180) + cy);
}
return (float) (cy - (whsize / 2 - Dp2Px(getContext(), 45))
* Math.sin(getxy(x, y) * Math.PI / 180));

}

当然,我们这里在加入一个新功能,限制进度*拖动。只能在规定刻度上拖动,也就是说,相当于一个拨码盘的功能。比如,现在吧,最开始的起点定为16,中点定为30.一共15个点,在圆弧上平均分配。这样显示的时候,就只能在规定的数字上停留、不会出现小数点的情况。下面,我们来分配圆弧数,也就是开始初始化的时候 private float angle = 180 / 13;下面就标记,当拖动或完成时。所在的刻度计数。

 /** 在第几个刻度上 */
int count = 0;
/** 判断是否在刻度上 **/
private void getposition(float getx, float gety)
{
/** 小圆即进度头圆点,在轨道上的 弧度 */
float mangle = 0;
point.x = (int) getx;
point.y = (int) gety;
if (temp > 0)
{
mangle = temp;
} else
{
mangle = 180 + temp;
}
if (mangle % angle > 5)
{
count = (int) (mangle / angle + 1);
} else
{
count = (int) (mangle / angle);
}
invalidate();
}

在touch事件up的的时候。我们就已经确定进度位置了,那么,就该显示当前进度的值和进度到达的弧度。

 /** 按下弹起后画点位置 **/
private void drawbit(Canvas canvas)
{
RectF oval = new RectF(Dp2Px(getContext(), 45),
Dp2Px(getContext(), 45), whsize - Dp2Px(getContext(), 45),
whsize - Dp2Px(getContext(), 45));
//画进度
canvas.drawArc(oval, -180, (int) (angle * count), false, propaint);
currentdit = (int) count + 16;
//画进度头圆圈,有边界线
canvas.drawCircle(point.x, point.y, 45, cicpaint);
canvas.drawCircle(point.x, point.y, 43, cicwpaint);
//画数值
canvas.drawText(currentdit + "", point.x - 22, point.y + 15, txtpaint);
canvas.drawText(currentdit + "", cx - Dp2Px(getContext(), 45), cy - 20,
txtpaint);
}

当没有up时,即,按下和移动的时候,会调用下面这个方法,里面有一个temp的判断,temp参数代表,上面绘制界面图的∠a度数。因为,坐标的时候,是通过∠a来计算的,当∠a大于90度时,计算结果是一个负数,需要有个转换,才能显示进度数值。

 /** 画 **/
private void drawmap(Canvas canvas)
{
RectF oval = new RectF(Dp2Px(getContext(), 45),
Dp2Px(getContext(), 45), whsize - Dp2Px(getContext(), 45),
whsize - Dp2Px(getContext(), 45));
if (temp < 0)//判断是否超过90度
{
canvas.drawArc(oval, -180, 180 + temp, false, propaint);
int a = (int) (180 + temp);
} else
{
canvas.drawArc(oval, -180, temp, false, propaint);
}
canvas.drawCircle(point.x, point.y, 45, cicpaint);
canvas.drawCircle(point.x, point.y, 43, cicwpaint);
canvas.drawText(currentdit + "", point.x - 22, point.y + 15, txtpaint);
}

前面讲过,如果想要自己自定义属性,的话,就要在values文件夹下新建一个attrs.xml.里面要设置你自己想要的属性.比如

<declare-styleable name="ProgressBar">
<attr name="roundColor" format="color"/>
<attr name="roundProgressColor" format="color"/>
<attr name="roundWidth" format="dimension"></attr>
<attr name="textColor" format="color" />
<attr name="textSize" format="dimension" />
<attr name="imageMax" format="integer"></attr>
<attr name="isProgressImage" format="boolean"></attr>
<attr name="pointRadius" format="dimension"></attr>
<attr name="pointWidth" format="dimension"></attr>
</declare-styleable>

然后,在三个参数的构造方法中引用。如下类似:

public MusicProgressBar(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);

paint = new Paint();

TypedArray mTypedArray = context.obtainStyledAttributes(attrs, R.styleable.RoundProgressBar);

//获取自定义属性和默认值
roundColor = mTypedArray.getColor(R.styleable.RoundProgressBar_roundColor, Color.RED);
roundProgressColor = mTypedArray.getColor(R.styleable.RoundProgressBar_roundProgressColor, Color.GREEN);
roundWidth = mTypedArray.getDimension(R.styleable.RoundProgressBar_roundWidth, 3);
textColor = mTypedArray.getColor(R.styleable.RoundProgressBar_textColor, Color.GREEN);
textSize = mTypedArray.getDimension(R.styleable.RoundProgressBar_textSize, 15);
max = mTypedArray.getInteger(R.styleable.RoundProgressBar_imageMax, 100);
pointRadius = mTypedArray.getDimension(R.styleable.RoundProgressBar_pointRadius, 3);
pointWidth = mTypedArray.getDimension(R.styleable.RoundProgressBar_pointWidth, 2);
//这个必须要
mTypedArray.recycle();}

上面自定义属性是可以不用的,只是,为了方便扩展,我们也可以写。就看你自己了。

这样,这个控件就完成了,当然,细心的朋友肯定会发现,这个控件,做得不够好。可能有点小问题。没有标刻度等,不过,我在这里也是抛砖引玉,给大家一个思路。你自己在来优化一下代码。最后,按照国际惯例,把源码奉上。希望大家,多多,指教。

public class MyView extends View
{


private Paint bgpaint;
private Paint colorpaint;
private Paint txtpaint;
private Paint propaint;

private int cx, cy;
private int width, height;
private int whsize;
//判断是否按下
private boolean isdown;

private float angle = 180 / 13;
private Paint cicpaint;
private Paint cicwpaint;
private int currentdit;

public MyView(Context context)
{
super(context);
init();
}

public MyView(Context context, AttributeSet attrs)
{
super(context, attrs);
init();
}

public MyView(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
init();
}

private void init()
{
bgpaint = new Paint();
colorpaint = new Paint();
txtpaint = new Paint();
propaint = new Paint();
cicpaint = new Paint();
cicwpaint = new Paint();

bgpaint.setAntiAlias(true);
bgpaint.setColor(Color.GREEN);
bgpaint.setStyle(Paint.Style.STROKE);
bgpaint.setStrokeWidth(35);
bgpaint.setStrokeCap(Paint.Cap.ROUND);

colorpaint.setAntiAlias(true);
colorpaint.setColor(Color.WHITE);
colorpaint.setStyle(Paint.Style.STROKE);
colorpaint.setStrokeWidth(33);
colorpaint.setStrokeCap(Paint.Cap.ROUND);

propaint.setAntiAlias(true);
propaint.setColor(Color.GREEN);
propaint.setStyle(Paint.Style.STROKE);
propaint.setStrokeWidth(33);
propaint.setStrokeCap(Paint.Cap.ROUND);

txtpaint.setAntiAlias(true);
txtpaint.setColor(Color.BLACK);
txtpaint.setStyle(Paint.Style.FILL);
txtpaint.setStrokeWidth(1);
txtpaint.setTextSize(30);

cicpaint.setAntiAlias(true);
cicpaint.setColor(Color.GREEN);
cicpaint.setStyle(Paint.Style.FILL);
cicpaint.setStrokeWidth(1);
cicpaint.setTextSize(20);

cicwpaint.setAntiAlias(true);
cicwpaint.setColor(Color.WHITE);
cicwpaint.setStyle(Paint.Style.FILL);
cicwpaint.setStrokeWidth(1);
cicwpaint.setTextSize(20);

point.x = (int) Dp2Px(getContext(), 45);
point.y = (int) (whsize - Dp2Px(getContext(), 45));

}

@Override
protected void onDraw(Canvas canvas)
{
super.onDraw(canvas);
drawarc(canvas);
Log.d("renk", "invar");
if (isdown)
{
drawmap(canvas);
} else
{
drawbit(canvas);
}
}



/** 按下弹起后画点位置 **/
private void drawbit(Canvas canvas)
{
RectF oval = new RectF(Dp2Px(getContext(), 45),
Dp2Px(getContext(), 45), whsize - Dp2Px(getContext(), 45),
whsize - Dp2Px(getContext(), 45));
canvas.drawArc(oval, -180, (int) (angle * count), false, propaint);
currentdit = (int) count + 16;
canvas.drawCircle(point.x, point.y, 45, cicpaint);
canvas.drawCircle(point.x, point.y, 43, cicwpaint);
canvas.drawText(currentdit + "", point.x - 22, point.y + 15, txtpaint);

canvas.drawText(currentdit + "", cx - Dp2Px(getContext(), 45), cy - 20,
txtpaint);
}

/** 画点 **/
private void drawmap(Canvas canvas)
{
RectF oval = new RectF(Dp2Px(getContext(), 45),
Dp2Px(getContext(), 45), whsize - Dp2Px(getContext(), 45),
whsize - Dp2Px(getContext(), 45));
if (temp < 0)
{
canvas.drawArc(oval, -180, 180 + temp, false, propaint);
int a = (int) (180 + temp);
// canvas.drawText(100 * a / 180 + "%", cx - Dp2Px(getContext(),
// 45), cy - 20, txtpaint);
} else
{
canvas.drawArc(oval, -180, temp, false, propaint);
// canvas.drawText(100 * (int) temp / 180 + "%", cx -
// Dp2Px(getContext(), 45), cy - Dp2Px(getContext(), 45),
// txtpaint);
}
canvas.drawCircle(point.x, point.y, 45, cicpaint);
canvas.drawCircle(point.x, point.y, 43, cicwpaint);
canvas.drawText(currentdit + "", point.x - 22, point.y + 15, txtpaint);

}

private void drawarc(Canvas canvas)
{
RectF oval = new RectF(Dp2Px(getContext(), 45),
Dp2Px(getContext(), 45), whsize - Dp2Px(getContext(), 45),
whsize - Dp2Px(getContext(), 45));
canvas.drawArc(oval, -180, 180, false, bgpaint);
canvas.drawArc(oval, -180, 180, false, colorpaint);
// 中心圆点
// canvas.drawCircle(cx, (int) ry + 32, 10, txtpaint);

// 画刻度
// canvas.save();
drawsize(canvas);
// canvas.restore();
}

/** 旋转画刻度 **/
private void drawsize(Canvas canvas)
{

}

public void getsize()
{
// startx = whsize / 2 - Math.cos(15 * Math.PI / 180) +
// Dp2Px(getContext(), 45);
// starty = whsize / 2 + Math.sin(15 * Math.PI / 180) +
// Dp2Px(getContext(), 45);
// ry = (cx - 40) / Math.cos(15 * Math.PI / 180);
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh)
{
width = w;
height = h;
whsize = w > h ? h : w;
cx = width / 2;
cy = height / 2;
//getsize();
super.onSizeChanged(w, h, oldw, oldh);
}

float x;
float y;

@Override
public boolean onTouchEvent(MotionEvent event)
{
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
x = event.getX();
y = event.getY();
if (isNearBy(x, y))
{
isdown = true;
move(getx(x, y), gety(x, y));
} else
{
isdown = false;
}
break;

case MotionEvent.ACTION_MOVE:
x = event.getX();
y = event.getY();
if (isdown)
{
move(getx(x, y), gety(x, y));
}
break;
case MotionEvent.ACTION_UP:
x = event.getX();
y = event.getY();
if (isdown)
{
isdown = false;
getposition(getx(x, y), gety(x, y));
}
Log.d("renk", "x:" + x + " y:" + y + " getx:" + getx(x, y)
+ " gety:" + gety(x, y) + " " + isNearBy(x, y));
break;
}
return true;
}

/** 在第几个刻度上 */
int count = 0;

/** 判断是否在刻度上 **/
private void getposition(float getx, float gety)
{
/** 小圆在轨道上的 弧度 */
float mangle = 0;

point.x = (int) getx;
point.y = (int) gety;
if (temp > 0)
{
mangle = temp;
} else
{
mangle = 180 + temp;
}
if (mangle % angle > 5)
{
count = (int) (mangle / angle + 1);
} else
{
count = (int) (mangle / angle);
}
invalidate();
}

Point point = new Point();

/** 移动,进度 */
private void move(float x, float y)
{
point.x = (int) x;
point.y = (int) y;
Log.d("renk", "point:" + point.toString());
if (x >= cx * 2 - Dp2Px(getContext(), 45)
&& y >= whsize - Dp2Px(getContext(), 45))
{
return;
}
invalidate();
}

/** 判断是否在圆环边上>20 */
private boolean isNearBy(Float x, Float y)
{
boolean is = Math.abs(whsize / 2 - Dp2Px(getContext(), 45)
- Math.sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy))) < 20 ? true
: false;
return is;
}

/** 保存点的角度 **/
private float temp = 0;

/** 获取角度 */
private float getxy(float x, float y)
{
float ao = (float) (Math.atan((cy - y) / (cx - x)) / Math.PI * 180);
Log.d("renk", "ao" + ao);
return ao;
}

private float getx(float x, float y)
{
Log.d("renk",
"xxx:" + whsize / 2 * Math.cos(getxy(x, y) * Math.PI / 180));
temp = getxy(x, y);
if (getxy(x, y) < 0)
{
return (float) (cx + (whsize / 2 - Dp2Px(getContext(), 45))
* Math.cos(getxy(x, y) * Math.PI / 180));
}
return (float) (cx - (whsize / 2 - Dp2Px(getContext(), 45))
* Math.cos(getxy(x, y) * Math.PI / 180));
}

private float gety(float x, float y)
{
Log.d("renk",
"yyy:" + (whsize / 2) * Math.sin(getxy(x, y) * Math.PI / 180));

if (getxy(x, y) < 0)
{
return (float) ((whsize / 2 - Dp2Px(getContext(), 45))
* Math.sin(getxy(x, y) * Math.PI / 180) + cy);
}
return (float) (cy - (whsize / 2 - Dp2Px(getContext(), 45))
* Math.sin(getxy(x, y) * Math.PI / 180));

}

public float Dp2Px(Context context, float dp)
{
final float scale = context.getResources().getDisplayMetrics().density;
return (dp * scale + 0.5f);
}

public float Px2Dp(Context context, float px)
{
final float scale = context.getResources().getDisplayMetrics().density;
return (px / scale + 0.5f);
}

}