使用 Flutter 制作翻转动画

第一次看到 AnimationSwitcher 微件时,我以为它可以把翻转微件。

我们想要的效果

后来发现我理解错了:AnimationSwitcher 可通过用户选择的动画效果在多个微件之间切换,默认动画效果是是淡出。

1_gLmCtOi65vBl7z8ljFNoAw

它的优点是简单易上手,我会在下文展示如何切换动画。

另外,我发现了一个可以翻转动画的 flutter 包,叫作 animated_card_switcher ,但它似乎没人维护,而且代码也过于复杂。

以下是开发步骤:

  • 创建正反面微件
  • 使用 AnimationSwitcher 微件设置动画效果
  • 编写旋转卡片的自定义转场生成器
  • 添加曲线

创建正反面微件

因为前后微件不太重要,所以本示例中我会使用前后微件的简化版本。

大家只需要记住,一定要为顶级微件设置一个键,便于 AnimationSwitcher 检测到微件发生的更改,从而执行动画。

1_48-WQli2uaxyr7mwpdlSCw

我会使用下列微件布局:

Widget __buildLayout({Key key, String faceName, Color backgroundColor}) {
  return Container(
    key: key,
    decoration: BoxDecoration(
      shape: BoxShape.rectangle,
      borderRadius: BorderRadius.circular(20.0),
      color: backgroundColor,
    ),
    child: Center(
      child: Text(faceName.substring(0, 1), style: TextStyle(fontSize: 80.0)),
    ),
  );

微件的正面视图和背面视图如下:

Widget _buildFront() {
  return __buildLayout(
    key: ValueKey(true),
    backgroundColor: Colors.blue,
    faceName: "F",
  );
}

Widget _buildRear() {
  return __buildLayout(
    key: ValueKey(false),
    backgroundColor: Colors.blue.shade700,
    faceName: "R",
  );
}

使用 AnimationSwitcher 微件设置动画效果

现在,我们用 AnimationSwitcher 微件为前后微件的转场设置动画效果。

我重写了 StatefulWidget 中的 build 方法,创建了一个页面,动画会显示在该页面的中心。

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  bool _displayFront;
  bool _flipXAxis;

  @override
  void initState() {
    super.initState();
    _displayFront = true;
    _flipXAxis = true;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(this.widget.title),
        centerTitle: true,
      ),
      body: Center(
        child: Container(
          constraints: BoxConstraints.tight(Size.square(200.0)),
          child: _buildFlipAnimation(),
      ),
    );
  }
}

我将动画从页面分离到 _buildFlipAnimation 方法,使代码更清晰。

以下是该方法的第一个版本:

Widget _buildFlipAnimation() {
  return GestureDetector(
    onTap: () => setState(() =>_showFrontSide = !_showFrontSide),
    child: AnimatedSwitcher(
      duration: Duration(milliseconds: 600),
      child: _showFrontSide ? _buildFront() : _buildRear(),
    ),
  );
}

单击微件可以看到正面微件正在消失,背面微件逐渐显现。再次单击,背面微件会消失,显现正面微件。


通过使用 AnimationTransfer 对转场进行定义,我们可以实现从 Y 轴旋转微件,只需输入 TransitionBuilder (转场生成器)即可。


编写自定义转场生成器来旋转卡片

我们想要的效果是旋转 180°,只需把我们的微件包装到 AnimatedBuidler 中,使用 Transform 微件应用旋转。

Widget __transitionBuilder(Widget widget, Animation<double> animation) {
  final rotateAnim = Tween(begin: pi, end: 0.0).animate(animation);
  return AnimatedBuilder(
    animation: rotateAnim,
    child: widget,
    builder: (context, widget) {
      return Transform(
        transform: Matrix4.rotationY(value),
        child: widget,
        alignment: Alignment.center,
      );
    },
  );
}

1_QSnBx2afBRXNYUomEq1cyA

上图这个翻转没错,但缺了一些层次,不是我们真正想要的效果。而且,动画转场时背面微件一直在上层。

正面微件一闪而过

我们想要的效果是正面微件逐渐转换为背面微件。

于是我们需要修改了下列两项:

  • 替换显示顺序:要显示的微件必须在堆栈的上层。
  • 被替换的微件不再出现在动画的中间。

我们更改了 AnimationSwitcher 实例的 layoutBuilder 输入。

layoutBuilder: (widget, list) => Stack(children: [widget, ...list]),

