物理模拟可以使应用交互感觉真实且互动性强。例如,你可能希望为小部件添加动画,使其表现得像是附着在弹簧上或因重力下落。

本示例演示了如何使用弹簧模拟将一个小部件从拖动点移回中心。

本示例将使用以下步骤

  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

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

  @override
  void initState() {
    super.initState();
    _controller =
        AnimationController(vsync: this, duration: const Duration(seconds: 1));
  }

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

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

#

让小部件在拖动时移动,并向 _DraggableCardState 类添加一个 Alignment 字段

Dart
class _DraggableCardState extends State<DraggableCard>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  Alignment _dragAlignment = Alignment.center;

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

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

步骤 3:为小部件添加动画

#

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

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

Dart
class _DraggableCardState extends State<DraggableCard>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<Alignment> _animation;
  Alignment _dragAlignment = Alignment.center;
Dart
void _runAnimation() {
  _animation = _controller.drive(
    AlignmentTween(begin: _dragAlignment, end: Alignment.center),
  );
  _controller.reset();
  _controller.forward();
}

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

Dart
@override
void initState() {
  super.initState();
  _controller =
      AnimationController(vsync: this, duration: const Duration(seconds: 1));
  _controller.addListener(() {
    setState(() {
      _dragAlignment = _animation.value;
    });
  });
}

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

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

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

Dart
return GestureDetector(
  onPanDown: (details) {},
  onPanDown: (details) {
    _controller.stop();
  },
  onPanUpdate: (details) {
    // ...
  },
  onPanEnd: (details) {},
  onPanEnd: (details) {
    _runAnimation();
  },
  child: Align(

步骤 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),
      ),
    );
  }
}