Android 仪表盘View
2016-09-02 20:53
736 查看
导语
这里展示的View估计项目中多半是用不到的,只是用来加深理解的。文章末尾会有全部的代码,如果想研究可以复制过去直接运行,不需要额外的资源。先看效果:
这里指针是通过手指来改变方向的,并不能通过数字参数来改变,如果需要,可以更改相应的代码。
需要的数学知识
理论的涉及也非常简单,如下所示:在坐标系中,一个点与原点连线与X轴的正切值 tan = 点的纵坐标 ÷ 点的横坐标
在每一个象限中,正切函数是单调函数;如图所示:
绘制流程
绘制由线段组成的圆弧
利用线段的旋转来绘图
绘制上图有多种方法,首先介绍一种简单的方法:将线段旋转多个角度,这样可以绘制出一个圆弧型:
private int width; private int height; @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.translate(width / 2, height / 2); Paint mPaint = new Paint(); mPaint.setStrokeWidth(5); for (int i = 0; i <= 360; i += 5) { // 绘制圆形之间的连接线 canvas.drawLine(0, 120, 0, 200, mPaint); canvas.rotate(10); } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); width = w; height = h; }
上述代码的执行效果:
虽然丑了点,但可以说明问题。不过这样做,我们缺乏对弧形的控制。例如:如何实现圆弧呢?是不是要手动计算起始坐标,旋转的角度,扫过的角度等各种各样的问题?因为怕麻烦,这个方案就被我华丽的抛弃了。
利用两个同心圆来绘制
思路:
从圆心发射一条射线出来,与两个圆相交于点A和点B,链接A与B,就可以划出一条我们想要的线段。
均匀的发射多条射线,我们就可以得到一个由线段组成的圆弧。
如果两个圆圈是圆弧的话,就可以达到我们所要的效果
所以,最终确定的步骤为:
画一个大圆弧
画一个缩小版的小圆弧
均匀地在两个圆弧之间画线段
用动态图来展示下:
相关代码比较多,在文章末尾已经贴出来了(88-172行,代码中有后续的细节处理,需要甄别下相关的代码),这里只是写下思路,不再重复贴代码了
画个一个长度固定、原点确定,方向随着手指变化的指针
这步要实现的效果如上图所示
假设,之前的指针为OZ,现在我们用手指触摸了点A,这时我们希望指针变为OB,那么,该如何实现呢?
获取A点的坐标(通过onTouchEvent()可以获取到)
画取线段OA(O点为(0,0),所以可以画取)
通过测量OA,可以利用PathMeasure.getPosTan()来获取B点的坐标(指针的长度是固定的)
在Cavas中画OB线段
如果我们触摸点为X,距离过短怎么办呢?
链接OX,并用MeasurePath来测量OX的长度,以及X的坐标(a,b)
计算OY与OX的比例 R = OY ÷ OX
计算Y点的坐标 x = a × R, y = b × R
在Cavas中画OY线段
相关代码同样比较多,在文章末尾已经贴出来了(179-242行,代码中有后续的细节处理,需要甄别下相关的代码),这里只是写下思路,不再重复贴代码了
处理越界的情况
上图情况是我们不想看到。如果指针偏到最右边,就不能再往下偏了;左边同理。这个时候,就需要想到tan函数的性质:
在每一个象限中,正切函数是单调函数
说明下:
在第二象限中,当前的tan值小于边界OA的tan值a时,说明此时是在边界外面;如果大于a,说明在边界里面
在第一象限中,当前的tan值大于OB的tan值b时,说明在边界外面;如果小于b,说明在边界里面
知道上述知识后,就非常好处理,具体过程如下:
在绘制弧形时,记录下左侧边界的tan值和右侧边界的tan值(下面代码129-151行)
在第一象限和第二象限时,记录下当前位置的tan值,并与边界的tan值进行比较来判断是否在边界中:(下面代码214-233行)
在边界中,不做处理
在边界外,指针根据情况指向点A或者点B
绘制出相应的指针
再具体的细节我在后面的代码中由详细的注释,各位可以看看
相关代码
/** * Created by Kevin on 2016/8/31. * * 需要费脑的地方: * 1.绘制多条线段组成弧形 * 2.指针跟随着手指方向且长度确定 * 3.指针的指向不能越过仪表盘 */ public class LinearCircle extends View { private int width; private int height; private Paint outerCirclePaint;//外层圆的画笔 private Paint innerCirclePaint;//内层圆的画笔 private Paint linePaint;//线段画笔 private Paint arrowPaint;//指针画笔 private Path outerCirclePath;//外层圆的Path private Path innerCirclePath;//内层圆的Path private Path linePath;//线段的Path private Path arrowPath;//指针的Path private Path measureArrowPath;//arrowPath借助该Path来保持一定的长度 private RectF outRectF;//用于绘制外层圆 private RectF innerRectF;//用于绘制内层圆 private int count = 80;//画count根线 private static int outerR = 200;//外部圆环的半径 private static int innerR = (int) (200 * 0.618f);//内部圆环的半径 private int shortageAngle = 140;//缺失的部分的角度 private int startAngle;//开始的角度 private int sweepAngle;//扫过的角度 private float[] leftEndPoint;//左侧边界的坐标 private float[] rightEndPoint;//右侧边界的坐标 private float leftEndTan;//左侧边界的tan值 private float rightEndTan;//右侧边界的tan值 private float nowX = 0;//触摸位置的横坐标 private float nowY = 0;//触摸位置的纵坐标 private static float percent = 0.9f;//指针与内层圆的比值 private float arrowLength = innerR * percent;//指针的长度 private PathMeasure arrowMeasure;//用于指针的测量 public LinearCircle(Context context) { super(context); initPaint(); initAngle(); } public LinearCircle(Context context, AttributeSet attrs) { super(context, attrs); initPaint(); initAngle(); } public LinearCircle(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initPaint(); initAngle(); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); width = w; height = h; //让指针一开始指向正上方 nowX = 0; nowY = -1; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.translate(width / 2, height / 2); drawOuterCircle(); drawInnerCircle(); drawLine(canvas); drawArrow(canvas); } /** * 外层圆圈 */ private void drawOuterCircle() { //一般绘制圆圈的方法,不做介绍了 outerCirclePath = new Path(); if (outRectF == null) { outRectF = new RectF(-outerR, -outerR, outerR, outerR); } outerCirclePath.addArc(outRectF, startAngle, sweepAngle); } /** * 内层圆圈 */ private void drawInnerCircle() { //一般绘制圆圈的方法,不做介绍了 innerCirclePath = new Path(); if (innerRectF == null) { innerRectF = new RectF(-innerR, -innerR, innerR, innerR); } innerCirclePath.addArc(innerRectF, startAngle, sweepAngle); } /** * 画直线,组成一个类似于弧形的形状 * * @param canvas */ private void drawLine(Canvas canvas) { linePath = new Path(); //用于外层圆的测量 PathMeasure outMeasure = new PathMeasure(outerCirclePath, false); float outlength = outMeasure.getLength(); float[] outPos = new float[2]; //用于内层圆的测量 PathMeasure inMeasure = new PathMeasure(innerCirclePath, false); float inlength = inMeasure.getLength(); float[] inPos = new float[2]; //确定左侧末尾的坐标以及tan值 if (leftEndPoint == null) { leftEndPoint = new float[2]; //通过getPosTan拿到内层圆的左侧末尾坐标 inMeasure.getPosTan(0, leftEndPoint, null); //因为指针要短一点;所以x,y都乘以percent才是指针真正的左侧末尾坐标 leftEndPoint[0] = leftEndPoint[0] * percent; leftEndPoint[1] = leftEndPoint[1] * percent; //确定指针在左侧末尾时的tan值 leftEndTan = leftEndPoint[1] / leftEndPoint[0]; } //确定右侧末尾的坐标以及tan值 if (rightEndPoint == null) { rightEndPoint = new float[2]; //通过getPosTan拿到内层圆的右侧末尾坐标 inMeasure.getPosTan(inlength, rightEndPoint, null); //因为指针要短一点;所以x,y都乘以percent才是指针真正的右侧末尾坐标 rightEndPoint[0] = rightEndPoint[0] * percent; rightEndPoint[1] = rightEndPoint[1] * percent; //确定指针在右侧末尾时的tan值 rightEndTan = rightEndPoint[1] / rightEndPoint[0]; } //用来画多条线段,组成弧形 for (int i = 0; i <= count; i++) { //外层圆当前的弧长 float outNowLength = outlength * i / (count * 1.0f); //当前弧长下对应的坐标outPos outMeasure.getPosTan(outNowLength, outPos, null); //内层圆当前的弧长 float inNowLength = inlength * i / (count * 1.0f); //当前弧长下对应的坐标inPos inMeasure.getPosTan(inNowLength, inPos, null); //moveTo到内层圆弧上的点 linePath.moveTo(outPos[0], outPos[1]); //lineTo到外层圆弧上的点 linePath.lineTo(inPos[0], inPos[1]); canvas.drawPath(linePath, linePaint); } } /** * 绘制指针 * * @param canvas */ private void drawArrow(Canvas canvas) { //measureArrowPath只专门用来做计算的,不绘制(当然也可以不用多创建这个对象,直接用arrowPath来完成测量,绘制工作; //这里是为了任务单一,做了区分) measureArrowPath = new Path(); //指针最终是由arrowPath来绘制的 arrowPath = new Path(); arrowPath.reset(); measureArrowPath.reset(); //用来封装指针的末尾坐标 float[] endPoint = new float[2]; //指针的起始坐标为原点,也就是(0,0) measureArrowPath.moveTo(0, 0); //指向手指目前的位置 measureArrowPath.lineTo(nowX, nowY); //arrowMeasure用来测量原点到手指位置的线段 arrowMeasure = new PathMeasure(measureArrowPath, false); //触摸位置与原点的长度 float nowLineLength = arrowMeasure.getLength(); //距离原点过近(也就是长度不够长)的处理 if (nowLineLength < arrowLength) { //计算需要扩大的倍数(固定长度 ÷ 当前长度) float expand = arrowLength / (nowLineLength); //重置数据,并测量新数据 measureArrowPath.reset(); measureArrowPath.moveTo(0, 0); measureArrowPath.lineTo(nowX * expand, nowY * expand); arrowMeasure = new PathMeasure(measureArrowPath, false); } //测量指针末尾的坐标(指针在measureArrowPath这条线段上,且小于等于measureArrowPath线段的长度; // 通过getPosTan()来确定线段在长度为arrowLength时的坐标位置) arrowMeasure.getPosTan(arrowLength, endPoint, null); //第一象限的处理 if (endPoint[0] > 0 && endPoint[1] > 0) { //右下角的情况处理 double nowTan = endPoint[1] / endPoint[0]; //当前触摸位置的tan值大于边界的tan值,表示手指目前在左侧边界的下方 if (nowTan > rightEndTan) { endPoint[0] = rightEndPoint[0]; endPoint[1] = rightEndPoint[1]; } } //第二象限的处理 if (endPoint[0] < 0 && endPoint[1] > 1) { //左下角的情况处理 double nowTan = endPoint[1] / endPoint[0]; //当前触摸位置的tan值小于边界的tan值,表示手指目前在右侧边界的下方 if (nowTan < leftEndTan) { endPoint[0] = leftEndPoint[0]; endPoint[1] = leftEndPoint[1]; } } //这里默认了第三、第四现象一般没有限制;如果圆弧的缺口过大,需要处理下;方式与上面的相似 //这时,指针的末尾位置最终确定了,可以绘制了 arrowPath.moveTo(0, 0); arrowPath.lineTo(endPoint[0], endPoint[1]); canvas.drawPath(arrowPath, arrowPaint); } //通过触摸等事件改变指针的指向 @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_MOVE: case MotionEvent.ACTION_UP: nowX = event.getX(); nowY = event.getY(); break; } //nowX和nowY是以左上角为原点的坐标系,这里进行了平移 nowX = nowX - width / 2; nowY = nowY - height / 2; invalidate(); return true; } /** * 初始化画笔 */ private void initPaint() { if (outerCirclePaint == null) { outerCirclePaint = new Paint(); outerCirclePaint.setStyle(Paint.Style.STROKE); outerCirclePaint.setColor(Color.BLACK); } if (innerCirclePaint == null) { innerCirclePaint = new Paint(); innerCirclePaint.setStyle(Paint.Style.STROKE); outerCirclePaint.setColor(Color.BLACK); } if (linePaint == null) { linePaint = new Paint(); linePaint.setStyle(Paint.Style.STROKE); linePaint.setStrokeWidth(4); linePaint.setColor(0xff1d8ffe); } if (arrowPaint == null) { arrowPaint = new Paint(); arrowPaint.setStyle(Paint.Style.FILL_AND_STROKE); arrowPaint.setColor(Color.RED); arrowPaint.setStrokeWidth(4); } } /** * 根据shortageAngle来调整圆弧的角度 */ private void initAngle() { sweepAngle = 360 - shortageAngle; startAngle = 90 + shortageAngle / 2; } }
结语
曾经让人烦透的数学还是有点用处的。转载请标明出处http://blog.csdn.net/qq_26411333/article/details/52399831
相关文章推荐
- (OK) New location of last_kmsg on Android 6.0 and above: /sys/fs/pstore/console-ramoops
- Android 5.0 之TabLayout
- Android之ndk之gdb调试
- TextView的autoLink属性
- Android侧拉菜单实现
- Android Fragment 你应该知道的一切
- Android 5.0 之CardView
- Android如何通过TextView实现超链接的跳转
- Android仿淘宝倒计时
- android的消息机制
- Android中跨进程通信
- HandlerThread详解
- Fresco的使用
- Android之主活动的创建
- Universal-Image-Loader的使用
- Android基于环信SDK开发IM即时聊天(二)
- [Android] 内存溢出的原因和解决方案
- 写给VR手游开发小白的教程:(五)Cardboard插件与Android之间的通信交互
- 带着疑惑走进Dagger2
- android5.0 RecyclerView 应用