前语

一向觉得阅读器里边的仿真页很有意思,最近在看阅读器相关代码的时分发现仿真页是根据贝塞尔曲线去完成的,所以就有了此篇文章。

仿真页一般有两种完成方法:

  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 = 0fprivate var per = 1.0f
  private var quadHeight = 100f
  private var cubicHeight = 200fprivate var bezierAnim: ValueAnimator? = nullinit {
    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