自定义控件绘图(path、贝塞尔曲线)篇四

参考:

  1. https://blog.csdn.net/harvic880925/article/details/50995587
  2. https://www.jianshu.com/p/40abd770d05c

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()的最大问题是,如何找到起始点结束点

过渡不是很平滑

如下图,三个点,连成的两条直线,很明显他们转折处是有明显折痕的(蓝色图圈)


copy from 源博客
copy from 源博客

上图中,使用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()

        // === 可以尝试使用联合动画
    }
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,056评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,842评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,938评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,296评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,292评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,413评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,824评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,493评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,686评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,502评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,553评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,281评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,820评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,873评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,109评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,699评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,257评论 2 341

推荐阅读更多精彩内容