Skip to content

ClampingScrollSimulation has large nonzero velocity when near the end, and velocity even increases slightly there #113424

@fzyzcjy

Description

@fzyzcjy

The problem

When using ClampingScrollSimulation, there are two problems:

  • It has large nonzero velocity when near the end (see figure below). We all know, for list view scrolling to feel comfortable, it should decrease its velocity and get to near-zero value when near end, instead of a large nonzero value.
  • velocity even increases slightly there (again see figure below). We know velocity should at least decrease due to friction.

Reproduction code for "velocity increases when near end"

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  test('ClampingScrollSimulation should have decreasing speed', () {
    final ClampingScrollSimulation simulation =
        ClampingScrollSimulation(position: 0, velocity: 1000);

    final List<double> tValues = List<double>.generate(60, (int i) => i / 60);
    final List<double> xValues = tValues.map(simulation.x).toList();
    final List<double> xDeltas = <double>[
      for (int i = 0; i < xValues.length - 1; ++i) xValues[i + 1] - xValues[i]
    ];

    // If curious, uncomment this to see the real values
    // debugPrint('$tValues\n$xValues\n$xDeltas');

    for (int i = 0; i < xDeltas.length - 1; ++i) {
      expect(xDeltas[i], greaterThan(xDeltas[i + 1]));
    }
  });
}

Figures

Code:

Details
import matplotlib
import numpy as np

matplotlib.use("MacOSX")

import matplotlib.pyplot as plt

import math
from dataclasses import dataclass

_kDecelerationRate = math.log(0.78) / math.log(0.9)
_initialVelocityPenetration = 3.065


def _decelerationForFriction(friction: float):
    return friction * 61774.04968


def _flingDistancePenetration(t):
    return (1.2 * t * t * t) - (3.27 * t * t) + (_initialVelocityPenetration * t)


def _flingVelocityPenetration(t):
    return (3.6 * t * t) - (6.54 * t) + _initialVelocityPenetration


def clampDouble(x, min, max):
    if x < min:
        return min
    if x > max:
        return max
    return x


def _sign(x):
    if x > 0:
        return 1.0
    if x < 0:
        return -1.0
    return x


def _array_map(x, f):
    return np.fromiter((f(xi) for xi in x), x.dtype)


@dataclass
class ClampingScrollSimulation:
    position: float
    velocity: float
    friction: float = 0.015

    def __post_init__(self):
        self._duration = self._flingDuration(self.velocity)
        self._distance = abs(self.velocity * self._duration / _initialVelocityPenetration)

    def _flingDuration(self, velocity: float):
        scaledFriction = self.friction * _decelerationForFriction(0.84)
        deceleration = math.log(0.35 * abs(velocity) / scaledFriction)
        return math.exp(deceleration / (_kDecelerationRate - 1.0))

    def x(self, time):
        t = clampDouble(time / self._duration, 0.0, 1.0)
        return self.position + self._distance * _flingDistancePenetration(t) * _sign(self.velocity)

    def dx(self, time):
        t = clampDouble(time / self._duration, 0.0, 1.0)
        return self._distance * _flingVelocityPenetration(t) * _sign(self.velocity) / self._duration


# %%

plt.clf()
plt.tight_layout()
ax1 = plt.gca()
ax2 = ax1.twinx()

dt = 1 / 60
t = np.arange(0, 1, dt)

simulation_a = ClampingScrollSimulation(position=0, velocity=1000)
x_a = _array_map(t, simulation_a.x)
v_a = _array_map(t, simulation_a.dx)
diffx_a = (x_a[1:] - x_a[:-1]) / dt

ax1.plot(t, x_a, label='x_a')
ax2.plot(t, v_a, label='v_a')
ax2.plot(t[:-1], diffx_a, label='diffx_a')

if False:
    b_start_time = 0.3
    simulation_b = ClampingScrollSimulation(
        position=simulation_a.x(b_start_time),
        velocity=simulation_a.dx(b_start_time),
    )
    x_b = _array_map(t, simulation_b.x)
    v_b = _array_map(t, simulation_b.dx)
    diffx_b = (x_b[1:] - x_b[:-1]) / dt

    ax1.plot(t + b_start_time, x_b, label='x_b')
    ax2.plot(t + b_start_time, v_b, label='v_b')
    ax2.plot(t[:-1] + b_start_time, diffx_b, label='diffx_b')

ax1.legend(loc="upper left")
ax2.legend(loc="upper right")

plt.xlim([0, 1])

plt.show()

Image:

image

zoom in:

image

zoom in again:

image

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listf: scrollingViewports, list views, slivers, etc.found in release: 3.3Found to occur in 3.3found in release: 3.5Found to occur in 3.5frameworkflutter/packages/flutter repository. See also f: labels.has reproducible stepsThe issue has been confirmed reproducible and is ready to work on

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions