在智能手机和平板电脑的黎明时期, Flipboard 推出“移动先行”的体验,使咱们能够从新思考页面中内容布局的原则,以及与触摸屏相关的,如何得到更好的用户体验的因素。css
为了创建完整的体验,咱们将 Flipboard 带到 web 端。咱们在 Flipboard 所作的,在每台用户使用的设备上都拥有独立的价值:整理那些来自你最关心的主题,来源以及人的最好的故事。所以把咱们的服务带到web端,也是一个合乎逻辑的延伸。html
当咱们开始这个项目后,认识到咱们须要把源自移动体验的思考搬到 web 端,试图提高 web 端的内容布局和交互。咱们想达到原生应用般的精致和性能,且仍能感知到真实的浏览器。html5
早些时间,通过测试大量的产品原型后,咱们决定使用滚动的方式做为 web 端体验。咱们的移动应用被你们所熟知的是相似翻书般的体验,在触摸屏上这很直观。但一系列的缘由代表,滚动在 web 端的体验更加天然。react
为了优化滚动的性能, 咱们知道咱们须要保证页面渲染的频率低于16ms,同时限制回流(reflow)和重绘(repaints)。这在动画中尤为重要。为了不动画中从新渲 染,有两个属性你能够安全地做用于动画上: CSS transform 和 opacity。但这样选择余地过小了。ios
当你想实现元素上宽度动画效果怎么办?css3
一帧帧的滚动动画如何处理?git
注意,在上图中,顶部的图标从白色到黑色。这里使用了两个单独的元素相互覆盖,根据下面的内容来互相裁剪)。 这些类型的动画一直在网上遭受诟病,特别是在移动设备上,只由于一个简单的缘由:DOM 太慢了。github
<canvas>
大多数现代移动设备都拥有硬件加速的 canvas,咱们为何不利用起来呢?HTML5 游戏已经作到了。咱们能真正在 canvas 上开发应用界面么?web
Canvas 是一种当即模式的绘图 API,这意味着绘制时不保留所绘制对象的信息。与其相反的是保留模式,这是一种声明性的 API,维护所绘制对象的层次结构。算法
保留模式api的优势是,对于你的应用程序,他们一般更容易构建复杂的场景,例如 DOM。一般这都会带来性能成本,须要额外的内存来保存场景和更新场景,这可能会很慢。
Canvas 受益于当即模式,容许直接发送绘图命令到 GPU。但若用它来构建用户界面,须要进行一个更高层次的抽象。例如一些简单的处理,好比当绘制一个异步加载的资源到一个元素上时会出现问题,如在图片上 绘制文本。在HTML中,因为元素存在顺序,以及 CSS 中存在 z-index
,所以是很容易实现的。
<canvas>
元素中创建UI相比 HTML+CSS,canvas 则有些先天不足,缺乏很是多在 HTML + CSS 中理所固然的特性。
canvas有一个很简单的 API 用于绘制文字:fillText(text, x, y [, maxWidth])
这个函数接受三个参数:文字自己以及绘制起点的x
,y
坐标。但 canvas 只能一次绘制一行文字。若是你须要让文字换行,须要本身写函数。
你可使用drawImage()
函数在 canvas 上绘制图片。这是个可变参数函数,你能够指定更多参数,从而控制定位和裁切。可是canvas不在意图像是否加载,或不能肯定只在图像加载事件后调用函数。
经过 DOM 元素的顺序或使用 CSS 的 z-index属性,在 HTML 和 CSS 指定一个元素是否在另外一个上应该很容易。但请记住,canvas 是当即模式的绘图 API。当元素重叠或者其中一个须要重绘时,都必须以同一顺序从新绘制(或至少局部重绘)。
译者注 关于局部重绘提升性能的文章你们能够参考:《提升HTML5 canvas性能的几种方法(转)》
须要使用一个自定义 web 字体吗? canvas 的文本 API 并不在意字体是否加载。你须要一种方法来知道一个字体是否加载,并绘制任何依赖此字体的区域。幸运的是,现代浏览器有一个基于promise的API。不幸的是, iOS WebKit (iOS 8 时)不支持它。
<canvas>
的优势鉴于全部这些缺点,人们开始质疑 canvas 来代替 DOM 这一选择。最终,咱们的讨论由一个很简单的真理来决定:你不能基于 dom 创建一个60 fps的滚动列表视图。
许多人(包括咱们)已经尝试过,但都失败了。可滚动的元素能够在纯 HTML 和 CSS 中经过 overflow:scroll
实现:(结合 IOS 上的 -webkit-overflow-scrolling:touch
),但这些不能在滚动动画中给予你逐帧控制,同时移动浏览器很难处理又长又复杂的内容。
为了构建一个内含至关复杂的内容的无限滚动列表,咱们须要在 web 端实现一个UITableView
与 DOM 相比,今天的大多数设备都有基于硬件加速的 canvas 实现,能够直接发送绘图命令到 GPU。这意味着咱们能够很是快的渲染元素;在许多状况下,咱们所说的是毫秒级的范围。
相比 HTML + CSS , canvas 也是一个很是“苗条”的 API ,这减小了界面上的 bug 或浏览器之间的不一致性。有一个理由更加直接,canvas 没有 Can I Use?。
译者注 UITableView 是 IOS 控件
如前所述,为了有点效果,咱们须要一个更高层次的抽象,而不是简单地绘制矩形、文本和图像。咱们构建了一个很是小的抽象,容许开发人员处理一个节点树,而不是处理一个严格的绘图命令序列。
渲染层( RenderLayer )是基本节点,其余节点创建在其上。常见的属性如 top
,left
,width
,height
,backgroundColor
和 zIndex
在这个层展示。 RenderLayer 只不过是一个普通的 JavaScript 对象,包含这些属性和一个子元素数组。
咱们使用图像层的附加属性来指定图像 URL 和信息。你没必要担忧图像加载事件的监听,图像层会处理后将一个信号发送到绘图引擎来表示图片须要更新。
文本层能够显示多行文本截断,这在 DOM 里处理成本很是高。文本层还支持自定义字体,也会处理当字体加载完毕后更新的动做。
这些层能够合成起来以便构建复杂的界面。下面是一个渲染层树
{
frame: [0, 0, 320, 480],
backgroundColor: '#fff',
children: [
{
type: 'image',
frame: [0, 0, 320, 200],
imageUrl: 'http://lorempixel.com/360/420/cats/1/'
},
{
type: 'text',
frame: [10, 210, 300, 260],
text: 'Lorem ipsum...',
fontSize: 18,
lineHeight: 24
}
]
}
当一个层须要重绘,例如一个图像加载后,它发送一个信号到绘图引擎表示其框架是脏( dirty )的。这些修改使用 requestAnimationFrame来批量处理,避免布局抖动,以后在下一帧画布重绘。
对于 web 端,也许咱们最理所固然关注的,是浏览器如何来滚动网页。浏览器厂商已经不遗余力提升滚动性能。
这实际上是一个妥协。为了达到60 fps 的滚动指标,移动浏览器在执行滚动期间,中止 JavaScript 的执行,这是怕 DOM 修改致使回流。最近, IOS 和 Android 暴露了 onScroll 事件,他们的工做过程更像桌面浏览器了,但若是你试图在滚动时保持 DOM 元素的位置同步,具体的实现可能会有差异。
幸运的是,浏览器厂商已经意识到这个问题。特别是 Chrome 团队已经开放了为了改善手机端这种状况所作的工做。
回到 canvas ,简短的回答是你必须用 JavaScript 实现滚动。
你首先须要的是一种计算滚动程度的算法。若是你不想研究数学,Zynga 开源的纯滚动实现,适合任何相似此布局的状况。
咱们使用一个 canvas 元素来完成滚动。在每个触摸事件时,根据当前的滚动程度去更新渲染树。以后,整个渲染树使用新的坐标来从新渲染。
这听起来使人难以置信的慢,在 canvas 上可使用一个重要的优化技术--画布上绘图操做的结果能够在离屏层(off-screen)canvas 被缓存。离屏层(off-screen)以后能够用来从新绘制层。
这种技术不只能够用于图像层,文本和图形也适用。两个成本最高的绘图操做是填充文本和图像。可是一旦这些层绘制一次之后,接下来使用离屏层从新绘制他们是很是快的。
在上面的演示中,每一个页面的内容分为两层:图像层和文本层。文本层包含多个元素组合在一块儿。每一帧滚动动画中,这两层都使用位图缓存来重绘。
在一个无限列表的滚动过程当中,大量的 RenderLayers 会被创建和销毁。这会在内存中建立大量的垃圾,当进行垃圾收集时将中止主线程。
为了不产生大量垃圾, RenderLayers 与相关对象聚集到一个池中。这意味着只有相对较少的层对象被建立。当再也不须要时,它会被释放回池中,以后能够重用。
缓存复合层的特性能够带来另外一个优点:可以将渲染的部分结构做为一个位图。你有没有创建部分 DOM 结构快照的需求?当你将这些结构渲染在 canvas 时,速度会快得使人难以置信。这个将一个项目放入一本杂志的界面,利用了这种特性来执行一个时间轴维度的平稳过渡。快照包含去掉顶部和底部的整个项目。
如今咱们已经拥有了构建一个应用程序的砖块。然而,经过命令来构建 RenderLayers 多是乏味的。若是咱们有个相似于DOM工做方式的声明式 API 不是很好么?
咱们是 React 框架的忠实粉丝。它的单必定向的数据流和声明式API已经改变了人们构建应用程序的方式。react最引人注目的特征就是虚拟 DOM (virtual DOM)。呈现为HTML容器只是它在浏览器中的一个简单实现。最近引入的 React Native 证实了这一点。
若是咱们将咱们的 canvas 布局引擎与 react 组件结合起来会咋样?
React Canvas 使React组件拥有了渲染到canvas的能力。
第一个版本的 canvas 布局引擎看上去很像命令式的代码。若是你作过 JavaScript DOM 操做你可能会运行过这样的代码:
// Create the parent layer
var root = RenderLayer.getPooled();
root.frame = [0, 0, 320, 480];
// Add an image
var image = RenderLayer.getPooled('image');
image.frame = [0, 0, 320, 200];
image.imageUrl = 'http://lorempixel.com/360/420/cats/1/';
root.addChild(image);
// Add some text
var label = RenderLayer.getPooled('text');
label.frame = [10, 210, 300, 260];
label.text = 'Lorem ipsum...';
label.fontSize = 18;
label.lineHeight = 24;
root.addChild(label);
固然,这样能完成效果,可是谁想这样写代码?除了容易出错,也很难想象出渲染结果
使用React Canvas则变成下面这样:
var MyComponent = React.createClass({
render: function () {
return (
<Group style={styles.group}>
<Image style={styles.image} src='http://...' />
<Text style={styles.text}>
Lorem ipsum...
</Text>
</Group>
);
}
});
var styles = {
group: {
left: 0,
top: 0,
width: 320,
height: 480
},
image: {
left: 0,
top: 0,
width: 320,
height: 200
},
text: {
left: 10,
top: 210,
width: 300,
height: 260,
fontSize: 18,
lineHeight: 24
}
};
您可能会注意到,一切都彷佛是绝对定位实现的。确实是这样。咱们的canvas渲染引擎的诞生,就伴随着驱动像素级布局,实现多行文本过长省略的使命。传统的CSS不能作到这一点,因此绝对定位的方式对咱们来讲很适合。然而,这种方法并不适合于全部应用程序。
Facebook 最近开源的CSS的JavaScript实现。他支持 CSS 的一些子集,包括 margin
、padding
,position
和最重要的 flexbox
。
将 css 布局整合到 React Canvas 只是一个时间问题。看看这个例子,看看咱们是如何改变组件样式的。
你如何在 React Canvas 中建立一个达到60 fps ,无限,分页的滚动列表?事实证实这实现起来很是容易,由于 react 会作虚拟 DOM 的 diff 。在render()
函数中只有当前可见的元素被返回,React负责更新滚动期间所需的虚拟 DOM 树。
var ListView = React.createClass({
getInitialState: function () {
return {
scrollTop: 0
};
},
render: function () {
var items = this.getVisibleItemIndexes().map(this.renderItem);
return (
<Group
onTouchStart={this.handleTouchStart}
onTouchMove={this.handleTouchMove}
onTouchEnd={this.handleTouchEnd}
onTouchCancel={this.handleTouchEnd}>
{items}
</Group>
);
},
renderItem: function (itemIndex) {
// Wrap each item in a <Group> which is translated up/down based on
// the current scroll offset.
var translateY = (itemIndex * itemHeight) - this.state.scrollTop;
var style = { translateY: translateY };
return (
<Group style={style} key={itemIndex}>
<Item />
</Group>
);
},
getVisibleItemIndexes: function () {
// Compute the visible item indexes based on `this.state.scrollTop`.
}
});
为了勾住(捕获)滚动,咱们在列表视图组件中,调用Scroller(滚动组件)的 setState()
方法。
...
// Create the Scroller instance on mount.
componentDidMount: function () {
this.scroller = new Scroller(this.handleScroll);
},
// This is called by the Scroller at each scroll event.
handleScroll: function (left, top) {
this.setState({ scrollTop: top });
},
handleTouchStart: function (e) {
this.scroller.doTouchStart(e.touches, e.timeStamp);
},
handleTouchMove: function (e) {
e.preventDefault();
this.scroller.doTouchMove(e.touches, e.timeStamp, e.scale);
},
handleTouchEnd: function (e) {
this.scroller.doTouchEnd(e.timeStamp);
}
...
尽管这是一个简化版本,但展现了 React 一些最优秀的特性。触摸事件被声明式绑定在 render()函数中。每一个 touchmove 事件被转发到 Scroller 中来计算当前滚动的偏移。每一个 Scroller 发出的滚动事件则用于更新状态列表视图组件,只对当前屏幕可见的元素进行渲染。全部这一切发生在16ms如下,由于 react 的 diff 算法很是快。
你能够查看这个滚动列表完整实现的源代码。
React Canvas 并不能彻底取代 DOM。咱们在咱们的移动 web app 中,性能要求最关键的地方去使用,主要是滚动时间轴视图这部分。
当渲染性能不是问题的时候, Dom 多是一个更好的方法。事实上,对某些元素输入字段和音频/视频等,这是惟一的方法
从某种意义上说, Flipboard 的移动 web 是一个混合( hybird )的应用程序。相比传统的原生应用和网络技术结合的方式, Flipboard 的内容所有是 web 内容。它的 UI 基于 dom 实现,并在适当的地方使用 canvas 渲染。
这个领域须要进一步探索。使用降级内容( canvas 的 DOM 子树)应该容许 VoiceOver 这样的屏幕阅读器与内容交互。咱们在测试的设备上看到了不一样的结果。另外,关于焦点的管理也有标准,但目前暂时不被浏览器支持。
Bespin在2009年提出的一种方法,是元素渲染到 canvas 时,同时维护一个平行Dom,用于元素同步。咱们正在继续研究实现可访问性的正确方法。
在追求60 fps 的过程当中,咱们有时会采起极端措施。 Flipboard 为研究移动网络的局限性提供了一个案例。虽然这种方法可能并不适用于全部应用程序,咱们将应用的交互和性能水平提高到能够与本地应用相竞争。咱们但愿经过开放咱们在 React Canvas 中所作的工做,可让其余引人注目的例子出现。
用手机访问flipboard.com,体验一下。或者若是你没有 Flipboard 帐户,体验一下 Flipboard 上一系列的杂志。请让咱们得到你的想法。
特别感谢 Charles, Eugene 和 Anh 的编辑和建议。
http://jcodecraeer.com/a/qianduankaifa/css3/2015/0305/2549.html