背景概述

Google已经在Mobile Web App开发上取得了很大的突破. HTML5已经拉近了Mobile设备Native App与Web App的差距. mobile Gmail 正是Google的Fixed position的最佳实践之一. iPad上的基于两栏的Gmail也运用了这种实现机制.

桌面浏览器本身就支持 position: fixed. 但 mobile Safari在iOS5之前不支持, 我们只能定制一个模拟滚动的工具来替代原生的滚动.

本教程就是在Mobile Web App上如何实现position: fixed的,就以下几点来讲:

  • 如何布局
  • 通过transforms实现滚动的动画效果
  • 通过 transitions实现滚动的惯性冲力
  • 触摸屏幕时中止滚动,即在打断 transitions的执行

如何布局

要达到目的,选好固定部分和滚动部分是个重点. 本例是个竖排的三栏,顶部和底部固定,中间滚动.

<html>
<head>
<style>
.TOP_TOOLBAR,
.BOTTOM_TOOLBAR {
height: 50px; width: 100%; position: absolute;
}
.TOP_TOOLBAR {
top: 0;
}
.BOTTOM_TOOLBAR {
bottom: 0;
}
.SCROLLER_FRAME {
width: 100%; position: absolute; top: 50px; bottom: 50px;
}
</style>
</head>
<body>
<div class=”TOP_TOOLBAR”>
... 顶部工具栏 ...
</div>
<div class=”SCROLLER_FRAME”>
<div class=”SCROLLER”>
... 滚动区域 ...
</div>
</div>
<div class=”BOTTOM_TOOLBAR”>
... 底部工具栏 ...
</div>
</body>
</html>

以上代码中固定不动的就是class为TOP_TOOLBARSCROLLER_FRAMEBOTTOM_TOOLBAR的元素. 只有class为 SCROLLER的元素. The SCROLLER_FRAME 即是指定了可滚动的范围元素.下一步我们就来写SCROLLER实现在SCROLLER_FRAME内滚动的JS代码.

用动画驱动滚动

要使滚动部分有动画效果首先就得把原生的滚动给阻止了.

<pre>document.body.addEventListener('touchmove', function(e) {
// This prevents native scrolling from happening.
e.preventDefault();
}, false);</pre>

阻止原生滚动后我们给滚动区追加touch事件然后通过 webkit-transforms来定位到用户手指拖动的位置. 要实现滚动效果需要保存一些状态值, 我们定义一个Scroller类来接收要滚动的元素.

<pre>Scroller = function(element) {
this.element = this;
this.startTouchY = 0;
this.animateTo(0);

element.addEventListener(‘touchstart’, this, false);
element.addEventListener(‘touchmove’, this, false);
element.addEventListener(‘touchend’, this, false);
}

Scroller.prototype.handleEvent = function(e) {
switch (e.type) {
case “touchstart”:
this.onTouchStart(e);
break;
case “touchmove”:
this.onTouchMove(e);
break;
case “touchend”:
this.onTouchEnd(e);
break;
}
}

Scroller.prototype.onTouchStart = function(e) {
// 这部分的实现第4步会讲.
this.stopMomentum();

this.startTouchY = e.touches[0].clientY;
this.contentStartOffsetY = this.contentOffsetY;
}

Scroller.prototype.onTouchMove = function(e) {
if (this.isDragging()) {
var currentY = e.touches[0].clientY;
var deltaY = currentY - this.startTouchY;
var newY = deltaY + this.contentStartOffsetY;
this.animateTo(newY);
}
}

Scroller.prototype.onTouchEnd = function(e) {
if (this.isDragging()) {
if (this.shouldStartMomentum()) {
// 这部分的实现第3步会讲.
this.doMomentum();
} else {
this.snapToBounds();
}
}
}

Scroller.prototype.animateTo = function(offsetY) {
this.contentOffsetY = offsetY;

// 在 webkit-transforms用 translate3d 的动画会得到硬件加速,性能显著提高
this.element.style.webkitTransform = ‘translate3d(0, ‘ + offsetY + ‘px, 0)’;
}

