参考:
Path与贝赛尔曲线相关的函数
// 二阶贝赛尔
public void rQuadTo(float dx1, float dy1, float dx2, float dy2)
// 三阶贝赛尔
public void cubicTo(float x1, float y1, float x2, float y2,float x3, float y3)
public void rCubicTo(float x1, float y1, float x2, float y2,float x3, float y3)
二阶贝赛尔曲线
// (x1,y1)是控制点坐标,(x2,y2)是终点坐标
public void quadTo(float x1, float y1, float x2, float y2)
整条线的起始点是通过Path.moveTo(x,y)来指定的,而如果我们连续调用quadTo(),前一个quadTo()的终点,就是下一个quadTo()函数的起点;如果初始没有调用Path.moveTo(x,y)来指定起始点,则默认以控件左上角(0,0)为起始点;
上面的曲线(2次调用quadTo),用代码实现为:
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
strokeWidth = 2f
style = Paint.Style.STROKE
color = Color.RED
}
// 画曲线
val path = Path().apply {
moveTo(100f, 300f) // 移动到点(100,300)
// 点(100,300) 到 点(300,300)以控制点(200,200)画曲线
quadTo(200f, 200f, 300f, 300f)
// 点(300,300) 到 点(500,300)以控制点(400,400)画曲线
quadTo(400f, 400f, 500f, 300f)
}
canvas.drawPath(path, paint)
说明:
- 整条线的起始点是通过Path.moveTo(x,y)来指定的,如果初始没有调用Path.moveTo(x,y)来指定起始点,则默认以控件左上角(0,0)为起始点;
- 而如果我们连续调用quadTo(),前一个quadTo()的终点,就是下一个quadTo()函数的起点;
path 实现手势
val path = Path()
override fun onDraw(canvas: Canvas) {
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
strokeWidth = 2f
style = Paint.Style.STROKE
color = Color.RED
}
canvas.drawPath(path, paint)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
path.moveTo(event.x, event.y)
return true
}
MotionEvent.ACTION_MOVE -> {
path.lineTo(event.x, event.y)
postInvalidate() // 重新绘制
}
}
return super.onTouchEvent(event)
}
调用path.moveTo(event.getX(), event.getY());然后在用户移动手指时使用path.lineTo(event.getX(), event.getY());将各个点串起来。然后调用postInvalidate()重绘,形成图;
优化:使用Path.quadTo()函数实现过渡
path.lineTo()的最大问题就是线段转折处不够平滑(如下图)。
Path.quadTo()可以实现平滑过渡,但使用Path.quadTo()的最大问题是,如何找到起始点和结束点。
如下图,三个点,连成的两条直线,很明显他们转折处是有明显折痕的(蓝色图圈)
上图中,使用Path.lineTo()的时候,是直接把手指触点A,B,C给连起来。
如果要实现这三个点间的流畅过渡,就只能将这两个线段的中间点做为起始点和结束点,而将手指的倒数第二个触点B做为控制点。
更多请查看源博客;
在为了实现平滑效果,只能把开头的线段一半和结束的线段的一半抛弃掉;如下代码:
val path = Path()
// 控制点: 手指的前一个点,用来当控制点
var prevX = 0f
var prevY = 0f
override fun onDraw(canvas: Canvas) {
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
strokeWidth = 2f
style = Paint.Style.STROKE
color = Color.RED
}
canvas.drawPath(path, paint)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
path.moveTo(event.x, event.y)
prevX = event.x
prevY = event.y
return true
}
MotionEvent.ACTION_MOVE -> {
// path.lineTo(event.x, event.y)
// 结束点 为线段的中间位置
val endX = (event.x + prevX) / 2
val endY = (event.y + prevY) / 2
path.quadTo(prevX, prevY, endX, endY)
prevX = endX // 下一个控制点
prevY = endY
postInvalidate()
}
}
return super.onTouchEvent(event)
}
path.rQuadTo()
/**参数都为相对于上一个位置的位移偏量,可为负数*/
public void rQuadTo(float dx1, float dy1, float dx2, float dy2)
参数(从d可看出来意思):
- dx1:控制点X坐标,表示相对上一个终点X坐标的位移坐标;
- dy1:与dx1类似;
- dx2:终点X坐标,相对上一个终点X坐标的位移值;
- dy2:与dx2类似;
rQuadTo()方法实现波浪线
/*
path.moveTo(100,300);
path.quadTo(200,200,300,300);
path.quadTo(400,400,500,300);
*/
path.moveTo(100f,300f)
path.rQuadTo(100f, -100f, 200f,0f)
path.rQuadTo(100f, 100f, 200f, 0f)
canvas.drawPath(path, paint)
来自源博客的说明:
- 第一句:
path.rQuadTo(100,-100,200,0);
是建立在(100,300)
这个点基础上来计算相对坐标的。
所以- 控制点X坐标=上一个终点X坐标+控制点X位移 = 100+100=200;
- 控制点Y坐标=上一个终点Y坐标+控制点Y位移 = 300-100=200;
- 终点X坐标 = 上一个终点X坐标+终点X位移 = 100+200=300;
- 终点Y坐标 = 上一个终点Y坐标+控制点Y位移 = 300+0=300;
所以这句与path.quadTo(200,200,300,300);对等的
- 第二句:
path.rQuadTo(100,100,200,0);
是建立在它的前一个终点即(300,300)
的基础上来计算相对坐标的!
所以- 控制点X坐标=上一个终点X坐标+控制点X位移 = 300+100=200;
- 控制点Y坐标=上一个终点Y坐标+控制点Y位移 = 300+100=200;
- 终点X坐标 = 上一个终点X坐标+终点X位移 = 300+200=500;
- 终点Y坐标 = 上一个终点Y坐标+控制点Y位移 = 300+0=300;
所以这句与path.quadTo(400,400,500,300);对等的
rQuadTo(dx1, dy1, dx2, dy2)中的位移坐标,都是以上一个终点位置为基准来做偏移的!
波浪动画
整体代码分块,可从标号一个一个查看
val path = Path()
// 波浪高
val waveHeight = 100f
// 波浪宽
val waveWidth = 800f
// 波浪起始位置
var waveY = 300f
// 动画==波浪水平偏移
var waveWidthDx = 0f
// 动画==波浪垂直偏移
var waveHeightDx = 0f
val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
strokeWidth = 2f
style = Paint.Style.FILL
color = Color.GREEN
}
override fun onDraw(canvas: Canvas) {
path.apply {
reset() // 复位
// === 1.画波浪 ===
// path的起始位置向左移一个波长
moveTo(-waveWidth + waveWidthDx, waveY + waveHeightDx)
val halfWaveWidth = waveWidth / 2
var i = -halfWaveWidth
// 画出屏幕内所有的波浪
while (i <= width + halfWaveWidth) {
rQuadTo(halfWaveWidth / 2.0f, -waveHeight, halfWaveWidth, 0f)
rQuadTo(halfWaveWidth / 2.0f, waveHeight, halfWaveWidth, 0f)
i += halfWaveWidth
}
// === 2.闭合path,实现fill效果 ===
path.lineTo(width.toFloat(), height.toFloat())
path.lineTo(0f, height.toFloat())
path.close()
}
canvas.drawPath(path, paint)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
this.setOnClickListener {
// === 3.第三步动画,实现波浪动画
val valueAnimator = ValueAnimator.ofFloat(0f, waveWidth).apply {
duration = 2000
repeatMode = RESTART
repeatCount = ValueAnimator.INFINITE
interpolator = LinearInterpolator()
addUpdateListener { it ->
waveWidthDx = it.animatedValue as Float
postInvalidate()
}
}
valueAnimator.start()
// === 4. 不断缩小范围动画
ValueAnimator.ofFloat(0f, height.toFloat()).apply {
duration = 8000
repeatMode = RESTART
repeatCount = ValueAnimator.INFINITE
interpolator = LinearInterpolator()
addUpdateListener { it ->
waveHeightDx = it.animatedValue as Float
postInvalidate()
}
}.start()
// === 可以尝试使用联合动画
}
}