自定义View实现拖动条SeekBar
本着挤一点写一点的原则
这几天在做智能家具的一套设备的ui图,发现很多的视图都要使用自定义view,这几天研究了下,发现也是很简单,就是考考大家的画图能力,博主画图贼六~ 都是日漫惹的祸…
- 分析实现步骤
- 画图模块拆分
- 注意的细节
- 具体实现
- 代码
- 效果图
- 完成后的总结
分析实现步骤
有这样一个需求,要写一个拱桥似的空调遥控器,具体的ui稿子就不给大家了看了,来看一篇网上盗的图,和我画的图
这种不规则的按钮,你不可能全部去贴图片吧,毕竟有那么多点击事件的膜外区域,今天我们做的是第二张图的一个控件,要求拖动红色的小球去控制空调的度数,拖拽来增加,大家一定就想到了SeekBar,我擦!他么的 他是弯仔码头(其实他比这个还要复杂,不过挺好看的,可惜不能贴图)那么我么就去解决它把。
画图分析
图我们可以简单分析一波,注意我将画布的中心移动到了控件区域的中间,本来画布的中心应该在左上角
1.首先去掉圆形,我们要画两个区域,一个是黄色的已经滚动过的区域,一个是蓝色的暂时还没有滚动到的区域这样很简单我们只需要画两个path路径,并将他们链接起来即可。(如果你不知道path是什么,后面会讲)
2.圆的部分,那么我们要知道圆心的位置,圆心的位置不就是大圆弧与小圆弧之间的连接线的中心点。
3.圆心的位置是要变的,而且要根据手势来变换,其实也很简单,下面会给大家分析。
如上图,
AB为大圆圆弧,cd为小圆圆弧,ef为中间小球的圆心移动的圆弧,我们设置大圆半径为radioBig,小圆半径为radioSmall,那圆心移动的半径为radioCircle=(radioBig+radioSmall)/2(这个就不用说明了吧,小学操作来的)
他们的扫过角度都是90度如图oa,ob的夹角,这是没有加上小圆的操作,我们现在把小圆加上。
如上图
G点为圆心,在我们点击小球并移动手指时,有一条辅助线og他会将这个大的的圆弧分成两边,一个也就是我们说的左边的圆弧,一个是右边的,那么我们只需要知道夹角就可以画出这两个圆弧。我们设一个点是小球当前的圆心点circleXY ,设辅助线与x正轴的夹角为dege_x,辅助线与负y轴的夹角为dege_y,辅助线与左边起点边的oa的夹角为sweetRange(也就是扫过的角度)那么分三种情况:
1.circleXY 的横坐标小于0 :那么 ∠dege_y=∠go-y ∠dege_x= ∠dege_y+90° ∠sweetRange=45°-∠dege_y
2.circleXY 的横坐标等于0 :那么 ∠dege_y=0° ∠dege_x= 90° ∠sweetRange=45°
3.circleXY 的横坐标大于0 :那么 ∠dege_y=∠go-y ∠dege_x= 90°-∠go-y ∠sweetRange=45°+∠go-y
角度的读书都可以用圆心坐标的x、y值来求解(具体就是tan转化为角度,初中老师教的,不会的话。恭喜你)
具体的画图
由于我用的是path的addArc和arcTo方法,所以如果让这两段圆弧刚好能首尾相连,需要按照以上两种箭头的方式来画两段圆弧,不然就会形成错误的图像,因为绘制的过程都是有方向的,如果你不按照一定的顺序完成图像,就会有错误的结果。至于path的这两个方法有什么区别以及怎么使用,请看这位大神的博客.
好了下面会讲到如何去得到圆心坐标:
圆心的轨迹路径圆弧path我们是知道的,那么我们只需要得到相应的测量方法就可以只到每一个对应的x的y坐标,那么圆心的坐标也就出来了,提供的方法是PathMeasure,如果你不知道如何使用,可以去看博客,这里我也简单说下这些方法的作用:
//新建一个圆心轨迹的路径
pathLine=new Path();
//为将轨迹圆弧增加进去
pathLine.addArc(new RectF(-lineRadio,-lineRadio,lineRadio,lineRadio),225,90);
//创建一个测量轨迹的方法,并将规矩放入,false 表示不为测量的path默认close
PathMeasure measure = new PathMeasure(pathLine,false);
//获取某一点的坐标以及方向tan值(此处tan传入0)
//第一个参数是距离路径起始点的距离
//第二个参数是将测量回来的坐标放入的参数
measure.getPosTan(measure.getLength(),circleXY,null);
还有一个方法就是Region可以表示为记录所画的区域,我们要记录小圆的区域,以测量我们在移动中的x轴坐标的变化。如果点击了小圆,且开始移动了,我们就将手势的x轴进行传递。
我们需要动态的知道,当前移动的手势的横坐标,然后计算他到开始点的距离以及手势的最大移动距离,然后根据比例求出当前的圆心应该距离原始点的距离,从而求出这点的坐标。(听的有点晕?,看图说话)
如上图,起始点圆心坐标的横坐标为xa,当前移动到的点的横坐标为xb,圆心坐标的终点横坐标为xc,
那么当前b距离a点的圆弧的长度为(xb–xa)/(xc-xa)*ac圆弧的长度,那么根据getPosTan就可以得到当前的圆心坐标,好了到此为止一切都搞定了。
源代码
//简单的自定义的进度条画法,我叫它空调view - -
public class KongTiaoView extends View {
//画布的大小
private int mHeight,mWidth;
//矩阵
private Matrix matrix;
//三个圆弧半径
private float bigRadio=150f,normalRadio=130f,lineRadio=140;
//四种绘制路径
private Path pathLeft,pathRight,pathCircle,pathLine;
//一些记录点的变量
private float [] circleXY=new float[2],touchXY= new float[2],curentXY=new float[2],startXY=new float[2],endXY=new float[2];
//画笔
private Paint paintCircle,paintLeft,paintRight;
//画出来的区域以及截取的区域
private Region regionCircle,region;
//当前圆心坐标距离最左边的距离
private float whereLen=-1f;
//标记的总横坐标长度
private float xLength=0f;
//小圆是否被点击
private boolean isClick=false;
//测量路径的方法
private PathMeasure measure;
public KongTiaoView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
matrix=new Matrix();
initPaint();
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
getParent().requestDisallowInterceptTouchEvent(true);
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
touchXY[0]=event.getRawX();
touchXY[1]=event.getRawY();
matrix.mapPoints(touchXY);
if(regionCircle.contains((int) touchXY[0],(int) touchXY[1])){
isClick=true;
}
break;
case MotionEvent.ACTION_MOVE:
touchXY[0]=event.getRawX();
touchXY[1]=event.getRawY();
matrix.mapPoints(touchXY);
if(isClick){
curentXY=touchXY;
synchronized (this) {
if (curentXY[0] > endXY[0]) {
whereLen = endXY[0] - startXY[0];
} else if (curentXY[0] >= startXY[0] && curentXY[0] <= endXY[0]) {
whereLen = curentXY[0] - startXY[0];
} else {
whereLen = 0;
}
}
}
invalidate();
break;
case MotionEvent.ACTION_UP:
isClick=false;
break;
default:break;
}
return true;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.translate(mWidth/2,mHeight/2);
matrix.reset();
if(matrix.isIdentity()){
canvas.getMatrix().invert(matrix);
}
initPath(region);
drawLeft(canvas);
drawRight(canvas);
drawCircle(canvas);
}
//画圆
private void drawCircle(Canvas canvas) {
canvas.drawPath(pathCircle,paintCircle);
}
//画右边的圆弧
private void drawRight(Canvas canvas) {
canvas.drawPath(pathRight,paintRight);
}
//画左边的
private void drawLeft(Canvas canvas) {
canvas.drawPath(pathLeft,paintLeft);
}
//初始化画笔
private void initPaint() {
paintCircle=new Paint();
paintCircle.setStyle(Paint.Style.FILL);
paintCircle.setAntiAlias(true);
paintCircle.setColor(Color.RED);
paintCircle.setStrokeWidth(2);
paintLeft=new Paint();
paintLeft.setColor(Color.YELLOW);
paintLeft.setAntiAlias(true);
paintLeft.setStyle(Paint.Style.FILL);
paintLeft.setStrokeWidth(2);
paintRight=new Paint();
paintRight.setColor(Color.BLUE);
paintRight.setStyle(Paint.Style.FILL);
paintRight.setAntiAlias(true);
paintRight.setStrokeWidth(2);
}
//初始化路径
private void initPath(Region region) {
pathLine=new Path();
pathLine.addArc(new RectF(-lineRadio,-lineRadio,lineRadio,lineRadio),225,90);
measure = new PathMeasure(pathLine,false);
startXY[0]= (float) (-Math.sqrt(2)/2*140);
startXY[1]= startXY[0];
endXY[0]= -startXY[0];
endXY[1]=startXY[0];
xLength= endXY[0]-startXY[0];
System.out.println("---------------------------"+whereLen);
if(whereLen!=-1){
measure.getPosTan(whereLen/xLength* measure.getLength(),circleXY,null);
}else {
//circleXY=startXY这里为什么不这样?!骚年你去试试
circleXY=new float[]{startXY[0],startXY[1]};
}
pathCircle=new Path();
pathCircle.addCircle(circleXY[0],circleXY[1],10, Path.Direction.CW);
regionCircle=new Region();
regionCircle.setPath(pathCircle,region);
pathLeft=new Path();
double dege_x;//扫描的边与x正轴的夹角
double dege_y;//扫描边与y负轴的夹角
double sweetRange;//与左边边界的角度
if(circleXY[0]>0){//圆已经走到右边
dege_x=Math.toDegrees(Math.atan(Math.abs(circleXY[1]/circleXY[0])));
dege_y=Math.toDegrees(Math.atan(Math.abs(circleXY[0]/circleXY[1])));
sweetRange=dege_y+45;
}else if (circleXY[0]<0){//圆在左边
dege_y=Math.toDegrees(Math.atan(Math.abs(circleXY[0]/circleXY[1])));
dege_x=dege_y+90;
sweetRange=45-dege_y;
}else {//圆在0
dege_x=90;
dege_y=0;
sweetRange=45;
}
pathLeft.addArc(new RectF(-bigRadio,-bigRadio,bigRadio,bigRadio),(float)-dege_x,-(float)sweetRange);
pathLeft.arcTo(new RectF(-normalRadio,-normalRadio,normalRadio,normalRadio),225,(float)sweetRange);
pathLeft.close();
pathRight=new Path();
pathRight.addArc(new RectF(-bigRadio,-bigRadio,bigRadio,bigRadio),-45, (float) (sweetRange-90));
pathRight.arcTo(new RectF(-normalRadio,-normalRadio,normalRadio,normalRadio),(float)-dege_x, (float) (90-sweetRange));
pathRight.close();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mHeight=h;
mWidth=w;
//区域的截取大小就为画布的大小
region=new Region(-w,-h,w,h);
}
}
代码很简单主要是过程,这个代码是我写的初稿,没有优化句柄,还可以有很多封装,只是写下思路,给需要的朋友参考下
效果图
抠脚来的不喜勿喷
大家有什么好的控件设计也可以私聊哦,目前就在搞这一块
很有趣