这么炫酷的双仿真,原理了解一下?

2,540 阅读5分钟

本文正在参加「金石计划」

前言

上上周的时候,和大伙分享了双仿真页,最近又完善了一些,效果更加丝滑。

由于本人是一个摄影爱好者,所以简单做了一个相册的Demo,话不多说,看最终效果:

画册

图片中的效果来自三星的网页模拟器。

一、基础知识

这一块儿的基础知识已经在《如何写一个炫酷的大屏仿真页》和大家说过:

  1. 贝塞尔曲线
  2. Canvas相关的Api
  3. Matrix

如果你对贝塞尔曲线还是不了解,可以阅读我之前的文章:《从阅读仿真页看贝塞尔曲线》

简单的掌握了这些,我们就可以很快捷的绘制出仿真页。

二、架构 & 整体流程

看一下整体的结构:

聊天过程.png

整体的结构还是比较简单的:

  • 外层的 DoubleFlipView 负责处理触摸事件
  • 中间的 DoubleRealFlipView 负责动画和绘制整个流程的把握
  • BaseDirectDrawAction 和下面的 LeftxxxRightxxx 都是抽象类,绘制的具体实施方是它们,根据不同的方向又可以分为下面的四种,分别是左下页、左上页、右上页和右下页滑动。

简单的了解一下我的整个绘制流程,从我的代码中也可以看出:

  1. 绘制非翻转页
  2. 绘制翻转页的基本内容
  3. 绘制两页之间的阴影
  4. 绘制翻转页下一层页露出的内容和阴影
  5. 绘制翻转页的两侧阴影
  6. 翻转背部的内容

简单的用图片标注一下,过程对应图中的数字:

绘制过程

三、View转Bitmap

xml 文件转 Bitmap,看代码:

private fun createBitmapTwo(index: Int): Bitmap {
    val root: View = LayoutInflater.from(this).inflate(R.layout.view_album_style_two, null, false)
    //... 省略
    root.measure(measureWidth, measureHeight)
    root.layout(0, 0, root.measuredWidth, root.measuredHeight)
    val bitmap = Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888)
    val canvas = Canvas(bitmap)
    canvas.drawColor(Color.WHITE)
    root.draw(canvas)
    return bitmap
}

需要先对 View 进行 measure 和 layout 的过程,最后使用 View#draw 方法将 View 的内容绘制在 Canvas 上面,最后返回我们的 Bitmap 即可。

四、翻页机制

整个翻页的核心机制是这样的:

  1. 左右滑动决定翻页的进度
  2. 上下滑动决定翻页的角度

不太清楚?没关系,看图!

左右滑动:

左右滑动

纵向滑动:

纵向滑动

纵向滑动的时候,实质上就是围绕滑动点坐圆,求到这个极值就可以,极值怎么算:

纵坐标计算

仔细分析一下:

C点 -> A点 直线滚动
DC = DA
c点已知,D点已知,B点X坐标已知,B点很轻松就可以求出来。

写双仿真的难点,很大一部分就来自于将这些动作转化成实际的数学公式,头快秃了~

五、绘制基本内容

终于到我们的绘制环节了,绘制的核心就是使用贝塞尔曲线构建Path,所有区域的选择都是基于这个Path。

1. 创建基础的Path

Path路径: A - E - EG曲线 - G - B - H - HF曲线 - F - A

关于 Path 中的每个点,我在之前的文章都讲过,大家可以查看之前的文章:《从阅读仿真页看贝塞尔曲线》

image.png

虽然图片是单仿真,但是单仿真页和双仿真在贝塞尔这块的原理其实都一致。

2. 绘制非翻转页

还是用的上图:

绘制过程

过程1对应的页面就是非翻转页,化繁为简,整个绘制区域不做处理,绘制整个Bitmap。

3. 绘制翻转页基础部分

其实就是绘制过程2,对应图片中的部分蓝色三角形部分(标注的有点粗糙,见谅)。

过程就是:将左页原来的矩形绘制区域,扣除之前的贝塞尔曲线,得到图中过程2对应的蓝色三角形。

选取一点代码:

override fun drawFlipPageContent(
    canvas: Canvas,
    reUsePath: Path,
    flipPath: Path,
    r: Int
) {
    canvas.save()
    reUsePath.reset()
    reUsePath.moveTo(mLeftPageRBPoint.x - r, mLeftPageLTPoint.y)
    reUsePath.arcTo( mLeftPageRBPoint.x - 2 * r, mLeftPageLTPoint.y, mLeftPageRBPoint.x, mLeftPageLTPoint.y + 2 * r, -90f, 90f, false)
    reUsePath.lineTo(mLeftPageRBPoint.x, mLeftPageRBPoint.y - r)
    reUsePath.arcTo(mLeftPageRBPoint.x - 2 * r, mLeftPageRBPoint.y - 2 * r,mLeftPageRBPoint.x, mLeftPageRBPoint.y, 0f, 90f, false)
    reUsePath.lineTo(mLeftPageLTPoint.x, mLeftPageRBPoint.y)
    reUsePath.lineTo(mLeftPageLTPoint.x, mLeftPageLTPoint.y)
    reUsePath.close()
    canvas.clipPath(reUsePath)
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        canvas.clipOutPath(flipPath)
    } else {
        canvas.clipPath(flipPath, Region.Op.DIFFERENCE)
    }
    mLeftTopBitmap?.let {
        canvas.drawBitmap(it, mLeftPageLTPoint.x, mLeftPageLTPoint.y, null)
    }
    canvas.restore()
}

中间区域的静态阴影没什么好说的,直接跳过。

4. 绘制底层露出的内容

底层内容对应单仿真页中的 KLB 中的三角形,我们可以:

  1. 先在 Canvas 抠出对应的贝塞尔区域
  2. 进一步抠出 KLB 三角形对应的区域

然后将下一层内容单独绘制在这一块儿区域,依然是 Canvas 结合 Path,绘制 Bitmap,没什么好说的。

5. 绘制页边阴影

绘制页边的阴影也不是件很容易的事,首先,它是分为两个图层画的,上下的阴影为一个图层,左右的阴影为一个图层,所以你可以看见,Canvassaverestore 方法我调用了两遍。

另外一个麻烦的事就是角度的计算,因为有四个方向的翻页,每种方向翻页的时候角度计算都有点差别,所以你去角度计算的时候可能会有点头疼~

6. 绘制背部内容

终于来到最后一步了。

抠涂层的步骤也是一样:

  1. 先抠贝塞尔Path
  2. 去除 KLB 三角形对应的区域

之后就是绘制内容,使用 Matrix

之前看单仿真,看代码先用了对称,然后旋转 + 平移,在写双仿真的时候,惊奇的发现,只用旋转和平移的方式就够了。

看图,我特意将第三张图和第五张图换成一样的:

架构.png

我相信你可以很轻松的理解。完了,使用 Matrix 可以轻松的完成这些东西。

总结

总的来说,双仿真看着不是特别难,但是你去看代码的时候,可能不是一件特别容易的事,特别是使用各种数学公式的时候。

限制于时间的关系,后半部分文章没有贴更多的代码,主要我觉得贴代码也不太利于去理解,感兴趣的同学可以自己去看代码,周六一天时间都花在整理 Demo 和写文章了。

Demo地址:github.com/mCyp/Double…

这段时间先学习 Opengles 了,想写个双仿真试试。