Flutter 3D 動畫

Flutter程式設計開發發表於2020-04-12

原文連結 : Perspective on Flutter

Flutter 中的 Transform 可以實現許多酷炫的動畫效果,在本篇文章中,將展示如何使用 Transfrom 來實現 3D 透視旋轉效果,下面示例的效果用 Flutter 很容易實現,但是如果用原生元件來實現這個效果可能就相對來說要困難一點。

Flutter 3D 動畫

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 效果。

Flutter 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、進階實戰-翻頁效果

接下來實現的效果相對複雜一點,類似翻頁效果動畫。

Flutter 3D 動畫

初步設計

第一眼看到這個效果,可能想到的就是,通過 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 等)。 執行上面的程式碼,可以看到如下的效果。

Flutter 3D 動畫

  • 實現翻轉效果

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,
          )),
        ),
        ...
      ],
    );
  }
複製程式碼

執行上面的程式碼。

Flutter 3D 動畫

顯然,這個效果僅僅是把上半部分縮小了,不是我們想要的效果。但是如果額外再指定 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,
  )),
),
...
複製程式碼

Flutter 3D 動畫

看起來這個是我們需要的效果,上面還一個引數,0.006,這個是怎麼來的?其實是試出來的,選一個自己感覺不錯的數值就行了?。

接下來就是給翻轉加上動畫了。但是這塊可能相對複雜一點。首先,每一頁都要理解為有兩面(正反面),但是要實現這個效果用程式碼可能不是很容易,因為我們在手機上看到的影像在任何時刻都只有一面。

我們假設,我們是向上翻轉的,那麼我們的動畫可以分成兩部分,第一部分是我們將下半部分向上翻轉一半時,這個過程的效果是,當前翻轉的頁面逐漸消失,而這個頁面的下一個頁面會逐漸顯示。第二部分是,將當前頁面繼續向上翻轉,這個過程的效果是,當前頁面會逐漸顯示,上半部分的當前頁面就是逐漸消失。

Flutter 3D 動畫

這個效果的實現,程式碼非常多,更詳細的程式碼請參考:

https://gist.github.com/hnvn/f1094fb4f6902078516cba78de9c868e
複製程式碼

最終實現效果:

Flutter 3D 動畫


github


最後

歡迎關注「Flutter 程式設計開發」微信公眾號 。

Flutter 3D 動畫

相關文章