The problem

Recently, I was working on a netease cloud music mini program following a course on MOOCs. I encountered a bug that the progress bar bounced back. Here I record the process of stepping on the bug and solving it.

See the following figure for details:


Expected behavior: After dragging the progress bar, go directly to the dragged position

What it actually does: After dragging the progress bar, it jumps back to where it was before, and then to where it was after.

Simulate debugging bugs

Code logic

Anyway, let’s take a look at the logic of the code:

The structure of the page is as follows. The left and right text shows the time, but mainly the progress bar in the middle. Instead of using the slider, which was originally provided by the widget, the bar was made using a combination of movable-Area and movable-View, which defined a sliding-area. And movable-View is a slider you can drag in the middle. When you drag the slider, there’s an X that keeps track of how far you’re dragging it, and the onXChange event that listens for x changes, and the onTouchEnd event that listens for when you drop the slider. In addition, there is a progress component below, this is used to show progress, has played progress given a white style.

<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> Copy the code

Once you have determined that the change in X is due to user drag and drop, set the progress in onXChange according to the scale. Note that the setData rendering of the view layer is not performed when the user drags and drags – because the user may drag and drop frequently, we want to avoid the performance loss caused by frequent setData. So, here we just save the data and wait for rendering.

onXchange(event){
    if(event.detail.source == "touch") {        ratio = event.detail.x / (movableAreaWidth - movableViewWidth) 
        this.data.progress = ratio * 100    
        this.data.movableDist = event.detail.x
 } }, Copy the code

Once the user lets go, he is almost sure that he has dragged the slider to the target position. At this point, the official setData operation is performed and the seek method is called to redirect the song to the corresponding position

onTouchEnd(){      
    let toSec = totalSec * ratio
    this.setData({
        progress:this.data.progress,
        movableDist: this.data.movableDist,
 ['showtime.currentTime'] :this.timeFormat(toSec)  })  backgroundAudioManager.seek(toSec) }, Copy the code

So far, there doesn’t seem to be any problem. But don’t forget, we also have an onTimeUpdate that listens for songs:

backgroundAudioManager.onTimeUpdate((a)= > {
    let currentTime = backgroundAudioManager.currentTime          
    // Get the current activation time
    let sec = currentTime.toString().split('. ') [0]
    // Set the movableView progress
 let movableDist = (movableAreaWidth - movableViewWidth) * currentTime / totalSec  // Set progress-bar to progress  let progress = 100 * currentTime / totalSec  / / assignment  if(compareSec ! = sec){ this.setData({  movableDist,  progress,  ['showtime.currentTime'] :this.timeFormat(currentTime)  })  compareSec = sec  } }) Copy the code

This function is used to enable the progress bar to follow the song as it plays.

The solution

Here’s the problem: drag and song play happen simultaneously, both of which modify the data bound to the same state, and it’s possible that conflicting data causes the bounce bug in the final render.

The solution is very simple, and here’s how to do it. Much like the mutual exclusion problem in OS, the progress bar is a mutual exclusion resource, and we just need to make sure that only one action can modify the progress bar at a time. Declare a variable isMoving as a “lock”, set it to true during drag, and limit onTimeUpdate’s ability to modify the data. After release, set to false and call seek to jump to A certain playing position of the music. Since in the case of onTimeUpdate, currentTime is also the time corresponding to position A, there will be no conflict.

Check the drag effect after modifying the code and find that there is no bounce bug:


You think this is the end of it? No~~

Real machine debugging bug

After confirming that there was no problem with simulation debugging, I turned on the phone for real debugging. Strangely, the bug appeared again, and the probability was almost 100%. How could I tolerate it? So continue to think of a solution.

As mentioned earlier, “Call seek to jump to A certain play position of the music. For onTimeUpdate, currentTime is also the time of A.” This is not the case in a real machine debugging scenario.

Delayed update issues

OnTimeUpdate (currentTime) is not the currentTime, but the time before the jump is not updated, so the data calculated according to this time will be rendered to the progress bar. We will see the progress bar before the drag, and later, when the time is updated, the progress bar will jump back to where it was after the drag. If that’s true, it might explain the jump back. So how do you verify that?

We can print the formatted values of currentTime and Progress inside the onTimeUpdate function. If the two remain at about the same level, we can assume that they are in sync. If there is a big gap at some point, CurrentTime is not updated in a timely manner (Progress is modified via onXchange and will not be a problem).

console.log('currentTime:' + this.timeFormat(backgroundAudioManager.currentTime))
console.log('the progress: + this.data.progress)
Copy the code

The print result is shown in the following figure:

There was no drag at first, so of course currentTime and Progress remain at about the same level. Then, notice the red circle. I dragged the bar back at the red circle, so you can see that Progress suddenly became larger, but currentTime didn’t change (it’s still a very small number)! This verifies the hypothesis above, because currentTime was not updated in time, and it affected other data, so the progress bar jumped back to its previous position, and then currentTime was updated, so the time jumped from 00:07 to 02:11, and then returned to normal.

However, why in the real machine debugging will have this “delayed update” problem? At first I thought this was because seek was asynchronous and onTimeUpdate had beaten it to execution, but I tested it and found that it was synchronous. So, maybe there’s a delay in real machine debugging? Anyway, let’s see how we can fix this bug.

The solution

The root cause of the problem is that currentTime is used as the standard for onTimeUpdate and is assumed to be the correct data, which is sometimes incorrect due to delayed updates. So we can make a judgment. Once the data is found to be wrong (not updated), we will use Progress as the standard to modify the data (Progress does not make mistakes).

PS: Why not take Progress as the standard? Since progress is evaluated based on currentTime without drag, it is normal to use currentTime.)

How do you tell if the data is wrong? A rather clumsy and inelegant method is used here: when calling onTimeUpdate, get the actual number of current seconds and the ideal number of current seconds calculated based on progress. It is found through the test that the deviation between the two is not more than 2 under normal conditions, but in abnormal conditions (such as the part circled in red in the screenshot), the difference between the two will be very large, and the difference between them is about the difference before and after dragging the progress bar.

Thus, we can change the code to:

backgroundAudioManager.onTimeUpdate((a)= > {
    // setData only when there is no drag
    if(! isMoving){        let currentTime = 0
        if(Math.abs(backgroundAudioManager.currentTime - totalSec * this.data.progress/100) < 2) { console.log('sync')  currentTime = backgroundAudioManager.currentTime  } else {  console.log('out of sync')  currentTime = totalSec * this.data.progress/100  }  // Get the current activation time  let sec = currentTime.toString().split('. ') [0]  // Set the movableView progress  let movableDist = (movableAreaWidth - movableViewWidth) * currentTime / totalSec  // Set progress-bar to progress  let progress = 100 * currentTime / totalSec  / / assignment  if(compareSec ! = sec){ this.setData({  movableDist,  progress,  ['showtime.currentTime'] :this.timeFormat(currentTime)  })  compareSec = sec  }  } }) Copy the code

That sounds reasonable in theory, but how effective is it in practice? Real machine debugging take a look:


Because I recorded the screen and then converted it to GIF, the frame number is relatively low, but after repeated testing, there is no progress bar bounce bug.

At this point, the bug is solved. Of course, there may be other better solutions, I will find a time later to see whether it can be optimized and improved, if you have ideas, welcome to leave comments. There are a lot of holes in the mini program, but I think I should enjoy the feeling of stepping on the hole and getting out of the hole. And finally, a special thanks to the @Madman in the group for reminding me to locate the key part of the problem.

This article is formatted using MDNICE