angular-ui / ui-scroll

Unlimited bidirectional scrolling over a limited element buffer for AngularJS applications
http://angular-ui.github.io/ui-scroll/demo/
MIT License
326 stars 107 forks source link

Inertia fix #238

Closed dhilt closed 4 years ago

dhilt commented 4 years ago

Relates to issue https://github.com/angular-ui/ui-scroll/issues/205.

The problem

The update of the scroll position made programmatically can be overridden by the inertia scrolling process, which is handled by the browser internally.

Depends on inertia implementation (Device + OS + Browser).

Irregular, can't be 100%-reproduced.

Details

We have two processes: inertia scrolling (A) and manual scroll position update (B). They may conflict if B happen during A:

Good Bad
100, A 100, A
92, A 92, A
88, A 88, A
84, A 84, A
200, B 200, B
197, A 81, A
195, A 79, A
194, A 78, A

During A process Browser calculates the layout iteratively. Next state is based on previous one. B happens between two phases of A. The result depends on A implementation. A developer don't have access to A implementation. Nut sure about specification, but speaking of timing, this is a kind of black box. So A can override B and continue its next phase based on previous A phase, not on B. See Bad scenario.

Solution

The phases of the inertia scrolling process correlate with Browser repaint cycles. To provide right timing, which will not depend on the environment, the code dedicated to update the scroll position can be invoked with the requestAnimationFrame method.

So, basically the fix could be just

 requestAnimationFrame(() => viewport.scrollTop(newPosition));

This produces an additional asynchronicity, so I decided to provide some optimization and not postpone scrollTop change in case it is not necessary. For this purpose I store the value I want to be a new scroll position:

  viewport.synthetic = { position: newPosition };
  viewport.scrollTop(newPosition);

and then I re-adjust scroll position on the scroll event handler if current (handled) scroll position is not equal to expected one, which can happen if the inertia process is running:

  if (viewport.synthetic && viewport.scrollTop() !== viewport.synthetic.position) {
    requestAnimationFrame(() => {
      viewport.scrollTop(position);
      viewport.synthetic = null;
    });
  }

This produces an asynchronicity, but only in case the inertia does really break the synthetic scrollTop update.

Addition

New attribute had been added: handle-inertia. It is responsible for enabling/disabling inertia processing. The default value is "true". To disable inertia scroll processing the value should be set to "false":

<div ui-scroll="item in datasource" handle-inertia="false">

This attribute is not going to be documented.