BouncingScrollPhysics 数学模型:基于弹簧阻尼系统的边界回弹计算

BouncingScrollPhysics 数学模型:基于弹簧阻尼系统的边界回弹计算

大家好,今天我们来深入探讨 BouncingScrollPhysics 的数学模型,这是一种常见的滚动物理效果,广泛应用于移动应用和网页设计中。我们将从最基本的弹簧阻尼系统出发,一步步推导出 BouncingScrollPhysics 的核心公式,并结合代码示例,帮助大家理解其背后的原理。

1. 弹簧阻尼系统简介

BouncingScrollPhysics 的核心思想是模拟一个弹簧阻尼系统。弹簧阻尼系统由一个弹簧和一个阻尼器组成,用于描述物体在受到外力作用后,如何通过弹簧的恢复力和阻尼器的阻力逐渐回到平衡位置。

1.1 弹簧力

弹簧力与弹簧的伸长量成正比,方向与伸长方向相反。可以用胡克定律表示:

F_spring = -k * x

其中:

  • F_spring 是弹簧力
  • k 是弹簧刚度系数,表示弹簧的硬度,数值越大,弹簧越硬
  • x 是弹簧的伸长量,即物体偏离平衡位置的距离

1.2 阻尼力

阻尼力与物体的速度成正比,方向与速度方向相反。可以用以下公式表示:

F_damping = -c * v

其中:

  • F_damping 是阻尼力
  • c 是阻尼系数,表示阻尼的大小,数值越大,阻尼越大
  • v 是物体的速度

1.3 牛顿第二定律

根据牛顿第二定律,物体受到的合力等于物体的质量乘以加速度:

F_net = m * a

其中:

  • F_net 是物体受到的合力
  • m 是物体的质量
  • a 是物体的加速度

1.4 弹簧阻尼系统运动方程

将弹簧力、阻尼力和牛顿第二定律结合起来,可以得到弹簧阻尼系统的运动方程:

m * a = -k * x - c * v

由于加速度是速度的导数,速度是位移的导数,可以将上述方程改写为:

m * x''(t) + c * x'(t) + k * x(t) = 0

其中:

  • x(t) 是物体在 t 时刻的位移
  • x'(t) 是物体在 t 时刻的速度
  • x''(t) 是物体在 t 时刻的加速度

这个二阶常微分方程描述了弹簧阻尼系统的运动规律。

2. BouncingScrollPhysics 中的弹簧阻尼系统

BouncingScrollPhysics 将滚动视图的边界视为弹簧,当滚动超出边界时,弹簧力会将其拉回边界。同时,阻尼力会逐渐减小滚动速度,最终使滚动视图停留在边界位置。

2.1 超出边界的位移

BouncingScrollPhysics 首先需要确定超出边界的位移 overscroll。这可以通过以下方式计算:

double _clampedOverscroll(ScrollMetrics position) {
  return (position.outOfRange ? (position.pixels - position.maxScrollExtent).abs() : 0.0);
}

这段代码检查滚动位置是否超出范围 ( position.outOfRange )。如果是,它计算超出最大滚动范围的距离,并取绝对值。

2.2 弹簧刚度系数 (Spring Constant)

BouncingScrollPhysics 使用 spring 属性来控制弹簧的刚度。默认情况下,spring 的值为 SpringDescription.withDampingRatio(mass: 1.0, stiffness: 100.0, ratio: 0.6)。这意味着:

  • mass (质量) = 1.0
  • stiffness (刚度) = 100.0
  • ratio (阻尼比) = 0.6

弹簧刚度系数直接影响回弹的速度和力度。较大的刚度系数会导致更快速、更强烈的回弹。

2.3 阻尼系数 (Damping Coefficient)

阻尼系数由 SpringDescription 中的 ratio 属性间接控制。阻尼比用于描述阻尼的程度。阻尼比为 1 表示临界阻尼,阻尼比小于 1 表示欠阻尼,阻尼比大于 1 表示过阻尼。BouncingScrollPhysics 使用欠阻尼,以便产生回弹效果。

2.4 运动方程的简化

由于 BouncingScrollPhysics 主要关注的是超出边界时的回弹效果,因此可以将运动方程简化为:

m * x''(t) + c * x'(t) + k * x(t) = 0  (x(t) > 0)

其中 x(t) 代表超出边界的位移。当 x(t) 小于等于 0 时,表示没有超出边界,此时不需要应用弹簧阻尼系统。

