首页 > 解决方案 > 如何正确使用曲线的值来为小部件设置动画?

问题描述

最小的可重现代码:

class _MyPageState extends State<MyPage> {
  double _dx1 = 0;
  double _dx2 = 0;
  final Duration _duration = Duration(seconds: 1);
  final Curve _curve = Curves.linear;

  void _play() {
    final width = MediaQuery.of(context).size.width;
    _dx1 = width;
    var i = 0;
    Timer.periodic(Duration(milliseconds: 1), (timer) {
      if (i > 1000) {
        timer.cancel();
      } else {
        setState(() {
          _dx2 = _curve.transform(i / 1000) * width;
          i++;
        });
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: _play,
        child: Icon(Icons.play_arrow),
      ),
      body: SizedBox.expand(
        child: Stack(
          children: [
            AnimatedPositioned(
              left: _dx1,
              duration: _duration,
              curve: _curve,
              child: _box,
            ),
            Positioned(
              left: _dx2,
              top: 60,
              child: _box,
            ),
          ],
        ),
      ),
    );
  }

  Container get _box => Container(width: 50, height: 50, color: Colors.red);
}

输出

在此处输入图像描述

如您所见,我的第二个自定义动画框没有赶上第一个默认AnimatedPositioned小部件。

注意:这可以很容易地使用TweenAnimationController但我只想知道为什么我无法正确使用Curve's 的值来匹配默认行为。

标签: flutterflutter-animation

解决方案


假设:每毫秒定期调用回调。
预期结果:1秒后,i = 1000;

假设是错误的。添加以下代码进行验证:

void _play() {
  ...
  var i = 0;
  final start = DateTime.now().millisecondsSinceEpoch;
  Timer.periodic(Duration(milliseconds: 1), (timer) {
    final elapse = DateTime.now().millisecondsSinceEpoch - start;
    print('elapse = $elapse');
    print('i = $i');
    print('ticks = ${timer.tick}');
    print('######################');
    ...
  });
}

在我的电脑上,最后一个值为:

elapse = 14670
i = 1001
ticks = 14670
######################

因此,这意味着在我的 PC 上进行 1001 次回调需要 14 秒。这不是我们所期待的。从中我们可以推断出,有些回调被遗漏了,i并没有反映经过的时间。

但是,我们需要的值是timer.tick。引用文档

如果具有非零持续时间的周期性计时器延迟太多,那么应该发生不止一个滴答,除了过去的最后一个滴答外,其他所有滴答都被视为“错过”,并且不会为它们调用回调。滴答计数反映了已经过去的持续时间的数量,而不是已经发生的回调调用的数量。

所以,tick告诉我们period已经通过的 s 的数量。下面的代码将赶上AnimatedPositioned

void _play() {
  final width = MediaQuery.of(context).size.width;
  _dx1 = width;

  final period = Duration(milliseconds: 1);
  Timer.periodic(period, (timer) {
    final elapsed = timer.tick * period.inMilliseconds;
    if (elapsed > _duration.inMilliseconds) {
      timer.cancel();
    } else {
      setState(() {
        _dx2 = _curve.transform(elapsed / _duration.inMilliseconds) * width;
      });
    }
  });
}

现在,您可能会看到一些口吃,我们的盒子可能有点领先或落后于AnimatedPositioned,这是因为我们使用TimerAnimatedPositioned使用Ticker. 差异是Timer.periodicDuration我们作为周期传递的驱动,但Ticker由 驱动SchedulerBinding.scheduleFrameCallback。因此,值_dx2更新的瞬间和帧在屏幕上呈现的瞬间可能不一样。添加错过了一些回调的事实!


推荐阅读