用canvas实现的Flipboard

Flipboard在智能手机和平板电脑的火热时期推出,作为移动优先体验,促使我们需要重新思考web中的内容布局原则,以获取更优雅的触屏体验。
现在我们打算完整的把Flipboard转到web上,我们在Flipboard上做的最多的事情就是关于设备的性能消耗;策划来自于所有的主题,来源于你最关心的人的最好的故事。将我们的服务转到web上面是一种符合逻辑的扩展。
当我们开始着手做这个项目的时候,我们知道如果想让产品能够符合我们的想法我们就必须努力改变内容布局与交互方式。我们想要匹配原生应用的优雅与性能,让用户在浏览器里有一样的体验。
早期时候,在测试了众多原形以后,我们决定我们的应用体验应该是滚动,我们的移动应用程序以其书本分页式的体验而闻名,在触摸屏上非常直观,但是出于各种原因我们只好在web上选用滚动的方式。
为了优化滚动的性能,我们知道我们需要保持构建的时间在16毫秒以下,限制回流与重绘,这在使用动画期间是非常重要的,为了避免在动画期间重绘,这里有两个属性你可以安全的设置动画,CSS transform 和opacity,但这是真的限制了你的选择。
如果你想要得到动画元素的宽度怎么办呢?
这里写图片描述
如何逐帧的滚动动画?
这里写图片描述

(注意上面的图像中,顶部的图标从白色过渡到了黑色。这里是两个独立的元素,他们的边界框根据下面的内容被裁减)
这个类型的动画在网上被很多人诟病,尤其是在移动设备上,原因只有一个:
DOM实在是太慢了。
它不止是缓慢,它真的是很慢,如果你在动画期间以任何方式触摸了DOM,那么你就会超过16毫秒的帧值。

进入canvas

大多数的移动设备都支持硬件加速的canvas,那我们为什么不利用这一点呢?HTML5游戏可以直接做到,那我们真的可以用canvas开发一个应用程序吗?

立即模式与保留模式
canvas是一种立即模式绘制的API,意味着在绘图的表面不保留任何有关绘制到其中的对象的信息(是基于状态的绘制),这一点与保留模式正好相反,保留模式是一种声明式的API,它维持着绘制到其中对象的层次结构。
保留模式API的优点在于,它们更容易构建复杂的应用场景,例如DOM的应用程序,不过它常常伴随着性能的成本,需要额外的内存还保存场景与更新页面可能会很慢

canvas受益于立即模式方法,允许绘图命令直接加载到GPU。但是使用它来构建用户界面是需要做一个更高层次的抽象,例如当资源异步加载(在图像的顶部绘制一个文本)时,简单的将一个元素绘制在另外一个元素的上面可能会出现问题,在html里面通过css里面的z-index属性就可以很容易的完成这个目的。

用canvas创建一个界面
canvas缺少了很多HTML+CSS里面理所应当的能力。

Text

这是一个非常简单的API来绘制文本:fillText(text,x,y,[,maxWidth]).
这个函数接受三个参数:text是一个字符串,x y 代表绘制的起始位置,但是canvas只允许画一行文本,如果你想对文本进行包装,需要自己编写函数。

Images

为了在画布上放置一张图片,我们需要引用drawImage()这个方法。这个是一个可变的函数,你可以指定图像与裁减的位置方面做更多的控制,但是画布不关心图像是否已经加载或则是不加载,因此请确保尽在图像加载事件之后调用。

重叠元素

在HTML与CSS当中通过使用DOM与z-index是很容易的指定一个元素在另外一个元素的上面,但是请记住canvas是立即模式,当元素重叠,并且其中的一个需要重绘的时候,都必须以相同的顺序(或者是脏的部分)重绘

自定义字体
需要使用自定义的web字体吗?canvas的文本API并不会关心是否文字已经加载,你需要一种方式了解文本是否已经加载并重绘任何依赖该字体的区域,幸运的是,现代的浏览器都有一种promise-basedAPI来实现它,不幸的是ios8不支持它。