3. BouncingScrollPhysics 的代码实现

BouncingScrollPhysics 在 Flutter 框架中的实现主要体现在 applyPhysicsToUserOffset 方法中。该方法根据用户的滚动偏移量和当前的滚动状态,计算出新的滚动偏移量。

  @override
  double applyPhysicsToUserOffset(ScrollMetrics position, double time, double pixelsPerSecond) {
    assert(position.minScrollExtent <= position.maxScrollExtent);

    if (!position.outOfRange) {
      _lastVelocity = pixelsPerSecond;
      return pixelsPerSecond * time;
    }

    final double overscrollPastStart = math.max(position.minScrollExtent - position.pixels, 0.0);
    final double overscrollPastEnd = math.max(position.pixels - position.maxScrollExtent, 0.0);
    final double overscroll = overscrollPastStart > 0.0 ? overscrollPastStart : overscrollPastEnd;
    bool keepInBound = false;
    if (overscrollPastStart > 0.0) {
      keepInBound = true;
    } else if (overscrollPastEnd > 0.0) {
      keepInBound = false;
    }
    final double direction = keepInBound ? -1.0 : 1.0;

    final double velocityIndependentOfTime =  math.min(pixelsPerSecond, 0.05);

    return pixelsPerSecond * time - velocityIndependentOfTime * time * direction;
  }

让我们分解一下这段代码:

  • 检查是否超出范围: 首先,检查滚动位置是否在允许的滚动范围内 (position.outOfRange)。如果不是,则直接根据 pixelsPerSecondtime 计算新的偏移量,并返回。
  • 计算超出边界的距离: 如果超出范围,则计算超出起始位置的距离 (overscrollPastStart) 或超出结束位置的距离 (overscrollPastEnd)。overscroll 变量存储了超出边界的总距离。keepInBound 变量用于标记是否需要将滚动位置保持在起始位置(true)或结束位置(false)。
  • 计算方向: direction 变量表示回弹的方向。如果需要将滚动位置保持在起始位置,则方向为 -1.0,否则为 1.0。
  • 计算偏移量: 最后,根据 pixelsPerSecondtimedirection 计算新的偏移量。这里使用了一个简化的公式,没有直接使用弹簧阻尼系统的运动方程。velocityIndependentOfTime 限制了回弹速度。

2.5 滚动模拟 (Scroll Simulation)

Flutter 框架还提供了一个 ScrollSimulation 类,用于模拟滚动的过程。BouncingScrollSimulationScrollSimulation 的一个子类,专门用于模拟具有回弹效果的滚动。

class BouncingScrollSimulation extends Simulation {
  /// Creates a simulation that applies the bouncing physics.
  BouncingScrollSimulation({
    required double position,
    required double velocity,
    required double leadingExtent,
    required double trailingExtent,
    required SpringDescription spring,
    Tolerance tolerance = Tolerance.defaultTolerance,
  }) : _spring = spring,
       _leadingExtent = leadingExtent,
       _trailingExtent = trailingExtent,
       _position = position,
       _velocity = velocity,
       _tolerance = tolerance,
       super(tolerance: tolerance);

  final SpringDescription _spring;
  final double _leadingExtent;
  final double _trailingExtent;
  final double _position;
  final double _velocity;
  final Tolerance _tolerance;

  @override
  double x(double time) {
    final double extent = _clampDouble(_leadingExtent, _trailingExtent, _position);
    return _position + _spring.x(time, _position - extent, _velocity);
  }

  @override
  double dx(double time) {
    return _spring.dx(time, _position - _clampDouble(_leadingExtent, _trailingExtent, _position), _velocity);
  }

  @override
  bool isDone(double time) {
    return _spring.isDone(time, _position - _clampDouble(_leadingExtent, _trailingExtent, _position), _velocity);
  }
}
  • 构造函数: BouncingScrollSimulation 的构造函数接收初始位置 (position)、初始速度 (velocity)、滚动范围 (leadingExtenttrailingExtent)、弹簧描述 (spring) 和容差 (tolerance) 等参数。
  • x(time) 方法: 该方法根据时间 time 计算滚动位置。它使用 _spring.x() 方法来计算弹簧阻尼系统的位移。
  • dx(time) 方法: 该方法根据时间 time 计算滚动速度。它使用 _spring.dx() 方法来计算弹簧阻尼系统的速度。
  • isDone(time) 方法: 该方法判断滚动是否完成。当弹簧阻尼系统的速度和位移都足够小时,滚动被认为已完成。

SpringDescription 类定义了弹簧的属性,包括质量、刚度和阻尼比。SpringDescription.x()SpringDescription.dx() 方法用于计算弹簧阻尼系统的位移和速度。

4. SpringDescription 的实现细节

SpringDescription 类实际上并没有直接实现弹簧阻尼系统的运动方程的求解,而是依赖于 _computeSpringSolution 函数来计算。

class SpringDescription {
  const SpringDescription({
    required this.mass,
    required this.stiffness,
    required this.damping,
  });

  /// Creates a spring with a damping ratio.
  ///
  /// The damping ratio describes how oscillations in the spring decay based on
  /// the mass, stiffness, and damping. See
  /// [https://en.wikipedia.org/wiki/Damping_ratio].
  SpringDescription.withDampingRatio({
    required this.mass,
    required double stiffness,
    required double ratio,
  }) : stiffness = stiffness,
       damping = _computeDampingFromRatio(mass, stiffness, ratio);

  /// Mass of the spring in kg.
  final double mass;

  /// Stiffness of the spring in kg/s^2.
  final double stiffness;

  /// Damping of the spring in kg/s.
  final double double damping;

  static double _computeDampingFromRatio(double mass, double stiffness, double ratio) {
    return ratio * 2.0 * math.sqrt(mass * stiffness);
  }

  double x(double time, double displacement, double velocity) => _computeSpringSolution(time, displacement, velocity, mass, stiffness, damping).x;

  double dx(double time, double displacement, double velocity) => _computeSpringSolution(time, displacement, velocity, mass, stiffness, damping).dx;

  bool isDone(double time, double displacement, double velocity) {
    final SpringSolution solution = _computeSpringSolution(time, displacement, velocity, mass, stiffness, damping);
    return solution.x.abs() < tolerance.distance && solution.dx.abs() < tolerance.velocity;
  }
}

_computeSpringSolution 函数是一个核心的数学函数,它根据弹簧的质量、刚度、阻尼、初始位移和初始速度,计算出弹簧阻尼系统在任意时刻的位移和速度。

SpringSolution _computeSpringSolution(double time, double displacement, double velocity, double mass, double stiffness, double damping) {
  final double z = damping / (2 * math.sqrt(stiffness * mass));

  if (z > 1) {
    // Overdamped
    final double wD = math.sqrt(damping * damping - 4 * stiffness * mass);
    final double s1 = (-damping - wD) / (2.0 * mass);
    final double s2 = (-damping + wD) / (2.0 * mass);
    final double C2 = (velocity - displacement * s1) / (s2 - s1);
    final double C1 = displacement - C2;
    final double x = C1 * math.exp(s1 * time) + C2 * math.exp(s2 * time);
    final double dx = C1 * s1 * math.exp(s1 * time) + C2 * s2 * math.exp(s2 * time);
    return SpringSolution(x: x, dx: dx);
  } else if (z == 1) {
    // Critically Damped
    final double c1 = displacement;
    final double c2 = velocity + displacement * damping / (2 * mass);
    final double x = (c1 + c2 * time) * math.exp(-damping / (2 * mass) * time);
    final double dx = (c2 - (damping / (2 * mass)) * (c1 + c2 * time)) * math.exp(-damping / (2 * mass) * time);
    return SpringSolution(x: x, dx: dx);
  } else {
    // Underdamped
    final double w = math.sqrt(stiffness / mass - (damping * damping) / (4 * mass * mass));
    final double c1 = displacement;
    final double c2 = (velocity + displacement * damping / (2 * mass)) / w;
    final double exponential = math.exp(-damping / (2 * mass) * time);
    final double x = exponential * (c1 * math.cos(w * time) + c2 * math.sin(w * time));
    final double dx = -exponential * ((damping / (2 * mass)) * (c1 * math.cos(w * time) + c2 * math.sin(w * time)) - w * (c1 * math.sin(w * time) - c2 * math.cos(w * time)));
    return SpringSolution(x: x, dx: dx);
  }
}

这段代码根据阻尼比 z 的值,分别计算了过阻尼、临界阻尼和欠阻尼三种情况下的位移和速度。BouncingScrollPhysics 使用的是欠阻尼情况,因此会产生回弹效果。

5. 自定义 BouncingScrollPhysics

你可以通过继承 BouncingScrollPhysics 类,并重写其 applyPhysicsToUserOffset 方法,来自定义滚动物理效果。例如,你可以修改弹簧刚度系数和阻尼系数,来调整回弹的速度和力度。

class MyBouncingScrollPhysics extends BouncingScrollPhysics {
  const MyBouncingScrollPhysics({ScrollPhysics? parent}) : super(parent: parent);

  @override
  double applyPhysicsToUserOffset(ScrollMetrics position, double time, double pixelsPerSecond) {
    // 修改弹簧刚度系数和阻尼系数
    final double newPixelsPerSecond = pixelsPerSecond * 1.5; // 加快回弹速度
    return super.applyPhysicsToUserOffset(position, time, newPixelsPerSecond);
  }

  @override
  MyBouncingScrollPhysics applyTo(ScrollPhysics? ancestor) {
    return MyBouncingScrollPhysics(parent: buildParent(ancestor));
  }
}

在这个例子中,我们重写了 applyPhysicsToUserOffset 方法,并将 pixelsPerSecond 乘以 1.5,从而加快了回弹速度。同时,我们重写了 applyTo 方法,以便在滚动视图中使用自定义的 ScrollPhysics

6. 代码示例

以下是一个使用自定义 BouncingScrollPhysics 的代码示例:

import 'package:flutter/material.dart';
import 'dart:math' as math;

class MyBouncingScrollPhysics extends BouncingScrollPhysics {
  const MyBouncingScrollPhysics({ScrollPhysics? parent}) : super(parent: parent);

  @override
  double applyPhysicsToUserOffset(ScrollMetrics position, double time, double pixelsPerSecond) {
    if (!position.outOfRange) {
      return pixelsPerSecond * time;
    }

    final double overscrollPastStart = math.max(position.minScrollExtent - position.pixels, 0.0);
    final double overscrollPastEnd = math.max(position.pixels - position.maxScrollExtent, 0.0);
    final double overscroll = overscrollPastStart > 0.0 ? overscrollPastStart : overscrollPastEnd;
    bool keepInBound = false;
    if (overscrollPastStart > 0.0) {
      keepInBound = true;
    } else if (overscrollPastEnd > 0.0) {
      keepInBound = false;
    }
    final double direction = keepInBound ? -1.0 : 1.0;

    final double velocityIndependentOfTime =  math.min(pixelsPerSecond, 0.05);

    return pixelsPerSecond * time - velocityIndependentOfTime * time * direction * 2;
  }

  @override
  MyBouncingScrollPhysics applyTo(ScrollPhysics? ancestor) {
    return MyBouncingScrollPhysics(parent: buildParent(ancestor));
  }
}

void main() {
  runApp(
    MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Custom Bouncing Scroll Physics')),
        body: ListView.builder(
          physics: const MyBouncingScrollPhysics(), // 使用自定义的 ScrollPhysics
          itemCount: 100,
          itemBuilder: (context, index) {
            return ListTile(title: Text('Item $index'));
          },
        ),
      ),
    ),
  );
}

在这个示例中,我们将 MyBouncingScrollPhysics 应用于 ListView.builder,从而实现了具有更快回弹效果的滚动视图。

7. 表格总结

概念 描述 公式
弹簧力 与弹簧的伸长量成正比,方向与伸长方向相反。 F_spring = -k * x
阻尼力 与物体的速度成正比,方向与速度方向相反。 F_damping = -c * v
牛顿第二定律 物体受到的合力等于物体的质量乘以加速度。 F_net = m * a
弹簧阻尼系统运动方程 描述弹簧阻尼系统的运动规律。 m * x''(t) + c * x'(t) + k * x(t) = 0
超出边界的位移 滚动视图超出边界的距离。 (position.pixels – position.maxScrollExtent).abs()

8. 核心要点回顾

BouncingScrollPhysics 通过模拟弹簧阻尼系统,实现了具有回弹效果的滚动视图。其核心在于计算超出边界的位移,并根据弹簧刚度系数和阻尼系数,计算出新的滚动偏移量。通过自定义 BouncingScrollPhysics,可以调整回弹的速度和力度,从而实现更个性化的滚动体验。理解其数学模型,可以更深入地掌握其工作原理,并更好地进行定制和优化。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注