阅读 2963

Flutter 3D 动画

原文链接 : Perspective on Flutter

Flutter 中的 Transform 可以实现许多酷炫的动画效果,在本篇文章中,将展示如何使用 Transfrom 来实现 3D 透视旋转效果,下面示例的效果用 Flutter 很容易实现,但是如果用原生组件来实现这个效果可能就相对来说要困难一点。

1、使用 Transform 实现 3D 效果

以创建 Flutter 项目默认生成的代码为例来展示 3D 透视效果。先通过 Transform 来实现 3D 效果。代码如下:

// v1: move default app to separate function with fixed name
// Add transform widget, rotate and perspective
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Perspective',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key); // changed

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

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  Offset _offset = Offset(0.4, 0.7); // new

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Transform(  // Transform widget
      transform: Matrix4.identity()
        ..setEntry(3, 2, 0.001) // perspective
        ..rotateX(_offset.dy)
        ..rotateY(_offset.dx),
      alignment: FractionalOffset.center,
      child: _defaultApp(context),
    );
  }

  _defaultApp(BuildContext context) {  // new
    return Scaffold(
      appBar: AppBar(
        title: Text('The Matrix 3D'), // changed
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }

}
复制代码

运行上面的代码,将会呈现稍微有一些旋转角度的 3D 效果。

为了出于演示的目的,将默认的布局代码通过 _defaultApp 方法进行了封装,然后仅仅是通过 Transfrom 来实现 3D 效果。

2、Transform widget 介绍

上面代码中,通过 Transfrom 来实现透视效果,而 Transfrom 是通过 Matrix4 进行矩阵变换来实现的这个效果。

由于现在的智能手机都有用于图形计算的 GPU 单元,对于图形的计算与渲染进行了优化,因此即使是渲染 3D 图形也是非常快的。因此,基本上你看到的手机上的所有图形,都是通过 3D 的渲染方式来呈现的,即使是 2D 的图形素材。

通过设置变换矩阵,可以改变我们看到的视觉效果(甚至是 3D 效果)。通常来讲,矩阵变换包括: 平移、旋转、缩放、透视。上面代码中,我们通过 identity_matrix 创建了一个矩阵,然后应用给 Transform 。需要注意的是,矩阵变换不满足交换律,因此参数的位置要弄对,当传入矩阵之后,最后的矩阵运算结果会传递给 GPU ,然后对图像进行渲染。

矩阵运算是一门非常复杂的学科,如果想继续了解相关知识,请参考其他的资料。

3、透视效果的实现

上面代码实现了透视的效果,也就是,更远的部分,应该看起来更小一些。因此上面的参数里面,会根据距离进行 0.001 的缩放。

那么 0.001 这个参数是怎么来的?其实这个数据很随意,可以把这个数据增大或者减小看一下效果,这个数据越大,展现的效果就好像是我们越来越靠近观察对象。

Flutter 也提供了一个 makePerspectiveMatrix 方法进行透视矩阵变换,但是这个方法需要设置一下额外的参数,这些参数我们远远用不到,因此直接使用 matrix 来完成矩阵变换即可。

同时,上面的代码通过 _offset 来指定了 x 轴和 轴的旋转。

4、手势交互

直接通过 GestureDetector 来实现手势交互。

// v2: add Gesture detector
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Perspective',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key); // changed

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

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  Offset _offset = Offset.zero; // changed

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Transform(  // Transform widget
      transform: Matrix4.identity()
        ..setEntry(3, 2, 0.001) // perspective
        ..rotateX(0.01 * _offset.dy) // changed
        ..rotateY(-0.01 * _offset.dx), // changed
      alignment: FractionalOffset.center,
      child: GestureDetector( // new
        onPanUpdate: (details) => setState(() => _offset += details.delta),
        onDoubleTap: () => setState(() => _offset = Offset.zero),
        child: _defaultApp(context),
      )
    );
  }

  _defaultApp(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('The Matrix 3D'), // changed
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }

}
复制代码

