Android 自定义贝塞尔曲线工具
之前在学习贝塞尔曲线的相关内容,在查找相关资料的时候发现网上的资料重复的太多了,而且因为android的canvas只提供了quadTo,cubicTo两种方法来绘制二阶和三阶的贝塞尔曲线.在线的贝塞尔曲线绘制网站也很少,(在这提供一个在线贝塞尔曲线 的网站,根据网上的资料整理的),而在android手机中缺没有类似的工具,在设计或者使用贝塞尔曲线的时候增加了很多的工作,刚好在学习相关的知识,就做了一个较为完善的android端的贝塞尔曲线工具.
贝塞尔曲线 基本的贝塞尔曲线的知识就不多说了,有兴趣的可以参考下我之后会完成的贝塞尔曲线的记录
其实理解贝塞尔曲线十分容易,可以将其理解为一种递归的形式.根据比例系数计算当前线段中的点,得到所有点之后再按照顺序连接线段,重复以上步骤,直至只剩下一个点,此点就在贝塞尔曲线中,计算各个比例系数下的点,这些点的集合就是贝塞尔曲线.
基本功能 绘制常见的贝塞尔曲线 可以绘制常见的二阶,三阶贝塞尔曲线
绘制多阶的贝塞尔曲线 可以绘制不常见的贝塞尔曲线
开启/关闭辅助线 可以开启不同颜色层级的辅助线段
绘制无上限制的贝塞尔曲线 突破15个关键点的限制 绘制无上限(虽然用处不大 但是开启辅助线后…迷之好玩)
微调关键点绘制新的贝塞尔曲线 微调关键点来绘制新的贝塞尔曲线
设置贝塞尔曲线的绘制时间 设置贝塞尔曲线的绘制时间,绘制时间越长贝塞尔曲线越流畅
设计过程 设计了两个自定义view,其中一个自定义view用于收集屏幕的触摸事件,并展示添加的控制点和控制点之间的连线,实现长按屏幕拖动一定范围内最近的点.另一个自定义view用于接收控制点的参数,并根据控制点绘制贝塞尔曲线及辅助信息.
贝塞尔曲线绘制层 通过递归的方法,每一层中绘制当前控制点控制点之间的线段.除了第一层的样式是固定的之外,一定阶数下的辅助线段及控制点都可以被控制是否展示.而当开启无限制模式的时候,当前绘制的贝塞尔曲线的控制点没有上限,但是为了展示的效果当前模式下的辅助线段的样式都是一致的.
屏幕触摸事件监测层 监测屏幕的点击事件,增加控制点,除此之外,在长时间触摸屏幕后还会开启是否需要移动一定范围内最近的点移动到触碰的位置的监测.并能提供当前点的列表用于贝塞尔曲线绘制层绘制贝塞尔曲线绘制层来绘制贝塞尔曲线.
代码实现 屏幕触摸事件监测层 主要在于对屏幕的触碰事件的监测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 override fun onTouchEvent (event: MotionEvent) : Boolean { touchX = event.x touchY = event.y when (event.action) { MotionEvent.ACTION_DOWN -> { toFindChageCounts = true findPointChangeIndex = -1 if (controlIndex < maxPoint || isMore == true ) { addPoints(BezierCurveView.Point(touchX, touchY)) } invalidate() } MotionEvent.ACTION_MOVE ->{ checkLevel++ if (inChangePoint){ if (touchX == lastPoint.x && touchY == lastPoint.y){ changePoint = true lastPoint.x = -1F lastPoint.y = -1F }else { lastPoint.x = touchX lastPoint.y = touchY } if (changePoint){ if (toFindChageCounts){ findPointChangeIndex = findNearlyPoint(touchX , touchY) } } if (findPointChangeIndex == -1 ){ if (checkLevel > 1 ){ changePoint = false } }else { points[findPointChangeIndex].x = touchX points[findPointChangeIndex].y = touchY toFindChageCounts = false invalidate() } } } MotionEvent.ACTION_UP ->{ checkLevel = -1 changePoint = false toFindChageCounts = false } } return true }
关于最近的点的检测,勾股定理就可以得到了.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private fun findNearlyPoint (touchX: Float, touchY: Float) : Int { Log.d("bsr" , "touchX: ${touchX} , touchY: ${touchY}" ) var index = -1 var tempLength = 100000F for (i in 0. .points.size - 1 ){ val lengthX = Math.abs(touchX - points[i].x) val lengthY = Math.abs(touchY - points[i].y) val length = Math.sqrt((lengthX * lengthX + lengthY * lengthY).toDouble()).toFloat() if (length < tempLength){ tempLength = length if (tempLength < minLength){ toFindChageCounts = false index = i } } } return index }
相对来说,主要的难点是屏幕的触碰检测,需要控制时间和是否长安后找到合适的点之后的移动.除此之外就是简单的更加触碰点添加线段就好.
贝塞尔曲线绘制层 主要的贝塞尔曲线是通过递归实现的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 private fun drawBezier (canvas: Canvas, per: Float, points: MutableList<Point>) { val inBase: Boolean if (level == 0 || drawControl){ inBase = true }else { inBase = false } if (isMore){ linePaint.color = 0x3F000000 textPaint.color = 0x3F000000 }else { linePaint.color = colorSequence[level].toInt() textPaint.color = colorSequence[level].toInt() } path.moveTo(points[0 ].x , points[0 ].y) if (points.size == 1 ){ bezierPoints.add(Point(points[0 ].x , points[0 ].y)) drawBezierPoint(bezierPoints , canvas) val paint = Paint() paint.strokeWidth = 10F paint.style = Paint.Style.FILL canvas.drawPoint(points[0 ].x , points[0 ].y , paint) return } val nextPoints: MutableList<Point> = ArrayList() for (index in 1. .points.size - 1 ){ path.lineTo(points[index].x , points[index].y) val nextPointX = points[index - 1 ].x -(points[index - 1 ].x - points[index].x) * per val nextPointY = points[index - 1 ].y -(points[index - 1 ].y - points[index].y) * per nextPoints.add(Point(nextPointX , nextPointY)) } if (!(level !=0 && (per==0F || per == 1F ) )) { if (inBase) { if (isMore && level != 0 ){ canvas.drawText("0:0" , points[0 ].x, points[0 ].y, textPaint) }else { canvas.drawText("${charSequence[level]}0" , points[0 ].x, points[0 ].y, textPaint) } for (index in 1. .points.size - 1 ){ if (isMore && level != 0 ){ canvas.drawText( "${index}:${index}" ,points[index].x , points[index].y , textPaint) }else { canvas.drawText( "${charSequence[level]}${index}" ,points[index].x , points[index].y , textPaint) } } } } if (!(level !=0 && (per==0F || per == 1F ) )) { if (inBase) { canvas.drawPath(path, linePaint) } } path.reset() level++ drawBezier(canvas, per, nextPoints) }
除此之外,因为每次计算得到的是贝塞尔曲线的点,所以需要将这些点收集起来,并将之前收集到的所有的点绘制出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private fun drawBezierPoint (bezierPoints: MutableList<Point> , canvas: Canvas) { val paintBse = Paint() paintBse.color = Color.RED paintBse.strokeWidth = 5F paintBse.style = Paint.Style.STROKE val path = Path() path.moveTo(bezierPoints[0 ].x , bezierPoints[0 ].y) for (index in 1. .bezierPoints.size -1 ){ path.lineTo(bezierPoints[index].x , bezierPoints[index].y) } canvas.drawPath(path , paintBse) }
相关的代码可以访问我的Github ,欢迎大家star或提出建议.