canvas的好处
鉴于所有这些缺点,人们可能已经开始质疑选择canvas上的DOM方法。最后,我们的决定来源于一个简单的真理:
您无法使用DOM构建60fps滚动列表视图。
许多人(包括我们)都试过了,失败了。滚动元素可以在纯HTML和CSS中使用溢出:scroll(结合-webkit溢出滚动:在iOS上触摸),但这些不会给你逐帧控制滚动动画和移动浏览器有一个合理的时间在有长,复杂的内容的情况下。
为了构建一个具有相当复杂内容的无限滚动列表,我们需要相当于Web的UITableView。

与DOM相比,目前大多数设备都有硬件加速canvas实现,它们直接向GPU发送绘图命令。这意味着我们可以令人难以置信地快速渲染元素;我们在许多情况下说亚毫秒级。

与HTML + CSS相比,Canvas也是一个非常小的API,减少了错误的表面积或浏览器之间的不一致。有没有一个办法是可以使用?相当于画布。

更快的DOM抽象

如前所述,为了可以完成,我们需要更高层次的抽象,而不是简单地以直接模式绘制矩形,文本和图像。我们构建了一个非常小的抽象,允许开发人员处理节点树,而不是严格的绘制命令序列。

RenderLayer

RenderLayer是其他节点基于的基本节点。常用属性,如top,left,width,height,backgroundColor和zIndex在此级别表示。 RenderLayer只是一个包含这些属性和子数组的纯JavaScript对象。

图片

有一些图像图层,具有指定图像URL和裁剪信息的其他属性。您不必担心监听图像加载事件,因为图像图层会为您执行此操作,并向绘图引擎发送需要更新的信号。

文本

文本图层能够渲染多行截断的文本,这在DOM中是非常昂贵的。文本图层还支持自定义字体面,并且将在字体加载时执行更新工作。

组件

这些层可以组成以构建复杂的接口。下面是一个RenderLayer树的示例:

{
      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
            }
          ]
    }

无效图层

当图层需要重绘时,例如在图像加载之后,它向绘图引擎发送其帧是脏的信号。更改是使用requestAnimationFrame批量化,以避免布局颠簸,并在下一帧画布重绘。

以60fps滚动

也许我们认为最重要的Web的一个方面是浏览器如何滚动网页。浏览器厂商已经竭尽全力提高滚动性能。

它有一个权衡,为了在移动设备上滚动60fps,浏览器用于在滚动期间停止JavaScript执行,因为担心DOM修改会导致回流。最近,iOS和Android已经暴露了onscroll事件,它们更像在桌面浏览器上工作,但如果您试图保持DOM元素与滚动位置同步,您的里程可能会有所不同。

幸运的是,浏览器厂商都知道这个问题。特别是,Chrome小组已经开始努力改善这种情况。

回到canvas,简单的答案是你必须实现JavaScript中的滚动。
你需要的第一件事是计算滚动动量的方法。如果你不想做数学Zynga的人开放源码一个pure logic scroller适合任何布局方法。

我们在单个画布元素使用滚动技术。在每个触摸事件处,通过将每个节点转换当前滚动偏移来更新当前渲染树。然后使用新的帧坐标重绘整个渲染树。

这听起来像是非常慢,但有一个重要的优化技术,可以在canvas中使用,其中绘图操作的结果可以缓存在离屏画布上。然后可以使用离屏画布来在稍后的时间重绘该层。

这种技术不仅可以用于图像层,还可以用于文本和形状。两个最昂贵的绘图操作是填充文本和绘图图像。但是一旦这些图层被绘制一次,使用离屏画布就可以非常快地重绘它们。

在下面的演示中,每页内容分为2层:图像层和文本层。文本图层包含多个分组在一起的元素。在滚动动画中的每个帧,使用缓存的位图重绘两个图层。
这里写图片描述

对象池

在滚动无限列表项的过程中,必须设置和拆除大量的RenderLayers。 这将会创建大量的垃圾,这将会停止主线程收。
为了避免创建的垃圾数量,RenderLayers和关联对象被积极地合并。 这意味着只有相对少量的层对象被创建。 当不再需要层时,它被释放回到池中,在那里它可以被重新使用。

快速快照

缓存复合层的能力带来另一个优点:能够将渲染结构的部分视为位图。 您是否需要仅拍摄部分DOM结构的快照? 当你在canvas中渲染这个结构时,这是非常快速和容易。
用于将项目翻转到仓库中的UI利用这种能力来执行从时间线的平滑过渡。 快照包含整个项目,减去顶部和底部chrome。
这里写图片描述

