遥想在Flipboard实习时,我的第一个react项目里曾经有一个小需求就是为一个图集页面实现一个图片查看器。当时能力所限只实现了一版非常丑陋的勉强能用的,sad~如今再回头发现自己已经有能力实现一个体验更好的图片查看器,那么就弥补一下遗憾喽。
Demo
先放一下最后的成果。

功能
实现
整个项目是用react16实现的,手势控制使用了alloyfinger,相较于另一个非常知名的web手势库hammer.js,alloyfinger更小,虽然功能可配置能力不如hammer.js,但是也足够用了。开发环境使用了storybook,storybook有比较丰富的插件,不过我这里没用(唯一个有迫切需求的移动端模拟插件,还是在下个版本才放出。。。)测试使用jest + enzyme。
实现过程中,有几个需要考虑的要点在这里记录一下。
切换
其实滑动切页是很常见的需求,jQuery的插件也数不胜数,但是这里面有个问题。很多插件会把所有图片都预先渲染好,如果图片数量较大,对于性能还是有一定影响的。理想状态下,应该只需要渲染最多三张,包括当前页,上一页,下一页。每次切换后再更新新的三页。
但是这样会带来新的问题,首先react在进行列表渲染时需要知道每一项的key,来决定如何优化,组件key发生变化会重新渲染整个组件,很多情况下没有必要,此外为了支持渲染两张相同url的图片,只使用url作为key也不合适。所以最后使用url+图片数组index来作为id。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const displayMax = index + 2 > images.length ? images.length : index + 2; //获取下一页的index const displayMin = index - 1 < 0 ? 0 : index - 1; //获取上一页的index return ( ... {images.slice(displayMin, displayMax).map((url, ind) => ( <div key={url + (ind + displayMin)} className="image-slides-blackboard"> {loaded[url] ? ( <img className="image-slides-content" src={url} /> ) : Loading} </div> ))} )
|
其中images为url数组,index为当前页数。
另外一个问题是如果我们渲染所有图片,偏移量计算很简单index * window.innerWidth
,翻到下一页,index 加一即可,但是如果只渲染3张的话,偏移量始终为1 * window.innerWidth
,翻到下一页偏移量变化过程为1 * window.innerWidth -> 1 * window.innerWidth + 手指划过距离(直到触发index + 1) -> 1 * window.innerWidth
这样的话过渡动画会很奇怪,左右闪。所以我这里的实现为1 * window.innerWidth -> 1 * window.innerWidth + 手指划过距离(直到触发index + 1) -> 0 * window.innerWidth + 手指划过距离 -> 1 * window.innerWidth
这里会在chrome上又又又碰到一坑。。。chrome在计算transform偏移时,如1 * window.innerWidth + 手指划过距离(直到触发index + 1) -> 0 * window.innerWidth + 手指划过距离 -> 1 * window.innerWidth
会做优化将变化合并,即把0 * window.innerWidth + 手指划过距离
这一步忽略了,所以我在这一步通过手动修改其他属性,来强制触发。翻到上一页同理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| gesturesManager.on('touchEnd', e => { const swipeTrigger = window.innerWidth * 0.2; if (this.lastContainerOffsetX > swipeTrigger) { if (this.getMedianIndex() > 0 && this.state.index !== 1) { style.transform = `translate3d(${this.lastContainerOffsetX - (GUTTER_WIDTH + window.innerWidth) * 2}px, 0, 0)`; } else if (this.state.index === 1) { style.transform = `translate3d(${this.lastContainerOffsetX - (GUTTER_WIDTH + window.innerWidth)}px, 0, 0)`; } this.last(); } else if (this.lastContainerOffsetX < -swipeTrigger) { style.transform = `translate3d(${this.lastContainerOffsetX}px, 0, 0)`; if (this.state.index === 0) { style.transition = 'all 0.3s'; } this.next(); } style.transition = 'all 0.3s'; style.transform = `translate3d(${-( GUTTER_WIDTH + window.innerWidth ) * this.getMedianIndex()}px, 0, 0)`; this.lastContainerOffsetX = 0; this.isMoving = false; e.preventDefault(); });
|
(后来想到另外一种做法是监听transitionend事件,再触发翻页)
拖动
其实这里和滑动翻页纠缠在一起,核心就是如何确定触摸事件触发图片拖动还是滑动翻页。在FLipboard时这个问题通过滚动条交给浏览器来解决,但是滚动条在pc上很丑陋,所以这次决定还是自己控制。控制优先级为:当图片拖动到边界时,控制权交给滑动翻页。用户双击放大控制权交还图片。超出边界判定的规则是图片偏移量大于图片宽度减去视口宽度的1/2.
1 2 3 4 5 6
| if ( Math.abs(lastOffsetX + offsetX) > xRange && Math.abs(lastOffsetX + offsetX) > Math.abs(lastOffsetX) ) { isInLimit = false; }
|
优化和兼容
- 对于图片元素设置will-change提升为合成层使用GPU加速。
- 针对uc和webview中左滑后退,再touch事件中调用preventDefault。
- 移动端需要解决滚动穿透的问题的(最简单的方案body高度设为0)
未解决的问题
- pinch手势放大缩小的功能没有实现,核心问题在于如何计算放大中心。
- 组件进入的时候缺少过渡效果。
Comment and share