Android自定义View进阶 --- 贝塞尔曲线

贝塞尔曲线的本质是通过数学计算的公式来绘制平滑的曲线,分为一阶,二阶,三阶及多阶。但是这里不讲数学公式和验证,那些伟大的数学家已经证明过了,所以就只讲讲Android开发中的运用吧!

在这里插入图片描述

对于Android开发,实现贝塞尔曲线还是比较方便的,有对应的API供你调用。由于一阶贝塞尔曲线就是一条直线,实际没啥多大用处,因此,下面主要讲解二阶和三阶。

二阶贝塞尔曲线

在Android中,使用quadTo来实现二阶贝塞尔

        path.reset()
        path.moveTo(startX, startY)
        path.quadTo(currentX, currentY, endX, endY)
        canvas.drawPath(path, curvePaint)

startX和startY,endX和endY为两个固定点,currentX和currentY就是控制点,通过改变控制点的位置来改变二阶贝塞尔曲线的形状。

Android自定义View进阶 --- 贝塞尔曲线_第1张图片

a点和b点就是固定点,c点是控制点,我们可以改变c点的位置来改变曲线的形状。

    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
                currentX = event.x
                currentY = event.y
                postInvalidate()
            }
        }
        return true
    }

三阶贝塞尔曲线

在Android中,使用cubicTo来实现三阶贝塞尔

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        path.reset()
        path.moveTo(startX, startY)
        path.cubicTo(fixedX1, fixedY1, fixedX2, fixedY2, endX, endY)
        canvas.drawPath(path, curvePaint)
        //绘制辅助线
        drawHelpLine(canvas)
    }

    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
                //divideLine区分触摸点是左边还是右边
                if (event.x < divideLine) {
                    fixedX1 = event.x
                    fixedY1 = event.y
                } else {
                    fixedX2 = event.x
                    fixedY2 = event.y
                }
                postInvalidate()
            }
        }
        return true
    }

其中,startX和startY,endX和endY为两个固定点,fixedX1和fixedY1,fixedX2和fixedY2分别为两个控制点,通过改变控制点的位置来改变三阶贝塞尔曲线的形状。

Android自定义View进阶 --- 贝塞尔曲线_第2张图片
a点和b点就是固定点,c点和d点是控制点,我们可以改变c点或d点的位置来改变曲线的形状。

OK,贝塞尔曲线的基础到此就讲完了,下面来个实战,体验一下贝塞尔曲线的丝滑吧!

关于贝塞尔曲线,最典型的应用就是波浪球了,那咱们也来整一个,先上图

Android自定义View进阶 --- 贝塞尔曲线_第3张图片

首先裁剪一下画布,变为圆形

        val circlePath = Path()
        circlePath.addCircle(width / 2f, height / 2f, width / 2f, Path.Direction.CW)
        canvas.clipPath(circlePath)

Path.Direction.CW:沿顺时针方向绘制,Path.Direction.CCW:沿逆时针方向绘制

以View为中心,画圆

        canvas.drawCircle(width / 2f, height / 2f, width / 2f, circularPaint)

利用二阶贝塞尔,绘制波浪,起点为屏幕外,circleLen为曲线1/4周期长度

    private val startPoint = Point(-4 * circleLen, 0)

根据进度改变起点坐标的y值,控制点为曲线的顶部和底部,循环绘制,然后构建曲线之下的封闭区域,填充

        //根据进度改变起点坐标的y值
        startPoint.y = ((1 - (progress / 100.0)) * height).toInt()
        //移动到起点
        wavePath.moveTo(startPoint.x.toFloat(), startPoint.y.toFloat())
        var j = 1
        //循环绘制曲线
        for (i in 1..8) {
            val controlX = (startPoint.x + circleLen * j).toFloat()
            //波顶和波底
            val controlY =
                if (i % 2 == 0) (startPoint.y + waveHeight).toFloat() else (startPoint.y - waveHeight).toFloat()
            //二阶贝塞尔
            wavePath.quadTo(
                controlX,
                controlY,
                (startPoint.x + circleLen * 2 * i).toFloat(),
                startPoint.y.toFloat()
            )
            j += 2
        }
        //绘制封闭的区域
        wavePath.lineTo(width.toFloat(), height.toFloat())
        wavePath.lineTo(startPoint.x.toFloat(), height.toFloat())
        wavePath.lineTo(startPoint.x.toFloat(), startPoint.y.toFloat())
        wavePath.close()
        canvas.drawPath(wavePath, wavePaint)
        wavePath.reset()
        //走完一周回到原点
        startPoint.x =
            if (startPoint.x + translateX >= 0) -circleLen * 4 else startPoint.x + translateX
            