上面的手势交互只有两种:

  • DoubleTap : 双击重置
  • onPanUpdate : 移动手指,旋转图像。

5、进阶实战-翻页效果

接下来实现的效果相对复杂一点,类似翻页效果动画。

初步设计

第一眼看到这个效果,可能想到的就是,通过 Stack 来实现,并且每一页都分成上下两部分,每一部分可以绕 X 轴旋转,旋转之后就会看到下一个页面。

那么该如何用代码来实现呢?可以分成两部分来进行。

  • 将一个页面分成两部分
  • 将其中的一部分绕 X 轴旋转。

那么,在 Flutter 中,什么样的 Widget 适合我们来实现这个效果呢?ClipRect 和 Transform 。

实现

  • 将一个页面分成两部分 ClipRect 这个组件有一个参数: clipper,这个参数可以定义裁剪的矩形区域的大小和位置,但是官方文档建议我们通过另一种方式来使用 ClipRect,那就是结合 Align 来使用。

接下来定义一个 Widget 来实现这个功能。

class FlipWidget extends StatelessWidget {
  Widget child;

  FlipWidget({Key key, this.child}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        ClipRect(
            child: Align(
          alignment: Alignment.topCenter,
          heightFactor: 0.5,
          child: child,
        )),
        Padding(
          padding: EdgeInsets.only(top: 2.0),
        ),
        ClipRect(
            child: Align(
          alignment: Alignment.bottomCenter,
          heightFactor: 0.5,
          child: child,
        )),
      ],
    );
  }
}
复制代码

这里面的 child 参数,可以传递任意类型的 Widget(text,image 等)。 运行上面的代码,可以看到如下的效果。

  • 实现翻转效果

Transform 这个 Widget 组件有一个 Matrix4 类型的参数 transform,这个参数决定了我们将应用何种类型的矩阵变换。同时,Matrix4 提供了一个名字为 rotationX() 的构造方法,这个似乎正是我们需要的,我们把这个应用给页面的上半部分试一下。

@override
Widget build(BuildContext context) {
   return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Transform(
          transform: Matrix4.rotationX(pi / 4),
          alignment: Alignment.bottomCenter,
          child: ClipRect(
              child: Align(
            alignment: Alignment.topCenter,
            heightFactor: 0.5,
            child: child,
          )),
        ),
        ...
      ],
    );
  }
复制代码

运行上面的代码。

显然,这个效果仅仅是把上半部分缩小了,不是我们想要的效果。但是如果额外再指定 Matrix4 的参数,让 row 为 3,column 为 2,试一下效果。

Transform(
  transform: Matrix4.identity()..setEntry(3, 2, 0.006)..rotateX(pi / 4),
  alignment: Alignment.bottomCenter,
  child: ClipRect(
      child: Align(
    alignment: Alignment.topCenter,
    heightFactor: 0.5,
    child: child,
  )),
),
...
复制代码

看起来这个是我们需要的效果,上面还一个参数,0.006,这个是怎么来的?其实是试出来的,选一个自己感觉不错的数值就行了😂。

接下来就是给翻转加上动画了。但是这块可能相对复杂一点。首先,每一页都要理解为有两面(正反面),但是要实现这个效果用代码可能不是很容易,因为我们在手机上看到的图像在任何时刻都只有一面。

我们假设,我们是向上翻转的,那么我们的动画可以分成两部分,第一部分是我们将下半部分向上翻转一半时,这个过程的效果是,当前翻转的页面逐渐消失,而这个页面的下一个页面会逐渐显示。第二部分是,将当前页面继续向上翻转,这个过程的效果是,当前页面会逐渐显示,上半部分的当前页面就是逐渐消失。

这个效果的实现,代码非常多,更详细的代码请参考:

https://gist.github.com/hnvn/f1094fb4f6902078516cba78de9c868e
复制代码

最终实现效果:


github


最后

欢迎关注「Flutter 编程开发」微信公众号 。

文章分类
阅读
文章标签