Android 自定義貝塞爾曲線工具

gengzhibo發表於2018-01-29

Android 自定義貝塞爾曲線工具

歡迎訪問我的個人網站來檢視本文章

之前在學習貝塞爾曲線的相關內容,在查詢相關資料的時候發現網上的資料重複的太多了,而且因為android的canvas只提供了quadTo,cubicTo兩種方法來繪製二階和三階的貝塞爾曲線.線上的貝塞爾曲線繪製網站也很少,(在這提供一個線上貝塞爾曲線的網站,根據網上的資料整理的),而在android手機中缺沒有類似的工具,在設計或者使用貝塞爾曲線的時候增加了很多的工作,剛好在學習相關的知識,就做了一個較為完善的android端的貝塞爾曲線工具.

貝塞爾曲線

基本的貝塞爾曲線的知識就不多說了,有興趣的可以參考下我之後會完成的貝塞爾曲線的記錄

其實理解貝塞爾曲線十分容易,可以將其理解為一種遞迴的形式.根據比例係數計算當前線段中的點,得到所有點之後再按照順序連線線段,重複以上步驟,直至只剩下一個點,此點就在貝塞爾曲線中,計算各個比例係數下的點,這些點的集合就是貝塞爾曲線.

基本功能

繪製常見的貝塞爾曲線

可以繪製常見的二階,三階貝塞爾曲線

二階貝塞爾曲線

繪製多階的貝塞爾曲線

可以繪製不常見的貝塞爾曲線

六階貝塞爾曲線

開啟/關閉輔助線

可以開啟不同顏色層級的輔助線段

開啟關閉輔助線段

繪製無上限制的貝塞爾曲線

突破15個關鍵點的限制 繪製無上限(雖然用處不大 但是開啟輔助線後...迷之好玩)

無限制下不展示輔助線的貝塞爾曲線繪製

微調關鍵點繪製新的貝塞爾曲線

微調關鍵點來繪製新的貝塞爾曲線

微調關鍵點繪製新的貝塞爾曲線

設定貝塞爾曲線的繪製時間

設定貝塞爾曲線的繪製時間,繪製時間越長貝塞爾曲線越流暢

不同時間長度的貝塞爾曲線繪製

設計過程

設計了兩個自定義view,其中一個自定義view用於收集螢幕的觸控事件,並展示新增的控制點和控制點之間的連線,實現長按螢幕拖動一定範圍內最近的點.另一個自定義view用於接收控制點的引數,並根據控制點繪製貝塞爾曲線及輔助資訊.

貝塞爾曲線繪製層

通過遞迴的方法,每一層中繪製當前控制點控制點之間的線段.除了第一層的樣式是固定的之外,一定階數下的輔助線段及控制點都可以被控制是否展示.而當開啟無限制模式的時候,當前繪製的貝塞爾曲線的控制點沒有上限,但是為了展示的效果當前模式下的輔助線段的樣式都是一致的.

螢幕觸控事件監測層

監測螢幕的點選事件,增加控制點,除此之外,在長時間觸控螢幕後還會開啟是否需要移動一定範圍內最近的點移動到觸碰的位置的監測.並能提供當前點的列表用於貝塞爾曲線繪製層繪製貝塞爾曲線繪製層來繪製貝塞爾曲線.

程式碼實現

螢幕觸控事件監測層

主要在於對螢幕的觸碰事件的監測

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
}
複製程式碼

關於最近的點的檢測,勾股定理就可以得到了.

//判斷當前觸碰的點附近是否有繪製過的點
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
}
複製程式碼

相對來說,主要的難點是螢幕的觸碰檢測,需要控制時間和是否長安後找到合適的點之後的移動.除此之外就是簡單的更加觸碰點新增線段就好.

貝塞爾曲線繪製層

主要的貝塞爾曲線是通過遞迴實現的

//通過遞迴方法繪製貝塞爾曲線
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)

}
複製程式碼

除此之外,因為每次計算得到的是貝塞爾曲線的點,所以需要將這些點收集起來,並將之前收集到的所有的點繪製出來

//繪製前段貝塞爾曲線部分
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或提出建議.

相關文章