这里是设置每隔100ms,进度加一

        progress = if (progress >= 100) 0 else progress + 1
        postInvalidateDelayed(100)

全部代码如下

class ProgressBallView : View {

    //曲线1/4周期的长度
    private val circleLen = DensityUtils.dp2px(context, 53)

    //曲线高度
    private val waveHeight = DensityUtils.dp2px(context, 27)

    //默认的长宽值
    private val defaultSize = DensityUtils.dp2px(context, 300)

    //进度
    private var progress = 0

    //平移的长度
    private val translateX = circleLen / 4

    //圆形Paint
    private val circularPaint = Paint()

    //波浪Paint
    private val wavePaint = Paint()

    //波浪的路径
    private val wavePath = Path()

    //曲线的起始坐标
    private val startPoint = Point(-4 * circleLen, 0)

    constructor(context: Context) : super(context)

    constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) {
        initPaint()
    }

    private fun initPaint() {
        with(circularPaint) {
            isAntiAlias = true
            color = Color.GRAY
        }

        with(wavePaint) {
            isAntiAlias = true
            color = Color.RED
        }
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        var viewWidth = measureView(widthMeasureSpec)
        var viewHeight = measureView(heightMeasureSpec)
        //取最小的,作为长宽
        viewWidth = min(viewWidth, viewHeight)
        viewHeight = viewWidth
        setMeasuredDimension(viewWidth, viewHeight)
    }

    private fun measureView(measureSpec: Int): Int {
        val mode = MeasureSpec.getMode(measureSpec)
        return if (mode == MeasureSpec.AT_MOST || mode == MeasureSpec.EXACTLY) {
            MeasureSpec.getSize(measureSpec)
        } else {
            defaultSize
        }
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        //裁剪画布为圆形
        cutCanvas(canvas)
        //绘制圆形
        drawRound(canvas)
        //绘制波浪
        drawWave(canvas)
        //自动增长进度
        autoGrow()
    }

    //进度从0-100,自动增长
    private fun autoGrow() {
        progress = if (progress >= 100) 0 else progress + 1
        postInvalidateDelayed(100)
    }

    //裁剪画布为圆形
    private fun cutCanvas(canvas: Canvas) {
        val circlePath = Path()
        circlePath.addCircle(width / 2f, height / 2f, width / 2f, Path.Direction.CW)
        canvas.clipPath(circlePath)
    }

    //绘制圆形
    private fun drawRound(canvas: Canvas) {
        canvas.drawCircle(width / 2f, height / 2f, width / 2f, circularPaint)
    }

    //绘制波浪
    private fun drawWave(canvas: Canvas) {
        //根据进度改变起点坐标的y值
        startPoint.y = ((1 - (progress / 100.0)) * height).toInt()
        //移动到起点
        wavePath.moveTo(startPoint.x.toFloat(), startPoint.y.toFloat())
        var j = 1
        //循环绘制曲线
        for (i in 1..8) {
            val controlX = (startPoint.x + circleLen * j).toFloat()
            //波顶和波底
            val controlY =
                if (i % 2 == 0) (startPoint.y + waveHeight).toFloat() else (startPoint.y - waveHeight).toFloat()
            //二阶贝塞尔
            wavePath.quadTo(
                controlX,
                controlY,
                (startPoint.x + circleLen * 2 * i).toFloat(),
                startPoint.y.toFloat()
            )
            j += 2
        }
        //绘制封闭的区域
        wavePath.lineTo(width.toFloat(), height.toFloat())
        wavePath.lineTo(startPoint.x.toFloat(), height.toFloat())
        wavePath.lineTo(startPoint.x.toFloat(), startPoint.y.toFloat())
        wavePath.close()
        canvas.drawPath(wavePath, wavePaint)
        wavePath.reset()
        //走完一周回到原点
        startPoint.x =
            if (startPoint.x + translateX >= 0) -circleLen * 4 else startPoint.x + translateX
    }

}

你可能感兴趣的