Android自定义View绘制贝塞尔曲线实现流程

来自:网络
时间:2022-12-26
阅读:
目录

前言

对于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绘制贝塞尔曲线实现流程

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绘制贝塞尔曲线实现流程

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

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

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

Android自定义View绘制贝塞尔曲线实现流程

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

        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
    }
}
返回顶部
顶部