Canvas绘制列表的尝试

485 查看

为什么尝试使用Canvas绘制列表?使用canvas绘制列表的好处在于页面只有一个dom元素,这样对于大量dom元素组成的列表来说,无疑更节省页面内存。

本文将一步一步分析,如何实现一个canvas绘制的长列表。

 

Step1:dom节点映射

首先考虑一个问题,对于我们在页面中常见的dom结点,在Canvas中如何表示?

因此我们的第一步工作就是实现dom结点到Canvas绘制对象的映射

 

 

1

上面列出了dom节点常见的属性和方法,因此对于canvas,我们也需要封装对应的对象,模拟dom节点的特性与方法接口:

2

 

 

上面封装的renderLayer对象模拟了dom节点的基础特性,并提供一个draw方法用于绘制节点。

我们把renderLayer对象对应为网页元素中最基本的div元素,接下来我们可以封装更多的继承于renderLayer的对象,分别对应更多的dom元素:

 

3

 

其中CanvasText类和CanvasImage类分别都继承于RenderLayer类,并且由于它们各有不同的展示方式,因此它们分别实现自己的draw方法,做定制化的展示。

4

 

Step2:绘制对象的布局机制实现

接下来思考的一个问题就是,如何定义绘制对象出现在canvas上的位置?

 

方案1:直接指定绘制坐标

5

 

这种方案的缺点是,不方便页面元素进行布局,列表结构复杂的时候,坐标难以维护。、

 

 

方案2:通过指定css样式 根据布局计算,转换到具体坐标6

 

 

方案2的优点是:方面元素布局,样式结构更便于维护。

 

需要实现在canvas中能够使用css的方式布局,我们需要依赖一个库:css-layout.js

http://github.com/facebook/css-layout

 

css-layout.js可以帮忙实现json对象的css样式到canvas坐标系的转换,有兴趣的同学可以看看其源码。

7

 

 

css-layout支持多种css属性的转换:

 

8

 

整个流程如下:

初始化canvas绘制对象 ->通过layout.js从根节点开始计算layout,得到每个对象的具体绘制坐标 ->绘制

 

 

step3:元素样式更新

 

首先我们会把样式分为两个类别:布局样式和渲染样式。

布局样式:决定元素的布局,例如大小,位置等。

渲染样式:决定元素的展示样式,例如色值等。

 

对于两种样式的变更,分别有不同的样式更新方式。

布局样式变更:在下次循环,需要重新计算layout ,然后再重新绘制所有对象。

然而对于渲染样式的变更:在下次循环,只需要重新绘制所有对象。

 

step4:实现列表滚动

 

在尝试模拟canvas的列表滚动之前,我们先来看看如何模拟传统dom页面的div滚动。

以iscroll为例,要模拟div的滚动,实质上就是对div层不断改变其translateY值,让其位移到不同地方,从而实现滚动的效果。

 

再来看canvas的列表滚动模拟,其实我们需要做的就是不断改变2dContext的绘制原点,使每次在不同的原点开始绘制canvas内的所有元素,实现滚动的视觉效果:

9

 

因此,我们需要的就是一个通过监听用户手势事件,模拟滚动并得到当前滚动距离的组件。

 

方案1:使用Zynga Scroller组件

组件github地址:http://github.com/zynga/scroller

该组件不同于iscroll之类的组件,它不对实际元素进行移位操作,而是仅仅根据手势,实时返回一个当前的滚动距离值,结合组件改变绘制原点的使用示例:

10

 

 

方式2:透明div接管滚动,获取scrollY值

11

 

该方法是把两个父子关系的透明div盖在canvas上面,设置子div高度为内容高度,并监听父div的scroll事件,在scrol事件的处理程序中获取scrollTop并用于改变Canvas 2dContext的绘制原点。该方案相比方案1的好处是不用通过js代码监听用户手势事件并模拟滚动的缓动效果,而是直接使用原生的浏览器滚动。

 

Step5:事件模拟

对于click,touch等dom事件的模拟,我们采用的方案是根据点击区域进行检测,并找出最底层的元素,递归寻找父元素并触发对应事件处理程序,从而模拟事件冒泡。

 

12

 

 

step6:滚动绘制流程优化

按照目前的做法,每个循环当中,我们都需要清除整个canvas,然后再把列表内的所有元素重新绘制一次,这样做无疑是非常损耗性能的:

13

 

因此我们可以做的一个优化就是,把绘制过的列表项缓存下来,把绘制结果保存到一个脱离dom的canvas中,放到一个canvas池中,下次绘制的时候,直接从canvas池中读取已缓存的canvas,把列表项的绘制结果绘制到列表的canvas当中:

14

 

step7:自定义标签

对于目前的代码组织方式,我们需要每次使用类的api的方式去创建一个新的对象,并添加到canvas中,然而我们还可以使用jsx的方式,让整个代码组织更便于维护,参考react canvas中的做法:

15

 

效果测试:

到这里,基本上已经实现了对列表在canvas上的模拟了,然而性能怎样呢?以下是一些测试结果:

 

android:不同机型fps差异较大

SumSung grand 2 :fps35左右

小米2:fps50左右

ios:

iphone5c: 50左右

 

然而针对android机型进行进一步的测试,尝试仅仅改变绘制区域的大小进行测试:

16

 

测试发现,仅仅通过减少绘制区域,对fps就会造成较大的影响,因此也证明一些机器上canvas绘制的最大瓶颈,在于绘制区域的大小。

 

其他需要注意的问题:

由于元素都绘制在Canvas上,因此不能被读屏软件识别,影响无障碍化。