- Published on
小程序中类抖音的交互实现方案
- Authors
- Name
- Hansuku
首先思考一个问题,如果想要在小程序中实现抖音上下切换、拥有漂浮物按钮等怎么办?
这个问题从 18 年 9 月开始一直让我头痛到睡不着觉,历时八个月,中间有过微信的更新,有过我们自己的努力,终于把这个坑爬出来了。下面会逐一做解决问题的分析,如果想看结果可以直接跳到结尾。
作为稍微有一点经验的前端,看到这道题的时候可能会代入传统网页的思想。
pageScrollTo
式
准备 N 个视频容器,在容器上做滑动、点击等事件监听,touchStart
时记录手指 XY 轴,结束时 diff XY 轴判断滑动方向然后调用 pageScrollTo
页面的滚动,滚动到上一个或下一个容器的位置。当然了,这是最基础的想法,且先不说touchStart
到touchEnd
件单靠 XY 轴改变值来判断是否滚动这样的触发条件可靠,视频一多起来实例简直要卡上天,并且如果想要实现手指拖多少视频容器滚动多少,就需要频繁调用 setData
,这在小程序中是一个相当高危的操作,即使做了节流性能依旧好不到哪里去,IOS 上可能通过 transition 来让他看起来不卡顿但是有段落感,安卓上直接会卡顿的异常明显。
pageScrollTo
升级版
第一个方案理应不动手直接 PASS,但是在我看过微视的小程序之后我还是决定一试。
微视的小程序通过创造一个 Video 容器和 N 个图片容器来做载体,在视频上滑动时,判断方向然后pageScrollTo
给 300ms 的过渡滚动到图片的位置并且给一定延迟,在确定滚动完成以后再次 pageScrollTo
并且过渡为 0ms 滚动回视频的位置。
这样的逻辑看起来可能有点扯淡,但微视他们确实这样做到了,只是效果不尽人意,第一,必须等用户手指离开以后一次性滚动到目标位置,虽然pageScrollTo
给延迟以后的流畅性还算不错,但用户在滑动的时候多少显得生硬。第二,微视把所有的事件监听挂载在了视频上方的一个容器当中,这极度容易导致误触,直到现在他们也没有解决在安卓上,点击漂浮于视频上方的按钮会导致视频切换,这也变相印证了第一个方案里依靠touchStart
和touchEnd
判断滑动与否是行不通的,他太容易误触了。
偷梁换柱式
在打算用内嵌网页来完成这个效果的时候,我们突然发现了一款名叫“趣看看短视频的”小程序,他给了我一些灵感。 由于在 19 年 2 月前,小程序还没有同层渲染,所以 Video
组件不能被放到 Swiper
当中,所以本身依靠Swiper
来实现这样的东西似乎不大可能,但是这个小程序确实依靠Swiper
来做到了手指拖动 十分舒适的切换。 他在Swiper
中放的不过是一些视频的封面图,也就是很多个Image
标签,视频被放置在Swiper
的上方,依靠绝对定位来漂浮。在视频播放时,Video
标签的 left
是0
,视频暂停或在滑动状态时,Video
标签的 left
被切换到了9999
,然后在滑动的时候,Swiper
有一个滑动动画结束的钩子animationFinsh
,这个钩子被执行以后再次把Video
标签的 left
变成0
,这样就完成了一整套滑动效果。
这样看起来很美好,而且后继我仿照这样的思路做出来一个在线上跑了 4 个月的版本,但它却有个一个致命的交互问题,如果要切换视频,就必须暂停视频,因为滑动事件是Swiper
的,我们把视频盖再了他上方会导致滑动事件完全自闭,所以只能在第一次滑动的时候把视频暂停并且推到left:9999
,再次滑动来完成切换。
再后来,是今年二月,春节回来以后我们迎来了喜大普奔的消息(以下是官方原话):
小程序原生组件因脱离 WebView 渲染而存在一些使用上的限制,为了方便开发者更好地使用原生组件进行开发,我们对小程序原生组件引入了 同层渲染 模式。通过同层渲染,小程序原生组件可与其他内置组件处于相同层级,不再有特殊的使用限制。
现阶段,小程序 video 组件 已切换至同层渲染模式。在该模式下,video 组件可以做到:
1、直接通过 z-index 属性对 video 组件进行层级控制;
2、无需使用 cover-view、cover-image 组件来覆盖 video 组件;
3、可在例如 scroll-view、swiper、movable-view 等内置组件中使用 video 组件;
4、可通过 CSS 对 video 组件进行控制;
5、video 组件不会遮挡 vConsole。
基础库 v2.4.0 及以上版本已默认开启 video 同层渲染,其他原生组件如 input、map、canvas、live-player、live-pusher 等也将逐步切换至同层渲染模式。
划重点,敲黑板,可以在Swiper
中使用video
组件了
于是,在 4 月初的时候我们终于打算依靠这个特性来修改原来的代码 首先我们设计思路是,永远保证只有一个视频容器,因为我们的数据不是列表制的,每一个视频都有对应的上一个或者下一个视频,所以需要在请求了视频详情以后拿到上一个下一个的封面图 ID 等信息动态插入到Swiper
的列表当中,图片的消耗相对是十分低的,这样用户在这个页面滑动了 500 次,即使不考虑回收也依旧可以让小程序流畅运行。
先上 wxml 部分的代码
<swiper class="swiper-container" vertical="{{true}}" duration="300" current="{{current}}" bindanimationfinish="swiperSuccess" bindtouchstart="swiperStart" bindtouchend="swiperTouchEnd" id="swiperInstant" data-e-animationfinish-so="this" data-e-animationfinish-a-a="{{current}}">
<swiper-item wx:key="index" wx:for="{{loopArray0}}" wx:for-item="item" wx:for-index="index">
<block>
<block wx:if="{{current == index}}">
<video src="{{cdn_name + data.video_path}}" controls="{{false}}" autoplay="{{true}}" poster="{{cdn_name + data.face_img}}" loop="{{true}}" id="{{'video' + index}}" class="video-container" bindplay="startPlay" bindtouchend="handlePlayClick" enable-progress-gesture="{{false}}" bindtimeupdate="handleTimeUpdate" data-e-touchend-so="null" data-e-touchend-a-a="{{index}}"></video>
</block>
<block wx:else>
<image src="{{cdn_name + item.$original.face_img}}" style="{{item.$loopState__temp2}}"></image>
</block>
</block>
<block wx:if="{{!isPlay}}">
<view class="pause-container" bindtap="handlePlayClick" data-e-tap-so="null" data-e-tap-a-a="{{current}}">
<image class="pause-img" src="{{cdn_name + '/rush_rabbit/img/video_play_v4.png'}}"></image>
</view>
</block>
</swiper-item>
</swiper>
可以看到 核心部分的代码其实就是wx:if
的那个判断,我们记录了Swiper
的 current
,并且对 swiper-item
的下标做比对来判断当前用户看到的是哪个 swiper-item
,一致则展示Video
组件,否则展示Image
组件。讲道理本身wx:key
的值不应该设置下标,但是考虑到我们并不会删除列表里的数据造成大量的 diff,也好取值,就用了 index
。 需要注意一点的是,视频上我绑定的点击事件会和Swiper
的bindanimationfinish
冲突,这里我也很莫名其妙,但是他确实执行了,为了避免导致bindanimationfinish
的误触,我们需要记录手指点击位置和松开位置,然后在bindanimationfinish
判断他是点击还是滑动,做响应的处理
// 点击事件
{
key: "swiperStart",
value: function swiperStart(e) {
this.setState({
startTime: e.timeStamp,
showGoods: false,
showShare: false,
pointer: {
startX: e.changedTouches[0].clientX,
startY: e.changedTouches[0].clientY,
endX: e.changedTouches[0].clientX,
endY: e.changedTouches[0].clientY
}
});
}
},
// 点击结束
{
key: "swiperTouchEnd",
value: function swiperTouchEnd(e) {
var startX = this.state.pointer.startX;
var startY = this.state.pointer.startY;
this.setState({
pointer: {
startX: startX,
startY: startY,
endX: e.changedTouches[0].clientX,
endY: e.changedTouches[0].clientY
}
});
}
},
// 滑动动画结束
{
key: "swiperSuccess",
value: function swiperSuccess(current, e) {
var _state$pointer = this.state.pointer,
startX = _state$pointer.startX,
startY = _state$pointer.startY,
endX = _state$pointer.endX,
endY = _state$pointer.endY;
// 如果点击屏幕的位置和松开屏幕的位置完全一致,则return
if (startX == endX || startY == endY) {
return;
}
var id = this.props.videoReducer.listData[e.detail.current].id;
// 切换视频
this.props.changeID(id, 'switch');
// 暂停视频
var video = _index2.default.createVideoContext("video" + current,this.$scope);
video.pause();
}
}
这是第一个版本的代码,其中还有一些可优化的点,甚至我们在思考是否可以加入 3 个 Video
容器来做预加载,但起码现在的效果,已经甩开市面上的小程序一大截了,至少目前快手、微视的小程序体验都十分糟糕,而我认为依靠我们的方案至少是能够实现 60 分的产品。