然后,动画旋转到 90° 时,微件的宽度为 0.0,只阻止前一个微件的旋转效果。

Widget __transitionBuilder(Widget widget, Animation<double> animation) {
  final rotateAnim = Tween(begin: pi, end: 0.0).animate(animation);
  return AnimatedBuilder(
    animation: rotateAnim,
    child: widget,
    builder: (context, widget) {
      final isUnder = (ValueKey(_showFrontSide) != widget.key);
      final value = isUnder ? min(rotateAnim.value, pi / 2) : rotateAnim.value;
      return Transform(
        transform: Matrix4.rotationY(value),
        child: widget,
        alignment: Alignment.center,
      );
    },
  );
}

1_ERigH9WHhpPWLU4-hCv2Lw

这个效果好多了,但还不够完美。要想加强微件的旋转效果,需要在微件上增加些“倾斜”效果。


此“倾斜”值在动画开始和结束时必须是 0.0。另外,因为我们要在部件的两端添加动画,所以两端的倾斜值必须是相反的。例如,如果正面部件的倾斜值是 0.2,那么背面部件的倾斜值必须是 -0.2。

Widget __transitionBuilder(Widget widget, Animation<double> animation) {
  final rotateAnim = Tween(begin: pi, end: 0.0).animate(animation);
  return AnimatedBuilder(
    animation: rotateAnim,
    child: widget,
    builder: (context, widget) {
      final isUnder = (ValueKey(_showFrontSide) != widget.key);
      var tilt = ((animation.value - 0.5).abs() - 0.5) * 0.003;
      tilt *= isUnder ? -1.0 : 1.0;
      final value = isUnder ? min(rotateAnim.value, pi / 2) : rotateAnim.value;
      return Transform(
        transform: Matrix4.rotationY(value)..setEntry(3, 0, tilt),
        child: widget,
        alignment: Alignment.center,
      );
    },
  );
}

为把倾斜应用到微件中,我们手动在 Matrix4 对象中设置了定义旋转的值。

点击下列链接获取 Matrix4 的更多信息:
https://medium.com/flutter-community/advanced-flutter-matrix4-and-perspective-transformations-a79404a0d828

添加曲线

最后,如果想给动画增加一些活力/强度,可以修改 AnimationSwitcher 的曲线输入参数。

1_YMVyYHq4sGxR4_E3Mva84g

有点曲线总是更好!

这是我的第一次尝试:

Widget _buildFlipAnimation() {
  return GestureDetector(
    onTap: _switchCard,
    child: AnimatedSwitcher(
      duration: Duration(milliseconds: 4600),
      transitionBuilder: __transitionBuilder,
      layoutBuilder: (widget, list) => Stack(children: [widget, ...list]),
      child: _showFrontSide ? _buildFront() : _buildRear(),
      switchInCurve: Curves.easeInBack,
      switchOutCurve: Curves.easeOutBack,
    ),
  );
}

大家看下图这个慢镜头,应该能看到这个动画的问题。

1_oEvLEJ4DElaIkItcg0Fy3Q

因为曲线并不相同,而且前面的微件动画被反向播放,导致能看到两个没正确叠加的微件,两个微件之间有轻微差异。

1_WaLZiQOJbxOo79UVhb8prg

为避免再出现类似问题,我们必须使用曲线的翻转属性。

switchInCurve: Curves.easeInBack,
switchOutCurve: Curves.easeInBack.flipped,

1_8ZmJHWo8kitJVRZbzLKJsg

这次很完美!

总结

我只用了一个属性(微件的显示面)以及大约 30 行代码(仅动画)就完成了这个动画效果,所以用 Flutter 制作翻转动画并不难。

我不想为此创建一个软件包。在代码中添加依赖项意味着,如果它不适用于版本更新(例如),那么项目将被暂停一段时间。而且,假设不再保持依赖关系,就无法保证项目在未来 6 个月、1 或 2 年内被正确编译…

请合理复制粘贴此示例

欢迎大家使用我的代码!我不喜欢复制粘贴,所以我希望大家复制粘贴这些代码之后,能理解并根据需要对其进行个性化修改!

请点击 GONZALEZD / flutter_demos 查看本文的 demo。



原文作者:David Gonzalez
原文链接:https://medium.com/flutter-community/flutter-flip-card-animation-eb25c403f371


推荐阅读
相关专栏
开源技术
106 文章
本专栏仅用于分享音视频相关的技术文章,与其他开发者和声网 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。