问题
最近跟着慕课网上的课程在做一个网易云音乐小程序,遇到了一个进度条回跳的 bug,这里记录一下踩坑和解决的过程。
具体情况见下图:
预期行为:在拖拽进度条之后,间接达到拖拽之后的地位
理论行为:在拖拽进度条之后,会首先回跳到拖拽之前的地位,而后再跳到拖拽之后的地位。
模仿调试的 bug
代码逻辑
无论如何,先来看一下代码的逻辑:
页面构造如下,左右两个 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
在监听歌曲的播放:
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
}
})
歌曲播放的时候,进度条要跟着走,这个函数就是用来实现该性能的。
解决方案
问题就在于:拖拽和歌曲播放是同时进行的,这两者都会对绑定同一个状态的数据进行批改,可能就是数据的抵触导致了最初渲染时回跳的 bug。
解决的计划很简略,这里参考视频的做法。其实很像 OS 中的过程互斥(这么说不精确,但能够近似了解)问题,进度条就相当于是互斥资源,咱们只有保障一个时间段内只有一个操作能够批改进度条就好了。具体做法是申明一个变量 isMoving
作为“锁”,在拖拽的时候置为 true
,并限度此时 onTimeUpdate
无奈批改数据;而在松手后置为 false
,并调用 seek
跳转到音乐的某个播放地位 A
。因为对 onTimeUpdate
来说,他获取的 currentTime
也是 A
地位对应的工夫,这样就不会发生冲突了。
批改代码后再来看一下拖拽成果,发现的确没有回跳的 bug 了:
你认为事件就这么完结了吗?No~~
真机调试的 bug
在确定模仿调试没问题的状况下,我关上手机进行真机调试,诡异的是,这个 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 就算解决了。当然,可能还会有其它更好的解决形式,后续我会找个工夫再看下能不能进行优化和改良,有思路的大佬也欢送留言指导。小程序的坑着实不少,然而我感觉应该享受这种踩坑后又从坑里爬出来的感觉。最初要特别感谢群里的 @疯子 大佬,多亏他的揭示,让我定位到问题的关键部位。