Widget的物理模拟动画效果

物理模拟能够让应用富有真实感和更好的交互性。例如,你可能会为一个 widget 添加动画,让它看起来就像安着弹簧,或是在随重力下落。

这个指南演示了如何将 widget 从拖动的点移回到中心,并使用弹簧模拟效果。

这个演示将进行下面几步:

  1. 创建一个动画控制器
  2. 使用手势移动 widget
  3. 对 widget 进行动画
  4. 计算速度以模拟弹跳运动

第一步:创建一个动画控制器

首先,创建一个叫做 DraggableCard 的 stateful widget:

import 'package:flutter/material.dart';

void main() {
  runApp(const MaterialApp(home: PhysicsCardDragDemo()));
}

class PhysicsCardDragDemo extends StatelessWidget {
  const PhysicsCardDragDemo({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: const DraggableCard(
        child: FlutterLogo(
          size: 128,
        ),
      ),
    );
  }
}

class DraggableCard extends StatefulWidget {
  const DraggableCard({required this.child, Key? key}) : super(key: key);

  final Widget child;

  @override
  _DraggableCardState createState() => _DraggableCardState();
}

class _DraggableCardState extends State<DraggableCard> {
  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() {
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Align(
      child: Card(
        child: widget.child,
      ),
    );
  }
}

让 _DraggableCardState 类继承至 SingleTickerProviderStateMixin。然后在 initState 中构造一个 AnimationController,并将其 vsync 属性设为 this

提示

继承的 SingleTickerProviderStateMixin 让 state 对象为 AnimationController 提供了 TickerProvider 的能力。要获得更多信息,请查看 TickerProvider 文档。

第二步:使用手势移动 widget

让 widget 可以被拖拽,并为 _DraggableCardState 类添加一个 Alignment 范围。

添加一个 GestureDetector 来捕获 onPanDownonPanUpdate,以及 onPanEnd 回调。为了调整对齐方式,请使用 MediaQuery 来获得 widget 的大小,然后除以 2。(这会将「拖动的像素」单位转为 Align 使用的坐标。)然后,将 Align widget 的 alignmnt 属性设为 _dragAlignment

第三步:对 widget 进行动画

当一个 widget 被释放,它应该就会弹回中心。

添加一个 Animation<Alignment>,以及 _runAnimation 方法。此方法定义了一个 Tween,它在 widget 被拖动到的点之间插入到中心点。

void _runAnimation() {
  _animation = _controller.drive(
    AlignmentTween(
      begin: _dragAlignment,
      end: Alignment.center,
    ),
  );
  _controller.reset();
  _controller.forward();
}

接下来,当 AnimationController 产生一个值时,更新 _dragAlignment

下一步,让 Align widget 使用 _dragAlignment 字段:

child: Align(
  alignment: _dragAlignment,
  child: Card(
    child: widget.child,
  ),
),

最后,更新 GestureDetector 来管理动画控制器:

第四步:计算速度以模拟弹跳运动

最后一步时做一些简单的数学计算,计算小部件被拖动完成之后的速度。这样小部件在被快速恢复之前实际上以该速度继续运动。(_runAnimation 方法已经通过设置动画的开始和结束对齐方式来设置方向。)

首先,引入 physics 这个 package:

import 'package:flutter/physics.dart';

onPanEnd 回调提供了一个 DragEndDetails 对象。此对象提供指针停止接触屏幕时的速度。速度以每秒像素为单位,但 Align widget 不使用像素。它使用 [-1.0,-1.0] 和 [1.0,1.0] 之间的坐标值,其中 [0.0,0.0] 表示中心。在步骤 2 中计算的 size 用于将像素转换为该范围内的坐标值。

最后,AnimationController 有一个 animateWith() 方法可以产生 SpringSimulation:

/// Calculates and runs a [SpringSimulation].
void _runAnimation(Offset pixelsPerSecond, Size size) {
  _animation = _controller.drive(
    AlignmentTween(
      begin: _dragAlignment,
      end: Alignment.center,
    ),
  );
  // Calculate the velocity relative to the unit interval, [0,1],
  // used by the animation controller.
  final unitsPerSecondX = pixelsPerSecond.dx / size.width;
  final unitsPerSecondY = pixelsPerSecond.dy / size.height;
  final unitsPerSecond = Offset(unitsPerSecondX, unitsPerSecondY);
  final unitVelocity = unitsPerSecond.distance;

  const spring = SpringDescription(
    mass: 30,
    stiffness: 1,
    damping: 1,
  );

  final simulation = SpringSimulation(spring, 0, 1, -unitVelocity);

  _controller.animateWith(simulation);
}

不要忘记调用 _runAnimation(),并传入速度和大小:

onPanEnd: (details) {
  _runAnimation(details.velocity.pixelsPerSecond, size);
},

提示

既然动画控制器使用了模拟,就不再需要指定 duration 参数。

交互式样例
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';

void main() {
  runApp(const MaterialApp(home: PhysicsCardDragDemo()));
}

class PhysicsCardDragDemo extends StatelessWidget {
  const PhysicsCardDragDemo({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: const DraggableCard(
        child: FlutterLogo(
          size: 128,
        ),
      ),
    );
  }
}

/// A draggable card that moves back to [Alignment.center] when it's
/// released.
class DraggableCard extends StatefulWidget {
  const DraggableCard({required this.child, Key? key}) : super(key: key);

  final Widget child;

  @override
  _DraggableCardState createState() => _DraggableCardState();
}

class _DraggableCardState extends State<DraggableCard>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  /// The alignment of the card as it is dragged or being animated.
  ///
  /// While the card is being dragged, this value is set to the values computed
  /// in the GestureDetector onPanUpdate callback. If the animation is running,
  /// this value is set to the value of the [_animation].
  Alignment _dragAlignment = Alignment.center;

  late Animation<Alignment> _animation;

  /// Calculates and runs a [SpringSimulation].
  void _runAnimation(Offset pixelsPerSecond, Size size) {
    _animation = _controller.drive(
      AlignmentTween(
        begin: _dragAlignment,
        end: Alignment.center,
      ),
    );
    // Calculate the velocity relative to the unit interval, [0,1],
    // used by the animation controller.
    final unitsPerSecondX = pixelsPerSecond.dx / size.width;
    final unitsPerSecondY = pixelsPerSecond.dy / size.height;
    final unitsPerSecond = Offset(unitsPerSecondX, unitsPerSecondY);
    final unitVelocity = unitsPerSecond.distance;

    const spring = SpringDescription(
      mass: 30,
      stiffness: 1,
      damping: 1,
    );

    final simulation = SpringSimulation(spring, 0, 1, -unitVelocity);

    _controller.animateWith(simulation);
  }

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this);

    _controller.addListener(() {
      setState(() {
        _dragAlignment = _animation.value;
      });
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final size = MediaQuery.of(context).size;
    return GestureDetector(
      onPanDown: (details) {
        _controller.stop();
      },
      onPanUpdate: (details) {
        setState(() {
          _dragAlignment += Alignment(
            details.delta.dx / (size.width / 2),
            details.delta.dy / (size.height / 2),
          );
        });
      },
      onPanEnd: (details) {
        _runAnimation(details.velocity.pixelsPerSecond, size);
      },
      child: Align(
        alignment: _dragAlignment,
        child: Card(
          child: widget.child,
        ),
      ),
    );
  }
}

上述代码可以在 https://www.dartpad.dev/ 进行在线测试。

参考链接


Widget 的物理模拟动画效果

发布者

发表回复

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