安卓自定义可拖动弧形进度条
最近在写一个音乐app时用到了弧形进度条
刚好这段时间也在学习自定义view就自己写了一个来练练手。
先上一个截图吧!!!
(源码放在文章最后,希望一起交流一起学习)
先说一下实现思路:
通过画笔(Paint)画出我们需要的样式,然后向外暴漏设置最大进度值和当前进度值的方法
画出样式后处理处理onTouchEvent事件,通过接口回调将滑动的实时进度或手指松开时需要
设置的进度传递给Activity,让Activity去更新其他UI或一些其他操作
我们知道自定义View是使用Paint在Canvas上去画各种形状
我们可以看到我们需要画三个东西:
1、整个半圆行进度条(淡黄色部分)
2、已播放进度(橘色部分)
3、当前进度位置(小黑点)
所以我们需要定义三个画笔:
1 //用于绘制整个半圆进度条 2 private Paint barPaint; 3 //用于绘制已完成进度 4 private Paint progressPaint; 5 //勇于绘制当前进度的小圆点 6 private Paint dotPaint;
此时我们还需要定义我们进度条中的一些属性:
进度条宽度,小圆点宽度,进度条颜色,进度条最大值和当前的进度等
1 //整个半圆进度条底色 2 private int bankBackground; 3 //已完成进度颜色 4 private int progressBackground; 5 //进度条宽度 6 private int progressBarHeight; 7 //原点宽度 8 private int dotHeight; 9 //圆点颜色 10 private int dotBackground; 11 //进度条最大值 12 private float maxNum; 13 //进度条当前进度 14 private float presentNum; 15 //小圆点坐标 16 private float x=0,y=0;
有了这些我们就能绘制出一个不能拖动的进度条
如果我们需要我们的进度条可以拖动就需要我们获取点击屏幕的位置手指滑动到的位置去进行判断和计算
所以也需要定义变量去存储位置的变化
1 //滑动时的位置 2 private float moveX,moveY; 3 //点击的位置 4 private float downX,downY;
定义完需要的对像和变量后就在init()方法里实例化他们:
1 //初始化变量 2 private void init(){ 3 bankBackground=getResources().getColor(R.color.danhuang); 4 progressBackground=getResources().getColor(R.color.huangse); 5 dotBackground=getResources().getColor(R.color.black); 6 progressBarHeight=10; 7 dotHeight=10; 8 maxNum=100; 9 presentNum=0; 10 barPaint=new Paint(); 11 progressPaint=new Paint(); 12 dotPaint=new Paint(); 13 }
设置最大进度和当前进度以及接口实现
1 //向外暴漏方法实现拖动进度接口 2 public void setProgressOfTheInter(ProgressOfTheInter progressOfTheInter) { 3 this.progressOfTheInter = progressOfTheInter; 4 } 5 6 //向外暴露方法设置进度条最大值 7 public void setMaxNum(int maxNum) { 8 this.maxNum = maxNum; 9 } 10 11 //向外暴露方法设置进度 12 public void setPresentNum(int presentNum) { 13 this.presentNum = presentNum; 14 //重绘 15 invalidate(); 16 }
定义接口
1 public interface ProgressOfTheInter{ 2 //获取拖动时的进度 3 void moveProgress(int index); 4 //松开时进度 5 void upProgress(int index); 6 }
因为我们需要的是半圆行进度条所以这里需要去判断我们设置的宽高是否一致
不一致我们需要重新设置,所以就要在 onMeasure() 方法中获取我们设置的宽高然后进行判断和设置
1 // 宽高测量 2 @Override 3 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 4 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 5 //拿到宽度和高度 6 int width=MeasureSpec.getSize(widthMeasureSpec); 7 int height=MeasureSpec.getSize(heightMeasureSpec); 8 //判断设置的宽高是否相等,并设置到当前的view 9 setMeasuredDimension(width==height?height:width,width==height?height:width); 10 }
设置完宽高后我们的画布大小就确定了
剩下的就是在 onDraw(Canvas canvas) 中用画笔(Paint)去绘制我们需要的各种形状
1 @Override 2 protected void onDraw(Canvas canvas) { 3 super.onDraw(canvas); 4 //画进度条 5 onDrawProgressBar(canvas); 6 //画进度 7 onDrawProgress(canvas); 8 //画圆点 9 onDrawdDot(canvas); 10 }
onDrawProgressBar(Canvas canvas)
1 //画整个进度条 2 private void onDrawProgressBar(Canvas canvas){ 3 //定义区域 4 RectF rectF=new RectF(progressBarHeight,progressBarHeight,getWidth()-progressBarHeight,getHeight()-progressBarHeight); 5 barPaint.setAntiAlias(true); 6 barPaint.setStrokeWidth(progressBarHeight);//设置画笔宽度 7 barPaint.setColor(bankBackground);//设置颜色 8 barPaint.setStyle(Paint.Style.STROKE);//设置为实心画笔 9 barPaint.setStrokeCap(Paint.Cap.ROUND);//设置画笔画圆形样式 10 barPaint.setAntiAlias(true);//设置抗锯齿 11 canvas.drawArc(rectF,360,180,false,barPaint); 12 }
上面代码中
RectF rectF=new RectF(progressBarHeight,progressBarHeight,getWidth()-progressBarHeight,getHeight()-progressBarHeight);
这行代码是设置我们绘制的区域(矩形区域)
RectF rectF=new RectF(left,top,right,bottom);四个参数的意思
left:左侧距离画布左边框的距离
top:顶部到画布上边框的距离
right:右侧到画布左边框距离
bottom:底部到画布上边框距离
下面这行代码就是绘制图形
canvas.drawArc(rectF,360,180,false,barPaint);
我们可以看下他的源码
public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter,@NonNull Paint paint) {
super.drawArc(oval,startAngle,sweepAngle,useCenter,paint);
}
我们看到我们需要传递五个参数,下面介绍一下五个参数的含义
oval:我们定义的位于画布中的矩形区域
startAngle:为圆弧的开始角度(时钟3点的方向为0度,顺时钟方向为正)
sweepAngle:为圆弧的扫过角度(正数为顺时钟方向,负数为逆时钟方向)
useCenter:表示绘制的圆弧是否与中心点连接成闭合区域
paint:为绘制圆弧的画笔
onDrawProgress(Canvas canvas)
1 //画进度 2 private void onDrawProgress(Canvas canvas){ 3 //判断当前进度是否大于最大进度,如果大于最大进度则始终等于最大进度 4 if (presentNum>maxNum) 5 presentNum=maxNum; 6 //定义区域 7 RectF rectF=new RectF(progressBarHeight,progressBarHeight,getWidth()-progressBarHeight,getHeight()-progressBarHeight); 8 progressPaint.setAntiAlias(true); 9 progressPaint.setStrokeWidth(progressBarHeight);//设置画笔宽度 10 progressPaint.setColor(progressBackground);//设置颜色 11 progressPaint.setStyle(Paint.Style.STROKE);//设置为实心画笔 12 progressPaint.setStrokeCap(Paint.Cap.ROUND); 13 progressPaint.setAntiAlias(true);//设置抗锯齿 14 canvas.drawArc(rectF,180,-((presentNum/maxNum)*180),false,progressPaint); 15 }
上面这段代码中主要的就是
canvas.drawArc(rectF,180,-((presentNum/maxNum)*180),false,progressPaint);
这五个数中值得注意的就是第三个
-((presentNum/maxNum)*180)
负号表示逆时针方向
(presentNum/maxNum)*180的意思就是将当前进度等比例转化为角度
因为圆弧是通过角度去绘制的所有不能直接给当前进度,需要将进度等比例转换
onDrawProgress(Canvas canvas)
1 //画圆点 2 private void onDrawdDot(Canvas canvas){ 3 dotPaint.setAntiAlias(true); 4 dotPaint.setStrokeWidth(progressBarHeight);//设置画笔宽度 5 dotPaint.setColor(dotBackground);//设置颜色 6 dotPaint.setStyle(Paint.Style.FILL);//设置为实心画笔 7 dotPaint.setStrokeCap(Paint.Cap.ROUND); 8 dotPaint.setAntiAlias(true);//设置抗锯齿 9 //判断角度是否大于90度 10 //通过三角函数计算x,y坐标 11 if((Math.cos(Math.toRadians((presentNum / maxNum) * 180))) * (getWidth() / 2 - progressBarHeight / 2)>=0) { 12 x = (float) (getWidth() / 2 - (Math.cos(Math.toRadians((presentNum / maxNum) * 180))) * (getWidth() / 2 - progressBarHeight / 2)) + progressBarHeight / 2; 13 }else { 14 x = (float) (getWidth() / 2 - (Math.cos(Math.toRadians((presentNum / maxNum) * 180))) * (getWidth() / 2 - progressBarHeight / 2)) - progressBarHeight/2; 15 } 16 y=(float) ((getHeight()/2-progressBarHeight/2)+(Math.sin(Math.toRadians((presentNum/maxNum)*180)))*(getHeight()/2-progressBarHeight/2)); 17 canvas.drawCircle(x,y,(float)dotHeight,dotPaint); 18 }
上方这段代码中的问题主要是为什么要判断进度是否超过90度呢?
解决这个问题前我们首先要明确 Canvas 画圆是通过什么进行绘制的?
public void drawCircle(float cx, float cy, float radius, @NonNull Paint paint) {
super.drawCircle(cx, cy, radius, paint);
}
可以看到画圆的方法只需要传四个参数:
cx:圆心的 x 坐标
cy:圆心的 y 坐标
radius:圆的半径
paint:画笔
知道这四个参数的含义后我们就知道画圆是通过圆心坐标和半径去确定我们所画圆的位置和大小的
知道怎么绘制的我们就可以去看看为什么要判断是否大于90度了,我们通过图来看更加直观
x,y 分别是A,B圆的圆心坐标位置
getWidth() / 2 - (Math.cos(Math.toRadians((presentNum / maxNum) * 180))) * (getWidth() / 2 - progressBarHeight / 2)
的意思是通过三角函数计算出来的小红点距离画布左边框的距离
所以当进度小于总进度的一半时小圆点很坐标为
X=(getWidth() / 2 - (Math.cos(Math.toRadians((presentNum / maxNum) * 180))) * (getWidth() / 2 - progressBarHeight / 2)) - progressBarHeight / 2;
当进度大于于总进度的一半时小圆点很坐标为
X=(getWidth() / 2 - (Math.cos(Math.toRadians((presentNum / maxNum) * 180))) * (getWidth() / 2 - progressBarHeight / 2)) + progressBarHeight / 2;
写到这里我们的进度条就可以显示了,但是此时是不能拖动的,接下来就是处理onTouchEvent事件
1 @Override 2 public boolean onTouchEvent(MotionEvent event) { 3 switch (event.getAction()) { 4 //手指按住 5 case MotionEvent.ACTION_DOWN: 6 downX=event.getX(); 7 downY=event.getY(); 8 //判断按下的位置是否在小圆点附件15像素位置 9 if(rangeInDefined((int)downX,(int)x-15,(int)x+15)&&rangeInDefined((int)downY,(int)y-15,(int)y+15)) 10 return true; 11 else 12 return false; 13 //手指移动 14 case MotionEvent.ACTION_MOVE: 15 //获取当前触摸点,赋值给当前进度 16 setMotionProgress(event); 17 return true; 18 //手指松开 19 case MotionEvent.ACTION_UP: 20 if (progressOfTheInter!=null) 21 progressOfTheInter.upProgress((int)presentNum); 22 return false; 23 } 24 return true; 25 }
先判断用户是否在圆点附近按下,不是则不做滑动处理,是的话则交给setMotionProgress进行计算,
松开时通过接口将此时最新进度传递给Activity进行其他处理
setMotionProgress(MotionEvent event)
1 //拖动时改变进度值 2 private void setMotionProgress(MotionEvent event) { 3 //获取当前触摸点 4 moveX = (int) event.getX(); 5 moveY=(int)event.getY(); 6 //圆心坐标((getWidth()/2 ,(getHeight()/2) 7 Log.d(TAG,"圆心坐标:("+String.valueOf(getWidth() / 2)+","+getHeight()/2+")"); 8 double longX=Math.pow((moveX-getWidth() / 2),2); 9 double longY=Math.pow((moveY-getHeight() / 2),2); 10 double longDuibian=moveY-(getWidth() / 2); 11 double asin=Math.asin(longDuibian/(Math.sqrt(longX+longY))); 12 //弧度转化为角度 13 double angle=Math.toDegrees(asin); 14 //判断手指移动的x坐标是否大于半径(进度超过90度) 15 if (moveX>getWidth()/2) 16 angle=180-angle; 17 if (angle>=180) 18 angle=180; 19 if (angle<0) 20 angle=0; 21 //角度等比例转换为进度 22 presentNum=(int)(angle*maxNum)/180; 23 Log.d(TAG,"设置进度:"+presentNum); 24 //progressOfTheInter接口是否为空 25 if(progressOfTheInter!=null) 26 progressOfTheInter.moveProgress((int)presentNum); 27 //重绘 28 invalidate(); 29 }
这里主要说明一下上面代码的逻辑
这里的A为滑动时的触摸点
通过两点的距离公式:
|OA|=√(xo-xA)2+(yo-yA)2
代码变量说明:
longX=(xo-xA)2
longY=(yo-yA)2
longDuibian为触摸焦点A到O所在水平线的垂直距离
通过两点的距离公式求出斜边OA后就可以通过 ( sinO=对边/斜边 )求出触摸点到圆心O所在水平线的正玄值
然后通过反三角函数求出此时的角度,因为此时求出的角度始终是相对与圆心O所在的水平线角度,所以需要
所以需要判断触摸的点是否大于圆心到画布最左边的距离(既此时进度大于90度),超过则用180减去此时角度
(既求出O点左边为0度的时角度),不超过不做处理,求出角度后等比例转换为进度值,然后通过接口回调的
方式将此时的进度传递给Activity,重回view。
这时我们的可拖动进度条就实现了。
使用方式
1 <edu.cdp.rjfy.popular.widget.HalfProgressBar 2 android:id="@+id/progressBar" 3 android:layout_width="195dp" 4 android:layout_height="195dp" 5 android:layout_centerHorizontal="true" 6 android:layout_centerVertical="true" />
全部代码:
HalfProgressBar
public class HalfProgressBar extends View { private String TAG="HalfProgressBar"; //进度条底色 private int bankBackground; //进度颜色 private int progressBackground; //进度条宽度 private int progressBarHeight; //原点宽度 private int dotHeight; //圆点颜色 private int dotBackground; //进度条最大值 private float maxNum; //进度条当前进度 private float presentNum; //画笔 private Paint barPaint; private Paint progressPaint; private Paint dotPaint; //滑动时的位置 private float moveX,moveY; //点击的位置 private float downX,downY; //小圆点坐标 private float x=0,y=0; private ProgressOfTheInter progressOfTheInter; //向外暴漏方法实现拖动进度接口 public void setProgressOfTheInter(ProgressOfTheInter progressOfTheInter) { this.progressOfTheInter = progressOfTheInter; } //向外暴露方法设置进度条最大值 public void setMaxNum(int maxNum) { this.maxNum = maxNum; } //向外暴露方法设置进度 public void setPresentNum(int presentNum) { this.presentNum = presentNum; //重绘 invalidate(); } //向外暴漏方法获取当前进度 public float getPresentNum() { return presentNum; } //new时调用 public HalfProgressBar(Context context) { this(context,null); } //xml文件调用 public HalfProgressBar(Context context, @Nullable AttributeSet attrs) { this(context,attrs,0); } public HalfProgressBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } //初始化变量 private void init(){ bankBackground=getResources().getColor(R.color.danhuang); progressBackground=getResources().getColor(R.color.huangse); dotBackground=getResources().getColor(R.color.black); progressBarHeight=10; dotHeight=10; maxNum=100; presentNum=0; barPaint=new Paint(); progressPaint=new Paint(); dotPaint=new Paint(); } // 宽高测量 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //拿到宽度和高度 int width=MeasureSpec.getSize(widthMeasureSpec); int height=MeasureSpec.getSize(heightMeasureSpec); setMeasuredDimension(width==height?height:width,width==height?height:width); } // 画笔 @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //画进度条 onDrawProgressBar(canvas); //画进度 onDrawProgress(canvas); //画圆点 onDrawdDot(canvas); } //画整个进度条 private void onDrawProgressBar(Canvas canvas){ //定义区域 RectF rectF=new RectF(progressBarHeight,progressBarHeight,getWidth()-progressBarHeight,getHeight()-progressBarHeight); barPaint.setAntiAlias(true); barPaint.setStrokeWidth(progressBarHeight);//设置画笔宽度 barPaint.setColor(bankBackground);//设置颜色 barPaint.setStyle(Paint.Style.STROKE);//设置为实心画笔 barPaint.setStrokeCap(Paint.Cap.ROUND); barPaint.setAntiAlias(true); canvas.drawArc(rectF,360,180,false,barPaint); } //画进度 private void onDrawProgress(Canvas canvas){ if (presentNum>maxNum) presentNum=maxNum; //定义区域 RectF rectF=new RectF(progressBarHeight,progressBarHeight,getWidth()-progressBarHeight,getHeight()-progressBarHeight); progressPaint.setAntiAlias(true); progressPaint.setStrokeWidth(progressBarHeight);//设置画笔宽度 progressPaint.setColor(progressBackground);//设置颜色 progressPaint.setStyle(Paint.Style.STROKE);//设置为实心画笔 progressPaint.setStrokeCap(Paint.Cap.ROUND); progressPaint.setAntiAlias(true); canvas.drawArc(rectF,180,-((presentNum/maxNum)*180),false,progressPaint); } //画圆点 private void onDrawdDot(Canvas canvas){ dotPaint.setAntiAlias(true); dotPaint.setStrokeWidth(progressBarHeight);//设置画笔宽度 dotPaint.setColor(dotBackground);//设置颜色 dotPaint.setStyle(Paint.Style.FILL);//设置为实心画笔 dotPaint.setStrokeCap(Paint.Cap.ROUND); dotPaint.setAntiAlias(true); //判断角度是否大于90度 //通过三角函数计算x,y坐标 if((Math.cos(Math.toRadians((presentNum / maxNum) * 180))) * (getWidth() / 2 - progressBarHeight / 2)>=0) { x = (float) (getWidth() / 2 - (Math.cos(Math.toRadians((presentNum / maxNum) * 180))) * (getWidth() / 2 - progressBarHeight / 2)) + progressBarHeight / 2; }else { x = (float) (getWidth() / 2 - (Math.cos(Math.toRadians((presentNum / maxNum) * 180))) * (getWidth() / 2 - progressBarHeight / 2)) - progressBarHeight/2; } y=(float) ((getHeight()/2-progressBarHeight/2)+(Math.sin(Math.toRadians((presentNum/maxNum)*180)))*(getHeight()/2-progressBarHeight/2)); canvas.drawCircle(x,y,(float)dotHeight,dotPaint); } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { //手指按住 case MotionEvent.ACTION_DOWN: downX=event.getX(); downY=event.getY(); //判断按下的位置是否在小圆点附件15像素位置 if(rangeInDefined((int)downX,(int)x-15,(int)x+15)&&rangeInDefined((int)downY,(int)y-15,(int)y+15)) return true; else return false; //手指移动 case MotionEvent.ACTION_MOVE: //获取当前触摸点,赋值给当前进度 setMotionProgress(event); return true; //手指松开 case MotionEvent.ACTION_UP: if (progressOfTheInter!=null) progressOfTheInter.upProgress((int)presentNum); return false; } return true; } //拖动时改变进度值 private void setMotionProgress(MotionEvent event) { //获取当前触摸点,赋值给当前进度 moveX = (int) event.getX(); moveY=(int)event.getY(); //圆心坐标((getWidth()/2 ,(getHeight()/2) double longX=Math.pow((moveX-getWidth() / 2),2); double longY=Math.pow((moveY-getHeight() / 2),2); double longDuibian=moveY-(getWidth() / 2); double asin=Math.asin(longDuibian/(Math.sqrt(longX+longY))); //弧度转化为角度 double angle=Math.toDegrees(asin); if (moveX>getWidth()/2) angle=180-angle; if (angle>=180) angle=180; if (angle<0) angle=0; //角度等比例转换为进度 presentNum=(int)(angle*maxNum)/180; //progressOfTheInter接口是否为空 if(progressOfTheInter!=null) progressOfTheInter.moveProgress((int)presentNum); invalidate(); } private boolean rangeInDefined(int current, int min, int max) { return Math.max(min, current) == Math.min(current, max); } public interface ProgressOfTheInter{ //获取拖动时的进度 void moveProgress(int index); //松开时进度 void upProgress(int index); } }