本文正在参加「金石计划」
前言
上上周的时候,和大伙分享了双仿真页,最近又完善了一些,效果更加丝滑。
由于本人是一个摄影爱好者,所以简单做了一个相册的Demo,话不多说,看最终效果:
图片中的效果来自三星的网页模拟器。
一、基础知识
这一块儿的基础知识已经在《如何写一个炫酷的大屏仿真页》和大家说过:
- 贝塞尔曲线
- Canvas相关的Api
- Matrix
如果你对贝塞尔曲线还是不了解,可以阅读我之前的文章:《从阅读仿真页看贝塞尔曲线》
简单的掌握了这些,我们就可以很快捷的绘制出仿真页。
二、架构 & 整体流程
看一下整体的结构:
整体的结构还是比较简单的:
- 外层的
DoubleFlipView
负责处理触摸事件 - 中间的
DoubleRealFlipView
负责动画和绘制整个流程的把握 BaseDirectDrawAction
和下面的Leftxxx
和Rightxxx
都是抽象类,绘制的具体实施方是它们,根据不同的方向又可以分为下面的四种,分别是左下页、左上页、右上页和右下页滑动。
简单的了解一下我的整个绘制流程,从我的代码中也可以看出:
- 绘制非翻转页
- 绘制翻转页的基本内容
- 绘制两页之间的阴影
- 绘制翻转页下一层页露出的内容和阴影
- 绘制翻转页的两侧阴影
- 翻转背部的内容
简单的用图片标注一下,过程对应图中的数字:
三、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 即可。
四、翻页机制
整个翻页的核心机制是这样的:
- 左右滑动决定翻页的进度
- 上下滑动决定翻页的角度
不太清楚?没关系,看图!
左右滑动:
纵向滑动:
纵向滑动的时候,实质上就是围绕滑动点坐圆,求到这个极值就可以,极值怎么算:
仔细分析一下:
C点 -> A点 直线滚动
DC = DA
c点已知,D点已知,B点X坐标已知,B点很轻松就可以求出来。
写双仿真的难点,很大一部分就来自于将这些动作转化成实际的数学公式,头快秃了~
五、绘制基本内容
终于到我们的绘制环节了,绘制的核心就是使用贝塞尔曲线构建Path,所有区域的选择都是基于这个Path。
1. 创建基础的Path
Path路径: A - E - EG曲线 - G - B - H - HF曲线 - F - A
关于 Path 中的每个点,我在之前的文章都讲过,大家可以查看之前的文章:《从阅读仿真页看贝塞尔曲线》
虽然图片是单仿真,但是单仿真页和双仿真在贝塞尔这块的原理其实都一致。
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 中的三角形,我们可以:
- 先在 Canvas 抠出对应的贝塞尔区域
- 进一步抠出 KLB 三角形对应的区域
然后将下一层内容单独绘制在这一块儿区域,依然是 Canvas 结合 Path,绘制 Bitmap,没什么好说的。
5. 绘制页边阴影
绘制页边的阴影也不是件很容易的事,首先,它是分为两个图层画的,上下的阴影为一个图层,左右的阴影为一个图层,所以你可以看见,Canvas
的 save
和 restore
方法我调用了两遍。
另外一个麻烦的事就是角度的计算,因为有四个方向的翻页,每种方向翻页的时候角度计算都有点差别,所以你去角度计算的时候可能会有点头疼~
6. 绘制背部内容
终于来到最后一步了。
抠涂层的步骤也是一样:
- 先抠贝塞尔Path
- 去除 KLB 三角形对应的区域
之后就是绘制内容,使用 Matrix
。
之前看单仿真,看代码先用了对称,然后旋转 + 平移,在写双仿真的时候,惊奇的发现,只用旋转和平移的方式就够了。
看图,我特意将第三张图和第五张图换成一样的:
我相信你可以很轻松的理解。完了,使用 Matrix 可以轻松的完成这些东西。
总结
总的来说,双仿真看着不是特别难,但是你去看代码的时候,可能不是一件特别容易的事,特别是使用各种数学公式的时候。
限制于时间的关系,后半部分文章没有贴更多的代码,主要我觉得贴代码也不太利于去理解,感兴趣的同学可以自己去看代码,周六一天时间都花在整理 Demo 和写文章了。
Demo地址:github.com/mCyp/Double…
这段时间先学习 Opengles 了,想写个双仿真试试。