贝塞尔曲线的本质是通过数学计算的公式来绘制平滑的曲线,分为一阶,二阶,三阶及多阶。但是这里不讲数学公式和验证,那些伟大的数学家已经证明过了,所以就只讲讲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就是控制点,通过改变控制点的位置来改变二阶贝塞尔曲线的形状。
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分别为两个控制点,通过改变控制点的位置来改变三阶贝塞尔曲线的形状。
a点和b点就是固定点,c点和d点是控制点,我们可以改变c点或d点的位置来改变曲线的形状。
OK,贝塞尔曲线的基础到此就讲完了,下面来个实战,体验一下贝塞尔曲线的丝滑吧!
关于贝塞尔曲线,最典型的应用就是波浪球了,那咱们也来整一个,先上图
首先裁剪一下画布,变为圆形
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
}
}