// 这方法的实现给读者作个练习吧.
// 你需要计算当前内容滚动到相对于可滚动范围的位置. 比如滚动超出范围要修正位置.
Scroller.prototype.snapToBounds = function() {
...
}

// 又是个练习.
// 就是判断用户的touch手势是不是拖动式的
Scroller.prototype.isDragging = function() {
...
}

// 再来个练习吧.
// 就是滚出范围时要作用上惯性冲力效果.
Scroller.prototype.shouldStartMomentum = function() {
...
}</pre>

至此我们的元素可以在滚动范围里滚动了,接下来就是惯性冲力的实现了. 为了不显得啰嗦以上几个简单的方法由读者实现. 下一步我们来讲惯性冲力的实现.

用动画驱动滚动

用户拖动力度和速度达到一定程序时,滚动的内容在手指离开后应当给予相应的惯性冲力效果反馈,使用webkit-transition属性来实现减速效果. 有3个值需要作用到webkit-transition上:

  • 滚动距离
  • 动画持续时间
  • transition的运动轨迹算法

滚动距离和时间可以通过拖动结束的速率和一个加速常量比 ( 0.0005 px/ms^2)计算得出. doMomentum()方法就是设置transition的.

<pre>Scroller.prototype.doMomentum = function() {
// 计算移动距离. 通过 开始位置和时间的比值来实现getEndVelocity方法
var velocity = this.getEndVelocity();
var acceleration = velocity &lt; 0 ? 0.0005 : -0.0005;
var displacement = - (velocity * velocity) / (2 * acceleration);
var time = - velocity / acceleration;

// 设置好 transition执行 transform.当然你要计算出transition的停止时间是必须的否则滚动会超出.
this.element.style.webkitTransition = ‘-webkit-transform ‘ + time +
‘ms cubic-bezier(0.33, 0.66, 0.66, 1)’;

var newY = this.contentOffsetY + displacement;
this.contentOffsetY = newY;
this.element.style.webkitTransform = ‘translate3d(0, ‘ + newY + ‘px, 0)’;
}</pre>

这就是惯性冲力的实现! 运转轨迹算法是cubic-bezier. 些算法是模拟自然效果的,调好一个合理的加速值最终看起来就非常的符合现实.还有一点我们没有提到,就是惯性冲力将滚动内容滚出范围怎么处理.这个比较难搞也超出了本教程的范围. 我们其实是使用几个 transitions队列来搞定的:

  • 第一个 transition 滚到边界, 这里因为终点值一不是0所以我们使用另一个cubic-bezier 算法.
  • 第二个transition滚出边界一点点, 终点值是0所以 使用原来的 cubic-bezier算法.
  • 第三个 transition减速并将内容带回边界内,使用 ease-out算法效果即可.

要使效果更加真实,第二个和第三个你最好另外调出合适的加速常理比.

触摸屏幕时中止滚动

如果滚动在运行时用户触摸屏幕内容应当在触摸位置处中止. 这点比较难搞,因为当滚动运行期间我们只知道开始和结束位置没法知道当前滚动到的确切位置. 下面我们就来讲如何计算滚动运行期间的具体位置和在正确的位置中止滚动.

<pre>Scroller.prototype.stopMomentum = function() {
if (this.isDecelerating()) {
// 获取样式对象.
var style = document.defaultView.getComputedStyle(this.element, null);
// 使用所得样式值通过webkitCSSMatrix计算出当前transform值.
var transform = new WebKitCSSMatrix(style.webkitTransform);
// 清除transition以免作用到下一个transform.
this.element.style.webkitTransition = ‘’;
// 指定transform到目标位置.
this.animateTo(transform.m42);
}
}</pre>

WebKitCSSMatrix 是WebKit内核提供可计算transform的方法. 其他支持HTML5的浏览器也有各自的方法. isDecelerating()方法你得自己实现了,作用是保存一些当前惯性冲力的状态值.

总结

通过这个教程你可能实现一个 fixed position的 DOM,基于touch事件实现滚动, 执行和停止惯性冲力. 其他知识点你慢慢消化,当然我们也会将开发都关注的技术慢慢跟进的总结出来.

原文