使用物理模拟为小部件设置动画

物理模拟可以使应用程序交互感觉真实且具有交互性。例如,您可能希望为小部件设置动画,使其表现得像连接到弹簧或在重力作用下坠落一样。

此食谱演示了如何使用弹簧模拟将小部件从拖动点移动回中心。

此食谱使用以下步骤

  1. 设置动画控制器
  2. 使用手势移动小部件
  3. 为小部件设置动画
  4. 计算模拟弹簧运动的速度

步骤 1:设置动画控制器

#

从名为 DraggableCard 的有状态小部件开始

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

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

class PhysicsCardDragDemo extends StatelessWidget {
  const PhysicsCardDragDemo({super.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, super.key});

  final Widget child;

  @override
  State<DraggableCard> 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

lib/{starter.dart → step1.dart} 重命名
@@ -29,14 +29,20 @@
29
29
  State<DraggableCard> createState() => _DraggableCardState();
30
30
  }
31
- class _DraggableCardState extends State<DraggableCard> {
31
+ class _DraggableCardState extends State<DraggableCard>
32
+ with SingleTickerProviderStateMixin {
33
+ late AnimationController _controller;
34
+
32
35
  @override
33
36
  void initState() {
34
37
  super.initState();
38
+ _controller =
39
+ AnimationController(vsync: this, duration: const Duration(seconds: 1));
35
40
  }
36
41
  @override
37
42
  void dispose() {
43
+ _controller.dispose();
38
44
  super.dispose();
39
45
  }

步骤 2:使用手势移动小部件

#

当小部件被拖动时使其移动,并在 _DraggableCardState 类中添加一个 Alignment 字段

lib/{step1.dart (alignment) → step2.dart (alignment)} 重命名
@@ -1,3 +1,4 @@
1
1
  class _DraggableCardState extends State<DraggableCard>
2
2
  with SingleTickerProviderStateMixin {
3
3
  late AnimationController _controller;
4
+ Alignment _dragAlignment = Alignment.center;

添加一个 GestureDetector 来处理 onPanDownonPanUpdateonPanEnd 回调。要调整对齐方式,请使用 MediaQuery 获取小部件的大小,并除以 2。(这将“拖动像素”单位转换为 Align 使用的坐标。)然后,将 Align 小部件的 alignment 设置为 _dragAlignment

lib/{step1.dart (build) → step2.dart (build)} 重命名
@@ -1,8 +1,22 @@
1
1
  @override
2
2
  Widget build(BuildContext context) {
3
- return Align(
4
- child: Card(
5
- child: widget.child,
3
+ var size = MediaQuery.of(context).size;
4
+ return GestureDetector(
5
+ onPanDown: (details) {},
6
+ onPanUpdate: (details) {
7
+ setState(() {
8
+ _dragAlignment += Alignment(
9
+ details.delta.dx / (size.width / 2),
10
+ details.delta.dy / (size.height / 2),
11
+ );
12
+ });
13
+ },
14
+ onPanEnd: (details) {},
15
+ child: Align(
16
+ alignment: _dragAlignment,
17
+ child: Card(
18
+ child: widget.child,
19
+ ),
6
20
  ),
7
21
  );
8
22
  }

步骤 3:为小部件设置动画

#

当小部件被释放时,它应该弹回中心。

添加一个Animation<Alignment>字段和一个_runAnimation方法。此方法定义一个Tween,它在小部件被拖动到的点和中心点之间进行插值。

lib/{step2.dart (animation) → step3.dart (animation)} RENAMED
@@ -1,4 +1,5 @@
1
1
  class _DraggableCardState extends State<DraggableCard>
2
2
  with SingleTickerProviderStateMixin {
3
3
  late AnimationController _controller;
4
+ late Animation<Alignment> _animation;
4
5
  Alignment _dragAlignment = Alignment.center;
dart
void _runAnimation() {
  _animation = _controller.drive(
    AlignmentTween(
      begin: _dragAlignment,
      end: Alignment.center,
    ),
  );
  _controller.reset();
  _controller.forward();
}

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

lib/{step2.dart (initState) → step3.dart (initState)} RENAMED
@@ -3,4 +3,9 @@
3
3
  super.initState();
4
4
  _controller =
5
5
  AnimationController(vsync: this, duration: const Duration(seconds: 1));
6
+ _controller.addListener(() {
7
+ setState(() {
8
+ _dragAlignment = _animation.value;
9
+ });
10
+ });
6
11
  }

接下来,让Align 小部件使用_dragAlignment 字段

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

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

lib/{step2.dart (gesture) → step3.dart (gesture)} RENAMED
@@ -1,5 +1,7 @@
1
1
  return GestureDetector(
2
- onPanDown: (details) {},
2
+ onPanDown: (details) {
3
+ _controller.stop();
4
+ },
3
5
  onPanUpdate: (details) {
4
6
  setState(() {
5
7
  _dragAlignment += Alignment(
@@ -8,7 +10,9 @@
8
10
  );
9
11
  });
10
12
  },
11
- onPanEnd: (details) {},
13
+ onPanEnd: (details) {
14
+ _runAnimation();
15
+ },
12
16
  child: Align(
13
17
  alignment: _dragAlignment,
14
18
  child: Card(

步骤 4:计算速度以模拟弹簧运动

#

最后一步是做一些数学运算,以计算小部件在拖动完成后时的速度。这样,小部件就可以以该速度真实地继续移动,然后再弹回。(_runAnimation 方法已经通过设置动画的开始和结束对齐方式来设置方向。)

首先,导入physics

dart
import 'package:flutter/physics.dart';

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

最后,AnimationController 有一个 animateWith() 方法,可以接受一个 SpringSimulation

dart
/// 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()

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

交互式示例

#
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';

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

class PhysicsCardDragDemo extends StatelessWidget {
  const PhysicsCardDragDemo({super.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, super.key});

  final Widget child;

  @override
  State<DraggableCard> 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,
        ),
      ),
    );
  }
}