最近跟着慕课网上的课程在作一个网易云音乐小程序,遇到了一个进度条回跳的 bug,这里记录一下踩坑和解决的过程。html
具体状况见下图:小程序
预期行为:在拖拽进度条以后,直接到达拖拽以后的位置异步
实际行为:在拖拽进度条以后,会首先回跳到拖拽以前的位置,而后再跳到拖拽以后的位置。ide
不管如何,先来看一下代码的逻辑:函数
页面结构以下,左右两个 text
显示时间就不说了,主要是中间的进度条。这个进度条没有使用小程序原生提供的 slider 来作,而是采用 movable-area 和 movable-view 相结合的方式,movable-area 划出了一块可供滑动的区域,而 movable-view 则是中间能够拖拽的滑块。拖拽滑块的时候会有个 x
来记录拖拽距离,同时绑定 onXChange
事件监听 x
的变化,绑定 onTouchEnd
事件监听拖拽松手的动做。另外,下面还有一个 progress 组件,这个是用来显示进度的,已经播放的进度给个白色样式。性能
<view class="container"> <text class="time">{{showtime.currentTime}}</text> <view class="control"> <movable-area class="movable-area"> <movable-view class="movable-view" direction="horizontal" damping="1000" x="{{movableDist}}" bindchange="onXchange" bindtouchend="onTouchEnd"> </movable-view> </movable-area> <progress percent="{{progress}}" stroke-width="4" backgroundColor="#969696" activeColor="#fff"></progress> </view> <text class="time">{{showtime.totalTime}}</text> </view>
一旦肯定 x
的变化来源于用户的拖拽,就在onXChange
里根据比例关系设置好进度。这里要注意的是,在用户拖拽没松手的时候先不进行 setData
渲染视图层的操做 —— 由于用户可能会频繁进行拖拽,咱们要避免频繁的 setData
带来的性能损耗。因此,这里只是把数据保存下来,等待渲染。测试
onXchange(event){ if(event.detail.source == "touch"){ ratio = event.detail.x / (movableAreaWidth - movableViewWidth) this.data.progress = ratio * 100 this.data.movableDist = event.detail.x } },
用户一旦松手,基本就能够肯定他已经把滑块拖拽到了目标位置,这时候就进行正式的 setData
操做,同时调用 seek
方法让歌曲跳转到对应的位置去播放优化
onTouchEnd(){ let toSec = totalSec * ratio this.setData({ progress:this.data.progress, movableDist: this.data.movableDist, ['showtime.currentTime']: this.timeFormat(toSec) }) backgroundAudioManager.seek(toSec) },
目前来看,好像并无什么问题。不过别忘了,咱们还有一个 onTimeUpdate
在监听歌曲的播放:this
backgroundAudioManager.onTimeUpdate(() => { let currentTime = backgroundAudioManager.currentTime // 获取当前激活时刻 let sec = currentTime.toString().split('.')[0] // 设置movableview进度 let movableDist = (movableAreaWidth - movableViewWidth) * currentTime / totalSec // 设置progress-bar进度 let progress = 100 * currentTime / totalSec // 赋值 if(compareSec != sec){ this.setData({ movableDist, progress, ['showtime.currentTime']: this.timeFormat(currentTime) }) compareSec = sec } })
歌曲播放的时候,进度条要跟着走,这个函数就是用来实现该功能的。spa
问题就在于:拖拽和歌曲播放是同时进行的,这二者都会对绑定同一个状态的数据进行修改,可能就是数据的冲突致使了最后渲染时回跳的 bug。
解决的方案很简单,这里参考视频的作法。其实很像 OS 中的进程互斥(这么说不许确,但能够近似理解)问题,进度条就至关因而互斥资源,咱们只要保证一个时间段内只有一个操做能够修改进度条就行了。具体作法是声明一个变量 isMoving
做为“锁”,在拖拽的时候置为 true
,并限制此时 onTimeUpdate
没法修改数据;而在松手后置为 false
,并调用 seek
跳转到音乐的某个播放位置 A
。因为对 onTimeUpdate
来讲,他获取的 currentTime
也是 A
位置对应的时间,这样就不会发生冲突了。
修改代码后再来看一下拖拽效果,发现确实没有回跳的 bug 了:
你觉得事情就这么结束了吗?No~~
在肯定模拟调试没问题的状况下,我打开手机进行真机调试,诡异的是,这个 bug 再次出现了,而且机率几乎是 100%,这怎么能忍呢?因而继续想方法解决。
在前面说过,“调用 seek
跳转到音乐的某个播放位置 A
,对于 onTimeUpdate
来讲,他获取的 currentTime
也是 A
位置对应的时间。” 在真机调试的场景下并非这样。
咱们假设一下,调用 seek
进行跳转后,onTimeUpdate
内部获取的 currentTime
不是当前时间,而仍然是跳转前的时间,也就是说它的时间没有更新过来,那么按照这个时间计算的数据最后渲染到进度条上,咱们看到的就还会是拖拽以前的进度条,而在稍后,时间更新过来了,进度条再次跳回到拖拽以后的位置。若是真的是这样,或许就能够解释回跳的缘由了。那么怎么验证呢?
咱们能够在 onTimeUpdate
函数内部打印格式化的 currentTime
和 progress
的值,若是这二者保持在差很少的水平,那么能够认为它们是同步的,若是某个时刻出现了很大的差距,那么就说明 currentTime
没有及时进行更新(progress
是经过 onXchange
修改的,不会有问题)。
console.log('currentTime:' + this.timeFormat(backgroundAudioManager.currentTime)) console.log('progress:' + this.data.progress)
打印结果见下图:
一开始没有拖拽,因此理所固然, currentTime
和 progress
保持在差很少的水平。而后,注意看红圈部分,红圈的时刻我日后拖拽了进度条,因此能够看到 progress
忽然变大了,可是这时候的 currentTime
居然没有跟着改变(仍然是一个很小的数)!这就验证了上面的假设了,由于 currentTime
没有及时更新,而它又影响着其它数据,因此致使进度条又跳回到以前的位置,而稍后 currentTime
更新了,因此时间又从 00:07 骤增到 02:11,此后才恢复正常。
不过,为何在真机调试下就会有这个“延迟更新”的问题呢?一开始我还猜测这是由于 seek
是异步的,onTimeUpdate
抢先它执行了,但通过测试发现它实际上是同步的。因此,或许是由于真机调试下有延迟?这个先无论了,如今咱们先看一下怎么解决这个 bug。
问题的根源在于,咱们在 onTimeUpdate
中是拿 currentTime
做为标准去进行数据修改的,而且认定 currentTime
是正确的数据,但其实,因为延迟更新的问题,这个数据有时候是错误的。因此咱们能够作一个判断,一旦发现数据是错误的(没更新过来),咱们就改用 progress
做为标准去进行数据修改(progress
不会出错)
PS:为何不统一以 progress
做为标准呢?由于在不拖拽的状况下,progress
是基于 currentTime
进行计算的,因此正常状况仍是得用 currentTime
)
如何判断数据是错误的呢?这里用了一个比较笨+不优雅的方法:在调用 onTimeUpdate
的时候,拿到实际的当前秒数以及基于 progress
计算的理想的当前秒数。通过测试发现,正常状况下这二者的误差不会大于2,而在不正常的状况下(好比截图红圈部分),这二者相差会很大,彼此的差距大概就是咱们拖动进度条先后的差距。
这样,咱们就能够把代码改为:
backgroundAudioManager.onTimeUpdate(() => { // 不拖拽的时候才setData if(!isMoving){ let currentTime = 0 if(Math.abs(backgroundAudioManager.currentTime - totalSec * this.data.progress/100) < 2){ console.log('同步') currentTime = backgroundAudioManager.currentTime } else { console.log('不一样步') currentTime = totalSec * this.data.progress/100 } // 获取当前激活时刻 let sec = currentTime.toString().split('.')[0] // 设置movableview进度 let movableDist = (movableAreaWidth - movableViewWidth) * currentTime / totalSec // 设置progress-bar进度 let progress = 100 * currentTime / totalSec // 赋值 if(compareSec != sec){ this.setData({ movableDist, progress, ['showtime.currentTime']: this.timeFormat(currentTime) }) compareSec = sec } } })
理论上好像说得过去,实际效果如何呢?真机调试看一下:
由于我是录屏而后转成 GIF 的,帧数比较低,可是通过反复测试,确实没有进度条回跳的 bug 了。
到这里,bug 就算解决了。固然,可能还会有其它更好的解决方式,后续我会找个时间再看下能不能进行优化和改进,有思路的大佬也欢迎留言指点。小程序的坑着实很多,可是我以为应该享受这种踩坑后又从坑里爬出来的感受。最后要特别感谢群里的 @疯子大佬,多亏他的提醒,让我定位到问题的关键部位。