从阅读仿真页看贝塞尔曲线

Posted bug樱樱

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从阅读仿真页看贝塞尔曲线相关的知识,希望对你有一定的参考价值。

前言

一直觉得阅读器里面的仿真页很有意思,最近在看阅读器相关代码的时候发现仿真页是基于贝塞尔曲线去实现的,所以就有了此篇文章。

仿真页一般有两种实现方式:

  1. 将内容绘制在Bitmap上,基于Canvas去处理仿真页
  2. OpenGl es

本篇文章我会向大家介绍如何使用Canvas绘制贝塞尔曲线,以及详细的像大家介绍仿真页的实现思路。

后续有机会的话,希望可以再向大家介绍方案二(OpenGL es 学习中…)。

一、贝塞尔曲线介绍

贝塞尔曲线是应用于二维图形应用程序的数学曲线,最初是用在汽车设计的。我们在绘图工具上也常常见到曲线,比如钢笔工具。

为了绘制出更加平滑的曲线,在 android 中我们也可以使用 Path 去绘制贝塞尔曲线,比如这类曲线图或者描述声波的图。

我们先简单的了解一下基础知识,可以在这个网站先体验一把如何控制贝塞尔曲线:

www.jasondavies.com/animated-be…

一阶到四阶都有。

1. 一阶贝塞尔曲线

给定点 P0 和 P1,一阶贝塞尔曲线是两点之间的直线,这条线的公式如下:

2. 二阶贝塞尔曲线

从二阶开始,就变得复杂起来,对于给定的 P0、P1 和 P2,都对应的曲线:

二阶的公式是如何得出来的?我们可以假设 P0 到 P1 点是 P3,P1 - P2 的点是P4,二阶贝塞尔也只是 P3 - P4 之间的动态点,则有:

P3 = (1-t) P0 + tP1

P4 = (1-t) P1 + tP2

二阶贝塞尔曲线 B(t) = (1-t)P3 + tP4 = (1-t)((1-t)P0 + tP1) + t((1-t)P1 + tP2) = (1-t)(1-t)P0 + 2t(1-t)P1 + ttP2

与最终的公式对应。

3. 三阶贝塞尔曲线

三阶贝塞尔曲线由四个点控制,对于给定的 P0、P1、P2 和 P3,有对应的曲线:

同样的,三阶贝塞尔可以由二阶贝塞尔得出,从上面的知识我们可以得处,下图中的点 R0 和 R1 的路径其实是二阶的贝塞尔曲线。

对于给定的点 B,有如下的公式,将二阶贝塞尔曲线带入:

R0 = (1-t)(1-t)P0 + 2t(1-t)P1 + ttP2

R1 = (1-t)(1-t)P1 + 2t(1-t)P2 + ttP3

B(t) = (1-t)R0 + tR1 = (1-t)((1-t)(1-t)P0 + 2t(1-t)P1 + ttP2) + t((1-t)(1-t)P1 + 2t(1-t)P2 + ttP3)

最终的结果就是三阶贝塞尔曲线的最终公式。

4. 多阶贝塞尔曲线

多阶贝塞尔曲线我们就不细讲了,可以知道的是,每一阶都可以由它的上一阶贝塞尔曲线推导而出。就像我们之前由一阶推导二阶,由二阶推导出三阶。

二、Android对应的API

Android提供了 Path 供我们去绘制贝塞尔曲线。一阶贝塞尔是一条直线,所以不用处理了。

看一下 Path 对应的 API:

  • Path#quadTo(float x1, float y1, float x2, float y2):二阶
  • Path#cubicTo(float x1, float y1, float x2, float y2,float x3, float y3):三阶

对于一段贝塞尔曲线来说,由三部分组成:

  1. 一个开始点
  2. 一到多个控制点
  3. 一个结束点

使用的方法也很简单,先挪到开始点,然后将控制点和结束点统统加进来:

class BezierView @JvmOverloads constructor(
    context: Context,
    attributeSet: AttributeSet? = null,
    defStyle: Int = 0
) : View(context, attributeSet, defStyle) 
​
    private val path = Path()
    private val paint = Paint()
​
    override fun onDraw(canvas: Canvas?) 
        super.onDraw(canvas)
​
        paint.style = Paint.Style.STROKE
        paint.strokeWidth = 3f

        path.moveTo(0f, 200f)
        path.quadTo(200f, 0f, 400f, 200f)
        paint.color = Color.BLUE
        canvas?.drawPath(path, paint)
​
        path.rewind()
        path.moveTo(0f, 600f)
        path.cubicTo(100f, 400f, 200f, 800f, 300f, 600f)
        paint.color = Color.RED
        canvas?.drawPath(path, paint);
    

最后的结果:

上面是二阶贝塞尔,下面是三阶贝塞尔,可以发现,控制点越多,就能设计出越复杂的曲线。如果想使用二阶贝塞尔实现三阶的效果,就得使用两个二阶贝塞尔曲线。

三、简单案例

既然刚刚画了两个曲线,我们可以利用这个方式简单模拟一个动态声波的曲线。

这个动画只需要在刚刚的代码的基础上稍微改动一点:

class BezierView @JvmOverloads constructor(
    context: Context,
    attributeSet: AttributeSet? = null,
    defStyle: Int = 0
) : View(context, attributeSet, defStyle) 
​
    private val path = Path()
    private val paint = Paint()
