Android Compose 仿真书籍翻页组件 PTQBookPageView 设计思路

2,160 阅读5分钟

本篇是Android Jetpack Compose仿真翻页组件PTQBookPageView的设计思路篇,因为篇幅的关系,以及因为毕竟是UI组件,光靠文字肯定无法完全解释清楚,因此之后会录制一个更详细的视频来把组件的设计、实现和源码完全讲清楚。本篇文章仅会介绍设计和实现的思路,具体的代码不会过多涉及。

那么接下来我们话不多说,直接开始。

1 需求分析

首先我们得明白有哪些要实现的功能,或者说想把这个组件做成什么样子,接下来罗列一下需要实现的点和需要解决的问题。

  1. 翻页算法
    • 如何将书页抽象成点模型,以便能够在屏幕上进行绘制
    • 如何计算手势与UI的关系,即输入为手势,输出为书页点
      • Loose状态下的算法
        • 处理翻转
      • thetaMin状态下的算法
      • wMin状态下的算法
      • tight状态下的算法
        • 书页跟随手指算法
      • 各个状态之间如何平滑过渡连接
  2. 绘制
    • 如何绘制阴影
    • 如何绘制光泽
    • 如何实现文字扭曲
    • 如何将计算出的点模型绘制成书页
  3. 动画
    • 如何实现起手/离手动画
    • 如何实现点击翻页
  4. Compose实现
    • 如何将屏幕内容绘制为Bitmap
    • 框架设计
      • 状态管理
      • 回调设计

大纲就列到这里,基本上就是这些功能点,接下来我们具体展开。

需要提一嘴的是,算法、绘制、动画的思路与平台无关,可以使用其他的实现方式,例如原生Android、Flutter都可以实现,最后一部分则是有关Compose的部分。

2 算法

算法部分主要解决“如何把页面抽象成点模型”以及“如何根据手势计算出点模型的点坐标”两个问题。

2.1 点模型

整个书页被抽象成由15个点构成的点类AllPoints,如下图所示。

为了数学计算的方便,在计算前会将所有点处理成笛卡尔直角坐标系,而非安卓View体系的坐标系(代码中称为绝对系),在计算完所有点后,再转换回来。这一点其实是可以优化的一个点,如果全部使用绝对系直接计算,可以减少部分转换的开销。

2.2 根据手势计算点坐标

具体过程无法用文字说明,因此省略1万字。

这一步完成之后,理应能够做到:输入一个唯一的手指坐标,输出一个与之对应的唯一AllPoints类。

3 绘制

这一步主要解决“如何绘制阴影”、“如何绘制光泽”、“如何实现文字扭曲”、“如何将计算出的点模型绘制成书页”四个问题。

3.1 阴影

书页边上有阴影,而这个阴影也是根据计算出的AllPoints绘制的。

3.2 光泽

与阴影类似,当前页背页的光泽也根据AllPoints进行绘制。

3.3 文字扭曲

当前页正面在翻起来的时候,翻起来的部分应该需要向上卷曲,这个可以利用圆筒算法和图像的扭曲算法(drawBitmapMesh方法)来实现。

3.4 绘制书页

在上述都完成后,我们可以把书页看成是由若干图层组合而成的,具体地:

  • 画下一页
  • 画当前页背页在下一页留下的阴影
  • 画当前页
  • 画当前页翻起部分在当前页留下的阴影
  • 画当前页背面(翻起部分)
  • 画当前页背面(翻起部分)光泽
  • 加强一下轮廓

全部画完以后,就可以得到书页了。

4 动画

动画包括两部分,“如何实现起手/离手动画”、“如何实现点击翻页”。

4.1 起手/离手动画

实际上,起手或离手动画就是让程序自动像手指点击那样产生一个手指坐标的输入,同样地,得到唯一的AllPoints输出,然后,每隔很小一段时间自动地控制手指坐标变化,就得到了想要的动画效果。

4.2 点击翻页

这部分基于起手/离手动画,当手指点击以后,直接设置好一个起手和离手动画即可。

5 Compose实现

主要分为“绘制Bitmap”和“框架设计”两部分。

5.1 绘制Bitmap

由于想做成一个能传入任何View的组件,因此,Compose侧需要解决的一个重头问题就是,如何获取到使用者自定义传入的View进行翻页。

这里采用的做法是,先将当前组件绘制为Bitmap,然后再结合AllPoints进行计算。我设计了BitmapController类专门负责生成和管理Bitmap对象。

但是其实获取当前View的Bitmap这一步在目前的Compose中遇到了很多困难,最后的实现是学习了fundroid大佬的这个思路

这样的Bitmap的方式应该不是性能上的最优解,如果能直接操作屏幕像素,应该性能上还可以提升,但是不知道有没有这样的方法,我不是太会hhh。

5.2 框架设计

框架设计部分主要考虑了两方面,一是状态传参,二是回调,我模仿了LazyColumn的方式进行传参和回调的处理,在组件调用上个人认为代码还算优雅和简洁。

6 最后

整个翻页组件的设计思路就差不多是这样了,这其中的内容太多了,而且很多东西无法用文字说明,因此之后会准备录制一个视频更加详细地讲清设计思路和代码细节,直接看源码。

就写到这里吧~