原文連結 : 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
複製程式碼
最終實現效果:
最後
歡迎關注「Flutter 程式設計開發」微信公眾號 。