需求
最近几天在忙着搞公司项目的一个新的需求,原因是这样的:公司准备开发一个偏向于社交娱乐项的小程序,其中首页是可以看到用户发的话题帖子之类的,每个帖子都至少包含一张图片或者一个视频,
然后产品那边希望首页可以实现instagram的交互效果,效果图如下(本来应该是显示图片,奈何我的gif图片大小超过两兆,不能上传,所以我就用表情包替换了):
嗯,大致上这个就是需求的背景,然后就是每个帖子的高度是不确定的,高度大概在500~600px之间。
实现思路
一开始接到这个需求,其实我心里还是有点慌的,毕竟有一段时间不怎么接触小程序,也不知道小程序更新到什么程度,文档更新到什么程度。仔细分析一下项目需求,大致上可以归类为两个:交互 和 性能优化。
性能优化
因为首页是一个长列表,众所周知,页面一旦渲染的节点过多,就会卡顿,更何况是小程序,并且小程序是分为逻辑层和渲染层,两者通过setData链接,所以处理的时候需要注意两点:
- setData的数据量不能太大,记得好像是有个大小限制,,忘了是多少,也懒得找:clown_face:,你们可以自己在官方文档上找一下;
- 页面能够渲染的帖子数量是有限的,在这里,我是控制为最多渲染25个帖子。
处理
针对于长列表的优化,官方也有相应的组件-recycle-view,但是貌似并不符合项目需求,所以被我pass掉了。
虽然没用官方的组件,但是在组件的文档里面把对于长列表得性能优化解释一遍,这里摘抄一下重点:
核心的思路就是只渲染显示在屏幕的数据,基本实现就是监听 scroll 事件,并且重新计算需要渲染的数据,不需要渲染的数据留一个空的 div 占位元素。
其实也就是设置一个变量控制该数据是否可以渲染,如果是不能够渲染得话,那我们就用一个空的view取代它,需要注意的失败的是:空的框架高度需要设置为帖子的高度,这样子才不会闪屏。
针对这种思路,我们就可以确定其中一种长列表的性能优化的解决思路:
- 将数据分为二维数组,这样子就可以限制每次setData的数据量,等待数据渲染完成之后,获取每组数据所占用的总高度,这里的高度是为了在改组数据不渲染时设置占位框的高度。
/**
* 获取 有渲染,但是高度还没获取到的分组 的高度
*/
_getGroupListHeight() {
this.data.list.forEach((item, index) => {
if (item.show && !item.height) {
const id = 'XXXXXXXX' // 组的id
let query = wx.createSelectorQuery()
query.select(id).boundingClientRect(rect => {
this.data.list[index].height = rect.height
}).exec();
this.getTopicHeight(item.data, index) // 获取列表中每个话题的高度,用于计算滑动时要滚动的距离
}
})
}
上面就是一个简单获取每组的高度的代码实例,当该组数据有被渲染但是高度不明的情况下,就会去获取,加一步判断是为了防止重获获取组的数据,造成不必要的浪费。在获取每组数据的高度时,还会对应去
获取该组的每个帖子的高度,这样子是为了后面实现 仿instagram 交互做准备。
- 获取到了每组数据的高度,接下来,我们就可以监听页面滚动的高度,从而控制需要渲染的数据,需要注意的一点是,我们需要在该组的数据基础上,多渲染上两组和下两组数据,目的是防止用户快速滑动的时候出现白屏的不友好体验。当然也可以根据自己的需要多渲染几组。
/**
* 页面滚动
* [@param](/user/param) e
*/
onPageScroll(e) {
// android页面滑动处理(非仿instagram版本)
if (!this.data.isIos && !this.data.scrollBoxInfo.canUseScrollBox) {
// 1. 处理当前页面正在播放的视频
if (this.data.currentPlayingId && Math.abs(e.scrollTop - this.data.scrollTop) > 100) {
this.selectComponent(this.data.currentPlayingId).pauseVideo()
this.data.currentPlayingId = ''
}
// 2. 处理 Andorid 渲染的分组数据
this._dealAndroidScroll(e)
// 3. 处理视频自动播放
if (this.timer) {
clearTimeout(this.timer)
}
this.timer = setTimeout(() => {
this.data.scrollTop = e.scrollTop // 记录下当前的滚动距离,滑动暂停视频播放的时候需要用到
this.handleAutoPlay(e) // 视频自动播放
}, 300)
}
}
/**
* Android 监听滚动,动态设置分组
* [@param](/user/param) {Object} e
*/
_dealAndroidScroll(e) {
let max_height = 0 // 最大高度
for (let i = 0; i <= this.data.topicScroll.show_index; i++) {
max_height += this.data.list[i].height
}
let min_height = max_height - this.data.list[this.data.topicScroll.show_index].height // 最小高度
// 超过,+1
if (e.scrollTop > max_height && this.data.topicScroll.show_index < this.data.list.length - 1) {
++this.data.topicScroll.show_index
this._dealListShow(this.data.topicScroll.show_index)
}
// 小于,-1
if (e.scrollTop < min_height) {
--this.data.topicScroll.show_index
this._dealListShow(this.data.topicScroll.show_index)
}
}
这里没有对代码进行过滤,代码实现的效果就是监听滚动到的位置,并且设置渲染分组数据,因为有些是视频帖子,所以需要在滚动完成之后实现自动播放视频的功能。这段代码只是处理anroid平滑滚动的情况,因为代码涉及到 instagram 交互的实现。
对于长列表性能优化的思路大致上就这样:数据分为二位数组,渲染指定分组的数据,减少渲染的数据量和需要渲染的节点数量,
不需要渲染的数据就用指定高度的空的view替代,指定高度是为了防止闪屏。
仿 instagram 交互实现
从上面的 instagram 交互视频可以看出来,我们需要监听用户的手势滑动从而控制帖子的切换,并且每次切换只是切换一个帖子。知道了交互的详情,我们就可以展开想象了,大致上可以给个基本的实现思路:
- 监听手指点击和手指离开的事件,记录下手指点击的高度 && 手指离开的高度,用来判断用户滑动的距离和方向;记录下手指点击 和 手指离开的时间,可以粗略用来判断用户当前的滑动行为是快滑还是慢滑。根据上面的得到的信息,我们基本上实现滑动切换帖子的操作:
/**
* 监听手指点击操作
* [@param](/user/param) e
*/
touchStart(e) {
this.data.topicScroll.startTimeStamp = new Date().getTime() // 记录下当前手指点击事件
this.data.topicScroll.startPosition = e.changedTouches[0].clientY // 记录下手指开始点击的位置
},
/**
* 手指离开屏幕
* [@param](/user/param) e
*/
touchEnd(e) {
const diffTime = new Date().getTime() - this.data.topicScroll.startTimeStamp // 手指离开的时候的时间戳
const clientY = e.changedTouches[0].clientY // 手指离开屏幕的位置
const diffY = Math.abs(clientY - this.data.topicScroll.startPosition) // 手指滑动的距离
const direction = this.data.topicScroll.startPosition - clientY > 0 // 手指滑动的方向,true为向上滑,false为向下滑
const scrollInfo = this.data.topicScroll
// 1. 第一个节点手指向下滑动 && 最后一个节点手指向上滑动 不做操作
if (!scrollInfo.parent_index && !scrollInfo.child_index && !direction) {
return
} // 第一个节点向下滑动不做操作
if (scrollInfo.parent_index === (this.data.list.length - 1) && scrollInfo.child_index === (this.data.list[scrollInfo.parent_index].data.length - 1) && direction) {
return
} // 最后一个节点向上滑动不做操作
// 2. 根据滑动的方向,判断需要滚动到哪个节点下
const can_move = (diffTime < 100 && diffY > 50) || (diffTime >= 100 && diffY > 80) // 是否可以滑动,手势滑动判断依据
if (can_move) {
if (direction) {
if (scrollInfo.child_index === 4) {
++scrollInfo.parent_index
scrollInfo.child_index = 0
} else {
++scrollInfo.child_index
}
} else {
if (scrollInfo.child_index === 0) {
--scrollInfo.parent_index
scrollInfo.child_index = 4
} else {
--scrollInfo.child_index
}
}
}
// 3. 处理滚动
this._dealScroll(can_move ? pausePlayId : '', can_move)
}
根据监听到信息,可以判断是否切换以及切换到那个帖子的操作,所以我们只需要根据面已经获取到的帖子高度就可以计算出来要滚动的高度。
- 根据第一点的操作,我们就可以实现 仿Instagram 交互效果,但是忽略了致命的一点:页面的惯性滚动。也正是因为这个所谓的“惯性滚动”,我多花了几天的时间去研究交互的实现💥
众所周知,为了使得页面的滑动更加流畅,当我们滑动停止的时候,页面就像会产生惯性一般,自动的滑动一定距离才停下。
安卓下默认有惯性滚动,而在 iOS 下需要额外设置-webkit-overflow-scrolling: touch的样式
而第一点方案实现的大前提是页面不能拥有惯性滚动,否则的话页面无法准确滚动到指定位置。
解决方案
- ios的解决方案比较简单,我们只需要设置 -webkit-overflow-scrolling: auto 的样式即可。
- 比较麻烦的是android的实现,一开始我是上网找了不少的资料去实现取消页面的惯性滚动,毕竟页面滚动的性能是最好的。不过很可惜,我没有找到可行的方案,所以我选择退而求其次,模拟手指的滑动。经过对小程序文档的浏览,我选择可两种比较可能实现的方案:①使用wxs实现手指滑动的效果;②使用scroll-view的fast-deceleration
属性。显而易见,使用wxs方案实现比较复杂,所以我一开始选择scroll-view这个方案。
android实现思路
scroll-view方案实现的思路与ios类似,比较不同的是页面的滚动,因为ios是可以直接使用页面滚动的,即wx.pageScrollTo,而scroll-view则是使用scroll-into-view 或者 scroll-top,为了减少操作,我是直接使用了scroll-top这个属性。正当我信心满满的时候,做出来的效果却是打了我的脸:每次滚动到指定位置的时候都会抖动一下,虽然没有仔细去查明原因,不过
我猜想是页面滚动依然存在一定的惯性滚动,当我们设置了scroll-top的时候,惯性滚动的存在会使得列表偏移位置,最后在偏移回来,这样子看起来就像是抖动了一下。
正当我准备放弃scroll-view这个方案的时候,无意中让我发现了scroll-view的api接口,本着想试一下新接口的心态,我尝试一下使用官方的api接口控制scroll-view滚动,神奇的是,成了!!!!
(此处为效果图)
既然scroll-view做出来的效果勉强还可以,我就直接pass后面的wxs方案,毕竟那一块的逻辑应该会比较复杂。
因为时使用了scroll-view的api接口,支持的版本库比较高,是2.14.4,虽然大部分的用户可以支持,但还是要兼容少部分的用户,所以android部分我是搞了一个scroll-view版本和平滑滚动版本
fast-deceleration这个特性在开发者工具那里貌似不生效,但是手机预览却是可以,不知道是开发工具的问题还是说只是兼容了部分手机(我这边测试了小米和华为这两款,没更多的手机测试了:clown_face:)
demo
说了这么多,也不一定有人看得下去,demo呈上
结尾
在调试这个效果的时候,遇到不少的坑,其中有一个坑印象巨深,在此记录一下:
touch 期间 touchstart 的目标节点被移除,则对应的 touchend 事件会因为没有目标节点而缺失。
遇到这个坑是因为小程序同个页面不允许存在多个视频,所以需要将没有播放的视频使用图片替换,需要播放的视频的时候就替换回来,这样子,就会出现上面的情况,导致无法监听到touchend事件,整个列表停在原地,没有滚动到指定位置。