Nativescript: ListPicker on Android emits selectedIndexChange events while scrolling

Created on 26 Apr 2019  路  6Comments  路  Source: NativeScript/NativeScript

Environment
Provide version numbers for the following components (information can be retrieved by running tns info in your project folder or by inspecting the package.json of the project):

  • CLI: N/A
  • Cross-platform modules:
  • Android Runtime: 7, 8 & 9
  • iOS Runtime: 12
  • Plugin(s): none

Describe the bug

On Android, the ListPicker continuously emits selectedIndexChange events as the user interacts with the list wheel. This results in UI jank when selectedIndexChange handler performs a heavy operation.

The same problem does not happen on iOS because iOS only emits a selectedIndexChange event once the user releases the list wheel and takes the finger off the screen.

To Reproduce

  1. Open demo app using NativeScript Preview on Android
  2. Use the ListPicker to attempt to select element
  3. As you select elements further down the list, you'll see increasing jank around 6th item in the list
  4. By 10th item, it'll completely lockup the screen for a few seconds

Repeat this on iOS to see that it's not a problem on iOS. Also, note that the selectedIndex value on the screen does not change on iOS until you let go of the input field.

Expected behavior

I would expect the behaviour to be consistent between iOS and Android. The behaviour on iOS makes sense because there is no point updating the UI if the user is still selecting their item.

Sample project

https://play.nativescript.org/?template=play-ng&id=ImoCcR

Additional context

None

Thank you

needs more info android

Most helpful comment

...Some Class...
lastTimer = {id: null, value: -1};

selectedIndexChangeDebouncer(args) {
        const picker = <ListPicker>args.object;
        console.log("picker selection: " + picker.selectedIndex);
        // If we are the same index as the last time, or the next time; we skip doing anything.
        if (picker.selectedIndex === this.lastTimer.value) { return; }

        // Grab our current value...
        this.lastTimer.value = picker.selectedIndex;

        // If the timer is already running, clear it...
        if (this.lastTimer.id != null) { clearTimeout(this.lastTimer.id); }

        // Start a new timer  (runs in 1/4 of a second)
        this.lastTimer.id = setTimeout(() => {
            this.lastTimer.id = null;
            this.selectedGroupIndexChanged(args);
        }, 250);
    }

Thx for the debounce hint! Works smooth now.

All 6 comments

Hi @taras,
Thank you for the provided sample project.
Regarding the case, there are some differences in ListPicker implementation which are based on some specifics in both platforms(Android and iOS). On that matter, there is some difference in the execution of selectedIndexChange event handler. However, I do not think that this is the cause of the performance issue. I notice that you are calling the slow method inside of it, which seems to make some loop that hangs the UI. If I remove the nested method call, the issue disappears on my side. For example:

slow(index) {
        // slow(index);
        this.selectedIndex = index;
    }

https://play.nativescript.org/?template=play-ng&id=ImoCcR&v=2

Hi @tsonevn,

I notice that you are calling the slow method inside of it, which seems to make some loop that hangs the UI. If I remove the nested method call, the issue disappears on my side.

Yes, this is intentional. It's there to show the impact on perceived performance between iOS and Android.

In ideally world, every problem would be a simple as commenting out the slow function in code. In practice, finding the source of slowness can be difficult. This is made harder by the fact that Android has a reputation for being slower than iOS and profiling tooling is very limited. In these conditions, I would prefer for NativeScript, as much as possible, to prevent performance gotchas caused by inconsistencies between iOS and Android implementations.

On iOS, the ListPicker does not emit events while the user is interacting with the wheel allowing the wheel to work smoothly even when the ListPicker is connected to a slow event handler. On Android, ListPicker emits events continuously which results in UI locking when the event handler is slow. In this case, I would expect Android implementation to mirror iOS.

NativeScript's ListPicker uses android.widget.NumberPicker which supports NumberPicker.OnScrollListener event. This listener emits SCROLL_STATE_TOUCH_SCROLL state that can be used to detect when the user is scrolling using touch, and his finger is still on the screen. This can be used to debounce selectedIndexChange events until the user releases the scroll wheel.

The Android implementation will need to be more complicated but that's the complexity that I would expect NativeScript to take care of in the framework code so I don't have to deal with this in my application code.

I'd be happy to try implementing this change if you agree with this suggestion.

ping 馃槃

Same issue here.

I have a listpicker that loads other listpicker values with an API call. The app almost freezes when scrolling.
Any suggestion on using another component from a plugin or a fix?

The simplest solution is to debounce it yourself. :grinning: This code is off the top of my head, might be some minor changes you need....

const lastTimer = {id: null, value: -1};
function selectedIndexChangeDebouncer(args) { 
    // If we are the same index as the last time, or the next time; we skip doing anything.
    if (args.selectedIndexChange === lastValue.value) return;

   // Grab our current value...
   lastTimer.value = args.selectedIndexChanges;

   // If the timer is already running, clear it...
   if (lastTimer.id != null) { clearTimeout(lastTimer.id); }

   // Start a new timer  (runs in 1/4 of a second)
   lastTimer.id = setTimeout( () => { 
       lastTimer.id = null; 
       selectedIndexChangeCallback(args);  
   }, 250);  
}

function selectedIndexChangeCallback(args) { 
    /* your real SelectIndexChange function that does your actual work */ 
}
...Some Class...
lastTimer = {id: null, value: -1};

selectedIndexChangeDebouncer(args) {
        const picker = <ListPicker>args.object;
        console.log("picker selection: " + picker.selectedIndex);
        // If we are the same index as the last time, or the next time; we skip doing anything.
        if (picker.selectedIndex === this.lastTimer.value) { return; }

        // Grab our current value...
        this.lastTimer.value = picker.selectedIndex;

        // If the timer is already running, clear it...
        if (this.lastTimer.id != null) { clearTimeout(this.lastTimer.id); }

        // Start a new timer  (runs in 1/4 of a second)
        this.lastTimer.id = setTimeout(() => {
            this.lastTimer.id = null;
            this.selectedGroupIndexChanged(args);
        }, 250);
    }

Thx for the debounce hint! Works smooth now.

Was this page helpful?
0 / 5 - 0 ratings