| 导语 手Q终端原生的图片预览器支持图片翻页和各种手势,这些用H5怎样实现?基于alloyFinger,本文将介绍在手Q动漫上的图片预览组件是如何做到媲美原生体验的手势效果,同时也介绍一下关于图片手势效果里隐含的一些细节。希望对要实现手势交互和动画的前端同学有所启发。
作者:朱晓华--腾讯web前端工程师
@IMWeb前端社区
先来看实现效果。目前已经上线的图片预览组件的路径如下:手Q动态——动漫——社区——点击图片。
类比手Q的AIO里的图片预览器,支持的手势和功能分别如下:
手Q动漫这里之所以没有直接用手Q原生的图片预览器,而是新造一个轮子,主要原因是手Q动漫的图片预览器有一些定制的功能和ui展示,用web来实现更快捷可控一些。从上表可以看出,除了旋转图片之外,基本上跟手Q原生体验无异。旋转图片在alloyFinger中有提供方法支持,但由于本需求中使用场景少而且涉及更复杂的坐标变换,因此我目前还没添加上。后续工作量许可的情况下会支持。
图片预览组件目前是基于alloyFinger.js来做手势支持,transform.js来做CSS3的变换,to.js来做动画的过渡函数。
关于alloyFinger.js组件 https://github.com/AlloyTeam/AlloyFinger 组件提供了单击、双击、长按、拖动、旋转等手势支持。基于这些手势有很多玩法。这里就不详细叙述了。
关于transform.js组件
http://alloyteam.github.io/AlloyTouch/transformjs/
alloyFinger只提供了手势支持,但手势具体要实现的图片位置变换或者缩放的效果,需要由transform.js来支持。transform.js给dom元素添加了css3的属性对应的js属性,例如translateX, translateY, translateZ, scaleX, scaleY, scaleZ, rotateX, rotateY, rotateZ, skewX, skewY, originX, originY, originZ。
获取属性只需要var x = ele.translateX
,而设置css3属性只需要ele.transalteX = 10
,非常方便。
关于to.js组件 https://github.com/AlloyTeam/AlloyFinger/blob/master/asset/to.js to.js组件以requestAnimationFrame为基础,提供了设置dom元素属性的过渡函数,支持传入渐入渐出相关的函数。用法如下:
理论上支持图片无限翻页,这里实现的方法是: 任何时候都保持三张图片在容器中并且中间的图片在屏幕内。翻页之后再通过删除前一张和补充后一张来维持三张图片的状态。 这样的好处是:更少的dom节点和更好的动画性能、支持用户主动添加和删除图片、支持异步添加图片。
正常情况下,图片缩放是只需要设置scale为你所需要的倍数就行了。双击缩放和双指缩放的原理差不多,都是需要先设置css3的transform的坐标变换中心origin,只不过双指缩放是以两个手指连线的中点作为缩放原点。因此开始的代码只需要是:
但在放大2倍的情况下,两个手指再次放到图片上另一个位置缩放的时候,图片会跳动。原因是,在有scale的情况下,改变了origin值,要保持图片位置不变,则需要同时改变translate来平移图片。 例如,对于宽高都是100的图片,在当前origin=(0,0),scale=2,translate=(0,0)的情况下,当你修改origin=(50,50)时,scale=2不变,应有translate=(50,50)。 ps:对于transform.js的origin,默认是0表示是图片50%的位置,只能设置px值不能设置比例 例如下图中,图1是当前图片从当前中心点放大两倍的情况,实际上等同与从图2平移到图3。
“容易证明”得以下数学公式↓↓↓ 以X轴为例,假设放大倍数是s,计算新的translateX的数学公式如下:
谨记这个公式,下面基本上所有涉及到缩放状态的变换都以这个为基础。
在这个公式的使用时机是每次touchstart的时候,都要重新调整origin和translate的值,然后手指缩放的touchmove里再对scale做改变。就能实现多次变换位置的缩放了。代码例如:
图片放大之后,支持拖动图片查看细节。实现的原理很简单,touchmove的时候,改变图片的translate值即可。对比手Q的AIO的图片预览,在拖动图片到图片边缘的时候,检测边界并禁止继续拖动。 (1) 当图片的缩放原点origin为(0,0)时 以X轴为例,假设图片宽度为w,放大倍数为s,则translateX的区间为
图示边界的四种情况:
(2) 但实际过程中,因为图片的原点origin不一定是(0,0),因此上述区间不适用
解决方法也简单,根据当前的origin和translate,通过translateX2 = translateX1 + (originX2 - originX1) * s
公式,计算出图片当前位置在origin=(0,0)的时候translate应该是哪些值。然后再套用上面的区间来判断边界即可。
当图片放大再缩小的时候,图片有可能还是超出边界了。因此,在手指松开之后,需要让图片自动贴近到该方向的屏幕边缘。 自动贴边得益于上述的边界检测的方法,在touchend中判断超出边界之后,自动把translate设置到最近的边界值。
单指拖动图片然后松开手指时,手Q原生的图片预览器有继续滑动一段距离的惯性效果。 滑动到终点之后,图片真正停在的点是在延长点上。那延长点要怎么计算呢,这里可以用向量的知识来指导坐标的计算方法。
假设起点起点坐标(x1, y1),终点坐标(x2, y2),滑动距离是l,需要计算延长点(x3, y3)的公式如下:
当然这里有个小问题,就是滑动距离l的定义。如果拖动的起点和终点距离很小,那么滑动距离也应该很小才对。否则就导致轻轻拖动一下,惯性却非常大。因此滑动距离是拖动距离的一个小比例值。
上述的实现惯性的方法其实只能算近似模拟,不符合现实生活中曲线运动的问题的惯性运动轨迹。 实际上的运动延长线的方向,应该是曲线在终点位置的切线。而且实际的惯性滑动距离,也是跟当前的速度有关,而不是一个恒定的比例值。这里后期优化的时候,会考虑这些点。
交互上,图片放大和缩小是有倍数限制的,超过最大/最小倍数值的时候,会让用户继续放大一部分,但再超过一定的阈值之后会停止放大,并在手指松开之后回弹到最大/最小倍数。这样的交互形式让用户对放大缩小的最大限制有一个直观的了解,避免生硬的交互体验。 这里的实现原理很简单:在alloyFinger的pinch回调中,设置最大倍数为max+n,其中n为超出阈值。在touchend的回调中,设置缩小值回弹到max。
感谢@dntzhang 提供alloyFinger的支持,感谢@javinzhong 提供的先行实验版。我的工作是站在两位巨人的肩膀上才得以实现。 总的来说,这个项目除了加深自己对web手势和css3动画的理解之外,对于深入挖掘图片手势的细节和效果也是很有帮助。很多产品细节是需要不断地打磨和优化的,对自己的工作需要多的细节追求和精力倾注才能有好的成果,与君共勉(不小心抖了个鸡汤-_-)
扫码下方二维码,
随时关注更多前端干货文章!
▼
微信:IMWebTech