声明性API

我们现在有一个应用程序的基本构建块。然而,强制构建RenderLayers的树可能是乏味的。是不是很好,有一个声明性的API,类似于DOM的工作原理?

React

我们是React的大粉丝。它的单向数据流和声明性API改变了人们构建应用程序的方式。 React最引人注目的特性是虚拟DOM。它在浏览器容器中呈现为HTML的事实只是一个实现细节。最近的React Native的介绍证明了这一点。
我们是否可以将我们的画布布局引擎绑定到React组件?

React Canvas简介

React Canvas添加了React组件呈现到而不是DOM的能力。

第一个版本的画布布局引擎看起来非常像命令式视图代码。如果你曾经使用JavaScript构建DOM,你可能会遇到这样的代码:

//创建
var root = RenderLayer.getPooled(); root.frame = [00320480];

//添加图片

var image = RenderLayer.getPooled('image');
image.frame = [00320200];
image.imageUrl ='http://lorempixel.com/360/420/cats/1/';
root.addChild(image);

/添加一些文本

var label = RenderLayer.getPooled('text');
label.frame = [10210300260];
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:{
    左:0,
    top:0,
    width:320,
    高度:480
  },

  图片: {
    左:0,
    top:0,
    宽:320,
    高度:200
  },

  text:{
    左:10,
    top:210,
    宽度:300,
    高度:260,
    fontSize:18,
    lineHeight:24
  }}
};
你可能会注意到,一切似乎都是绝对的定位。这是正确的。我们的画布渲染引擎

诞生于需要使用多行椭圆化文本来驱动像的布局。这不能用传统的CSS,所以一个方法,一切都绝对定位适合我们。然而,这种方法不太适合于所有应用。

css布局

Facebook最近开源了CSS的JavaScript实现。它支持CSS的一个子集,如margin,padding,position和最重要的flexbox。
将CSS布局集成到React Canvas中需要几个小时。查看示例以了解这将如何改变组件的样式。

声明式无限滚动

如何在React Canvas中创建一个60fps无限的分页滚动列表?
原来这是很容易,因为React的差异的虚拟DOM。在render()中,只返回当前可见的元素,React会在滚动期间根据需要更新虚拟DOM

var ListView = React.createClass({
  getInitialState:function(){
    return {
      scrollTop:0
    };
  },

  render:function(){
    var items = this.getVisibleItemIndexes()。map(this.renderItem);
    return(
      <组
        onTouchStart = {this.handleTouchStart}
        onTouchMove = {this.handleTouchMove}
        onTouchEnd = {this.handleTouchEnd}
        onTouchCancel = {this.handleTouchEnd}>
        {items}
      </ Group>
    );
  },

  renderItem:function(itemIndex){
    //将每个项目包裹在基于<组>>上下翻译的<组>中
    //当前滚动偏移。
    var translateY =(itemIndex * itemHeight) - this.state.scrollTop;
    var style = {translateY:translateY};
    return(
      <Group style = {style} key = {itemIndex}>
        <项/
      </ Group>
    );
  },

  getVisibleItemIndexes:function(){
    //根据`this.state.scrollTop`计算可见项目索引。
  }}
});
为了挂接滚动,我们使用Scroller库在我们的ListView组件```
setState()。
... ...

//在mount上创建Scroller实例。
componentDidMount:function(){
  this.scroller = new Scroller(this.handleScroll);
},

//这是由滚动器在每个滚动事件调用。
handleScroll:functionlefttop){
  this.setState({scrollTop:top});
},

handleTouchStart:functione){
  this.scroller.doTouchStart(e.touches,e.timeStamp);
},

handleTouchMove:functione){
  e.preventDefault();
  this.scroller.doTouchMove(e.touches,e.timeStamp,e.scale);
},

handleTouchEnd:functione){
  this.scroller.doTouchEnd(e.timeStamp);
}}

... ...
虽然这是一个简化的版本,它展示了一些React的最好的品质。触摸事件在render()中声明性地绑定。

每个touchmove事件被转发到计算当前滚动顶部偏移的滚动器。从Scroller发出的每个滚动事件更新

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值