​
    private var width = 0f
    private var height = 0f
    private var quadY = 0f
    private var cubicY = 0f
​
    private var per = 1.0f
    private var quadHeight = 100f
    private var cubicHeight = 200f
​
    private var bezierAnim: ValueAnimator? = null
​
    init 
        paint.style = Paint.Style.STROKE
        paint.strokeWidth = 3f
        paint.isDither = true
        paint.isAntiAlias = true
    
​
    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) 
        super.onSizeChanged(w, h, oldw, oldh)
​
        width = w.toFloat()
        height = h.toFloat()
​
        quadY = height / 4
        cubicY = height - height / 4
    
​
​
    fun startBezierAnim() 
        bezierAnim?.cancel()
        bezierAnim = ValueAnimator.ofFloat(1.0f, 0f, 1.0f).apply 
            addUpdateListener 
                val value = it.animatedValue as Float
                per = value
                invalidate()
            
            addListener(object :AnimatorListener
                override fun onAnimationStart(animation: Animator?) 
​
                
​
                override fun onAnimationEnd(animation: Animator?) 
​
                
​
                override fun onAnimationCancel(animation: Animator?) 
​
                
​
                override fun onAnimationRepeat(animation: Animator?) 
                    val random = Random(System.currentTimeMillis())
                    val one = random.nextInt(400).toFloat()
                    val two = random.nextInt(800).toFloat()
​
                    quadHeight = one
                    cubicHeight = two
                
​
            )
            duration = 300
            repeatCount = -1
            start()
        
    
​
​
    override fun onDraw(canvas: Canvas?) 
        super.onDraw(canvas)
​
        var quadStart = 0f
        path.reset()
        path.moveTo(quadStart, quadY)
        while (quadStart <= width)
            path.quadTo(quadStart + 75f, quadY - quadHeight * per, quadStart + 150f, quadY)
            path.quadTo(quadStart + 225f, quadY + quadHeight * per, quadStart + 300f, quadY)
            quadStart += 300f
        
        paint.color = Color.BLUE
        canvas?.drawPath(path, paint)
​
        path.reset()
        var cubicStart = 0f
        path.moveTo(cubicStart, cubicY)
        while (cubicStart <= width)
            path.cubicTo(cubicStart + 100f, cubicY - cubicHeight * per, cubicStart + 200f, cubicY + cubicHeight * per, cubicStart + 300f, cubicY)
            cubicStart += 300f
        
        paint.color = Color.RED
        canvas?.drawPath(path, paint);
    

上面基于二阶贝塞尔曲线,下面基于三阶贝塞尔曲线,加了一层属性动画。

四、仿真页的拆分

我们在本篇文章不会涉及到仿真页的代码,主要做一下仿真页的拆分。

下面的这套方案也是总结自何明桂大佬的方案。

从图中的仿真页中我们可以看出,上下一共两页,我们需要处理:

  1. 第一页的内容
  2. 第一页的背面
  3. 第二页露出来的内容

这三部分中,除了 GE 和 FH 是两段曲线,其他都是直线,直线是比较好计算的,先看两段曲线。

通过观察发现,这里的 GE 和 FH 都是对称的,只有一个平滑的弯,用一个控制点就能应付,所以选择二阶贝塞尔曲线就够了。GE 这段二阶段贝塞尔曲线,对应的控制点是 C,FH 对应的控制点是 D。

1. 第一页正面

再看图片,路径 A - F - H - B - G - E - A 之外的就是第一页正面,将内容页和这个路径的 Path 取反即可。

具体的过程:

  1. 已知 A 是触摸点,B 是内容页的底角点,可以求出中点 M 的坐标
  2. AB 和 CD 相互垂直,所以可得 CD 的斜率,从 M 点坐标推出 CD 两点坐标
  3. E 是 AC 中点,F 是 AD 中点,那么 E 和 F 的点位置很容易推导出来

2. 第二页内容

第二页的重点 KLB 这个三角形,M 是 AB 的中点,J 是 AM 的中点,N 是 JM 的重点,通过斜率很容易推导出与边界相交的KL 两点,之后从内容页上裁出 KLB 这个Path,第二页的内容绘制在这个 Path 即可。

3. 第一页的背面

背面这一块儿绘制的区域是三角形 AOP,AC、AD 和 KL 都已知,求出相交的 KL 点即可。

但是我们还得将第一页底部的内容做一个旋转和偏移,再加上一层蒙层,就可以得到我们想要的背面内容。

总结

可以看出,学会了贝塞尔曲线以后,仿真页其实并不算特别复杂,但是整个数学计算还是很麻烦的。

下篇文章再和大家讨论具体的代码,如果觉得本文有什么问题,评论区见!

参考文章:

blog.csdn.net/hmg25

作者:九心
链接:https://juejin.cn/post/7173850844977168392

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。


相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

全套视频资料:

一、面试合集

二、源码解析合集


三、开源框架合集


欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证微信卡片免费领取↓↓↓

以上是关于从阅读仿真页看贝塞尔曲线的主要内容,如果未能解决你的问题,请参考以下文章

Android进阶之自定义View实战贝塞尔曲线应用

Android进阶之自定义View实战贝塞尔曲线应用

贝塞尔 平滑曲线

CAShapeLayer和贝塞尔曲线配合使用

flutter贝塞尔曲线

从Android动画到贝塞尔曲线