很久以前,手机上的交互依赖键盘和触控笔。我们要查看一个很长很长的列表,必须使用非常难用的触控笔或键盘的上下左右键。后来黑莓发明了滚动球,缓解了大拇指按出茧的问题。
2007年,苹果推出iPhone。iPhone只有一个玻璃屏,没有触控笔,直接用手指操作,支持多手指。Multi-Touch这项技术在推出时被誉为和Mouse(Mac),Click wheel(iPod)一样革命性的发明。当然最近的3D Touch(Apple watch)也是革命性的。在推出iPhone之前,苹果已经做了多年的铺垫。2005年收购的小公司finger works就是专门做手势识别的团队,用macbook的人几乎是脱离鼠标的,因为其触控板非常好用,所用技术来自这家公司。
当时有很多程序猿和产品经理讨论这个技术如何实现。疯狂的Web开发者要在浏览器上实现基于鼠标中键的滚动效果:当很快地滚动中键并停止时,页面由于惯性,会继续往下滚动一段距离才停止。有一个专业名词用来描述,叫Momentum Scrolling。还有人用Web写了示例,用来模拟手指和屏幕交互过程中的数学逻辑,iScroll 和 Scrollability 都是不错的作品。头条的Web图集也有使用Javascript(以下简称JS)实现的惯性滚动代码。下面我们用最简单的Scrollability来讲解,如何实现惯性滚动。
一. 实现滚动
HTML代码:
JS代码:
页面中有四个元素,顶部的黑条,底部的黑条,黑条中的窗口A,以及窗口内一堆英文名列表B(可以想象成办公室的卷帘)。坐标系从窗口A左上角开始,横轴X(右边为正向),纵轴Y(下边为正向)。注意,这里Y轴的方向和数学课本里相反。手指在窗口A上移动,往下移动一段距离,得到的 distanceY > 0。代码由HTML,CSS和JS构成,HTML定义了两个黑条,窗口A和卷帘B。CSS定义了他们的颜色,位置等属性。Javascript则是重点要讲的,它用来控制手指和屏幕的交互。JS中定义了几个功能和变量,其中kBounceLimit这样以k开头的,是数学公式中的固定不变的参数。startX,startY,touchX,touchY用来记录手指位置,touchAnimator是负责操作卷帘B滚动的对象。
这几行代码表示,当用户手指触碰到屏幕时,会执行onTouchStart,去做一些事情。手指在屏幕上移来移去时,会执行onTouchMove。手指离开屏幕的瞬间,硬件会通知程序手指离开,此时执行onTouchEnd。touchstart和touchend都是瞬间事件,从手指按下,移动,抬起的一套过程中只会被通知一次,touchmove是连续触发的,移动1厘米,会收到几十个通知,告诉程序当前手指坐标x,y,时间点。
让卷帘随着手指移动的办法是,在touchstart时,记录手指位置y1;touchmove时根据得到y2,算出当前移动的距离distance=y2-y1,将卷帘在y轴上移动distance距离; touchmove不断触发,不断执行前面两步,把上一次的touchmove当作起始点,紧接着触发的touchmove当作后来的点,计算distance移动卷帘。
iPhone在处理滚动的过程中,当页面已经到最顶部时,再往下拉会有弹簧效果:手指拉了一个屏幕的距离,内容只移动了半屏。用这样的代码实现其效果:
velocity的含义是:手指在屏幕移动一段距离,触发n个touchmove事件,相邻两个事件的y坐标之差就是velocity(速度)。
打个比方,老李跑步,他的步长就是 velocity。当老李还没跑,想逃出森林公园南门时会有人把他往回拉。Velocity是1米,往回拉的弹性会给velocity打折,身体移动距离可能是0.8米,具体的折扣为:
(1.0 - (position - max) / bounceLimit)*kBounceLimit
position:当前卷帘移动到的位置,(逃出南门的距离)
parentOffsetHeight : 598 窗口A的高度
nodeOffsetHeight : 4602 卷帘B的长度
max:0
min:-4004 (parentOffsetHeight-nodeOffsetHeight,卷帘被挡住的长度,从下往上拉卷帘,最多移动的距离就是min值)
absMin = min
absMax = max
bounceLimit :窗口高度 parentOffsetHeight*kBounceLimit
kBounceLimit:0.75
转换后,速度velocity衰减的系数为 0.75 - position/ 598,当position越大,velocity打折越多,越难往下拖动。
二. 惯性和回弹
接着,我们用代码实现自由滚动和下拉时的回弹
在touchend时,加入takeoff()方法
takeoff方法中,根据手指离开屏幕时的velocity计算后面滚动动画的代码:
savekeyframe方法会保存计算出来的每一帧动画,包含位置和时间点。
自由滚动时的状态:
在顶部回弹时的状态:
position = easeOutExpo(decelStep, decelOrigin, decelDelta, kBounceTime);
continues = ++decelStep <= kBounceTime && Math.floor(Math.abs(position)) > max;迭代计算position的公式为缓动曲线easeOut,接受的四个参数为:
decelStep :初始值为0,每次减速,自增1
decelOrigin :decelStep为0时 position值
decelDelta :decelStep为0时 max-position
kBounceTime :240
对比linear,ease-in,ease-out和ease-in-out四个曲线,ease-out方程x=easeOut(t)在一开始有平缓的加速,在时间t到50%时,位置x已经快到达70%,
在后续50%的时间内减速直到停止。符合我们平时看到的iOS滚动逻辑。
function easeOutExpo(t, b, c, d) { return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b }
continues的计算自由滚动时:continues = Math.floor(Math.abs(velocity)*10) > 0; // Math.abs(velocity) < 0.1
顶部回弹时:continues = ++decelStep <= kBounceTime && Math.floor(Math.abs(position)) > max; // decelStep<=240加位置约束
底部回弹时:continues = ++decelStep <= kBounceTime && Math.ceil(position) < min; //同上
底部上拉的状态,同上。
saveKeyframe(!continues);
time += kAnimationStep;作用是根据diff判断是否保存这个关键帧,最后得到位置和时间的运动轨迹
根据keyframes得到运动轨迹,再使用实现滚动的代码,让页面运动起来。这里就不展开说明了
(gif图略卡,实际效果流畅很多)
三. 浏览器实现
苹果和谷歌在意识到这个需求后,为web开发者提供了原生的支持。
之前需要用iscroll等框架的需求,Chrome浏览器只需 overflow:scroll 一行代码即可。iOS设备需再加一行代码 -webkit-overflow-scrolling: touch;
原生实现的好处是
计算位置的过程要进行大量小数运算,这一点JS非常慢。
惯性滚动是苹果专利,原理涉及数学物理知识很多,远比web开发者模仿出来的要复杂,而且不开源。
如果滚动的内容非常长(手机通讯录,微信朋友圈等),要做延迟渲染,生成瓦片,贴图,销毁或者回收,内存管理,这些工作必须由系统支持,而不是业务支持。
iOS支持Scroll Snapping with CSS Snap Points(点击访问),在滚动结束时对齐到内部元素,只需要两行css代码。
随着手机硬件的发展,浏览器和操作系统在图形渲染,网络API,数据存储等方面的差距越来越小。而且浏览器面向业务,对底层架构的抽象更好,一行css,一个javascript对象可以替代几千行java代码(or 几百行swift代码?)。很多只在操作系统中提供的功能,例如消息推送,也已经在浏览器实现。
Web的开放性让很多公司都对其添砖加瓦。你很难想象微软会往Android仓库中贡献代码,或者谷歌给IE浏览器解决bug,但他们确实都在为HTML5标准做贡献。另一个例子是Adobe,他们希望将Photoshop中的滤镜,图层混合模式等功能提供给web开发者,于是提议做了css filter,css blender。
http://sarasoueidan.com/demos/css-blender/
还有很多简单好玩的东西,以后会在博客中慢慢写。如有描述错误和不当,请批评指正。
作者:王伟