- 原文地址:Zero to One with Flutter
- 原文作者:Mikkel Ravn
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:hongruqi
- 校對者:
Flutter 從 0 到 1
2016 年夏末,丹麥奧古斯谷歌辦公室。我來谷歌的第一個任務,是使用 Flutter 和 Dart 在 Android/iOS 應用程式中實現動畫圖表。除了是一個谷歌新人之外,我對 Flutter,Dart,動畫都不熟悉。事實上,我之前從未做過移動應用程式。我的第一部智慧手機也只有幾個月的歷史——我是在一陣恐慌中買的,因為擔心使用我的老諾基亞可能會導致電話面試失敗…我確實對桌面Java中的圖表有過一些經驗,但哪些圖表並不是動畫的。我感到…不可思議。部分是恐龍,部分重生.
長話短說 我發現 Flutter 的 widget 和 tween 的強大之處,在使用 Dart 開發 Android/iOS 應用程式的圖表動畫過程中。
2018 年 8 月 7 日更新,適配 Dart 2 語法。GitHub repo在 2018 年 10 月 17 日新增。下面的描述每步都是一個單獨提交。
遷移到新的開發棧可以讓您瞭解自己對技術的優先順序。在我的清單中排在前三位的是:
- 強大的概念通過提供簡單的,相關的構造方法,邏輯或資料,從而有效地處理複雜度。
- 清晰的程式碼讓我們可以清晰地表達概念,不被語言陷阱、過多的引用或者輔助細節所干擾。。
- 快速迭代是實驗和學習的關鍵 – 軟體開發團隊以學習為生:需求到底是什麼,以及如何通過最優的程式碼實現它。
Flutter 是用 Dart 實現,可以用一套程式碼同時構建 Android 和 iOS 應用的新平臺。由於我們的需求涉及到一個相當複雜的 UI,包括動畫圖表,所以只構建一次的想法似乎非常有吸引力。我的任務包括使用 Flutter 的 CLI 工具,一些預先構建的 Widgets 及其 2D 渲染引擎。除了編寫大量 Dart 程式碼來構建模型和動畫圖表外。我將在下面分享一些重點概念,併為您自己評估 Flutter/Dart 技術棧提供一個參考。
一個簡單的動畫條形圖,在開發過程中從 iOS 模擬器獲取
這是 Flutter 及其 “widgets” 和 “tween” 概念介紹的兩部分中的第一部分。我將通過使用它們實現顯示動畫(如上圖所示的圖表)來說明這些概念的強大之處。完整的程式碼示例將給你 Dart 程式碼能清晰表達問題的印象。我將包含足夠的細節,您應該能夠在自己的膝上型電腦(以及模擬器或裝置)上進行操作,並體驗 Flutter 開發週期的長度。
首先,安裝 Flutter,完成之後在終端執行。
$ flutter doctor複製程式碼
檢查設定:
$ flutter doctorDoctor summary (to see all details, run flutter doctor -v):[✓] Flutter (Channel beta, v0.5.1, on Mac OS X 10.13.6 17G65, locale en-US)[✓] Android toolchain - develop for Android devices (Android SDK 28.0.0)[✓] iOS toolchain - develop for iOS devices (Xcode 9.4)[✓] Android Studio (version 3.1)[✓] IntelliJ IDEA Community Edition (version 2018.2.1)[✓] Connected devices (1 available)• No issues found!複製程式碼
以上覆選框都滿足了,您將可以建立一個 Flutter 應用程式了。我們命名它為 charts:
$ flutter create charts複製程式碼
目錄結構:
charts android ios lib main.dart複製程式碼
大約生成 60 個檔案,組成一個可以安裝在 Android 和 iOS 上的完整示例程式。我們將在 main.dart
和它的同級檔案中完成所有編碼,而不需要觸及任何其他檔案或目錄。
您應該驗證是否可以啟動示例程式。 啟動模擬器或插入裝置,然後在 charts
目錄下,執行
$ flutter run複製程式碼
您應該在模擬器或裝置上看到一個簡單的計數應用程式。 它預設使用 MD 風格的 widgets,但這是可選的。作為 Flutter 架構的最頂層,這些 widgets 是完全可替換的。
讓我們首先用下面的程式碼替換 main.dart
的內容,作為玩轉圖表動畫的簡單起點。
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: ChartPage()));
}class ChartPage extends StatefulWidget {
@override ChartPageState createState() =>
ChartPageState();
}class ChartPageState extends State<
ChartPage>
{
final random = Random();
int dataSet;
void changeData() {
setState(() {
dataSet = random.nextInt(100);
});
} @override Widget build(BuildContext context) {
return Scaffold( body: Center( child: Text('Data set: $dataSet'), ), floatingActionButton: FloatingActionButton( child: Icon(Icons.refresh), onPressed: changeData, ), );
}
}複製程式碼
儲存更改,然後重新啟動應用程式。您可以通過按 “R” 從終端執行此操作。這種“完全重啟”操作會重置應用程式狀態,然後重建 UI。對於在程式碼更改後,現有應用程式狀態仍然有效的情況,可以按 “r” 執行“熱過載”,這隻會重建 UI。IntelliJ IDEA 安裝 Flutter 外掛,它提供了整合 Dart 編輯器相同的功能:
螢幕截圖來自 IntelliJ IDEA,帶有舊版本的 Flutter 外掛,顯示右上角的重新載入和重啟按鈕。如果已在 IDE 中啟動應用程式,則啟用這些按鈕。較新版本的外掛會在儲存時進行熱過載。
重新啟動後,應用程式會顯示一個居中的文字標籤,上面寫著 “Data set:null” 和一個浮動操作按鈕來重新整理資料。
要了解熱過載和完全重啟之間的區別,請嘗試以下操作:按幾次浮動操作按鈕後,記下當前資料集編號,然後將程式碼中的 Icons.refresh 改為 Icons.add,儲存並執行熱過載。觀察按鈕已經改變,但程式的狀態仍然保留;
我們仍然在文字上顯示獲取的隨機數。現在撤消 Icon 更改,儲存並完全重新啟動。應用程式狀態已重置,文字標籤顯示最初狀態 “Data set:null”。
我們簡單的應用程式顯示了 Flutter Widget 兩個核心方面:
- 使用者介面由不可變的 widgets 樹定義,它是通過呼叫建構函式(你可以在其中配置 widgets)和
build
方法構建的(其中 widget 可以決定子樹的外觀)。我們的應用程式生成的樹結構如下所示,每個 widget 的主要內容都在括號中。 正如您所看到的,雖然 widget 概念非常廣泛,但每個具體 widget 型別通常都具有非常集中的職責。
MaterialApp (navigation) ChartPage (state management) Scaffold (layout) Center (layout) Text (text) FloatingActionButton (user interaction) Icon (graphics)複製程式碼
- 使用不可變 widget 的不可變樹定義使用者介面,更改該介面的唯一方法是重建 widget 樹。當下一幀到期時,Flutter 會處理這個問題。我們所要做的就是告訴 Flutter 一個子樹所依賴的狀態已經改變了。這種狀態依賴子樹的根必須是
StatefulWidget
。像任何 widget 一樣,StatefulWidget
是不可變的,但是它的子樹是由State
物件構建的。Flutter 在樹重建期間保留 “State” 物件,並在構建期間將每個物件附加到新樹中的各自 widget 上。然後,他們決定 widget 的子樹是如何構建的。在我們的應用程式中,ChartPage
是一個StatefulWidget
,ChartPageState
作為它的State
。每當使用者按下按鈕時,我們執行一些程式碼來改變ChartPageState
。我們用setState
界定變化,以便 Flutter 可以進行內部處理並安排widget樹進行重建。當發生這種情況時,ChartPageState
將構建一個稍微不同的子樹,該子樹以新的ChartPage
例項為根。
不可變 widget 和狀態相關子樹是 Flutter,為了解決UI非同步響應事件,如按鈕按下,計時器滴答或傳入資料這樣複雜的狀態管理,而提供的主要工具。 從我的桌面應用開發經驗來看,我會說這種複雜性是非常真實的。評估 Flutter 的優勢,應該是讀者去實踐它:嘗試一些非平凡的事情。
我們的圖表應用程式將在 widget 結構方面保持簡單,但我們會做一些自定義檢視動畫。第一步是用非常簡單的圖表替換每個資料集的文字表示。由於資料集當前只涉及區間 “0..100” 中的單個數字,因此圖表將是帶有單個條形的條形圖,其高度由該數字決定。我們將使用初始值 “50” 來避免 “null” 高度:
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: ChartPage()));
}class ChartPage extends StatefulWidget {
@override ChartPageState createState() =>
ChartPageState();
}class ChartPageState extends State<
ChartPage>
{
final random = Random();
int dataSet = 50;
void changeData() {
setState(() {
dataSet = random.nextInt(100);
});
} @override Widget build(BuildContext context) {
return Scaffold( body: Center( child: CustomPaint( size: Size(200.0, 100.0), painter: BarChartPainter(dataSet.toDouble()), ), ), floatingActionButton: FloatingActionButton( child: Icon(Icons.refresh), onPressed: changeData, ), );
}
}class BarChartPainter extends CustomPainter {
static const barWidth = 10.0;
BarChartPainter(this.barHeight);
final double barHeight;
@override void paint(Canvas canvas, Size size) {
final paint = Paint() ..color = Colors.blue[400] ..style = PaintingStyle.fill;
canvas.drawRect( Rect.fromLTWH( (size.width - barWidth) / 2.0, size.height - barHeight, barWidth, barHeight, ), paint, );
} @override bool shouldRepaint(BarChartPainter old) =>
barHeight != old.barHeight;
}複製程式碼
CustomPaint
是一個widget,它將繪畫委託給 CustomPainter
,執行後只畫出一個條形圖。
下一步是新增動畫。每當資料集發生變化時,我們都希望條圖形平滑而不是突然地改變高度。Flutter 有一個用於編排動畫的AnimationController
類,通過註冊一個監聽器,我們被告知動畫值(從 0 到 1 的 double 值)何時發生變化。每當發生這種情況時,我們可以像以前一樣呼叫 setState
並更新 ChartPageState
。
出於解釋的原因,我們首先做一個簡單的事例:
import 'dart:math';
import 'dart:ui' show lerpDouble;
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: ChartPage()));
}class ChartPage extends StatefulWidget {
@override ChartPageState createState() =>
ChartPageState();
}class ChartPageState extends State<
ChartPage>
with TickerProviderStateMixin {
final random = Random();
int dataSet = 50;
AnimationController animation;
double startHeight;
// Strike one. double currentHeight;
// Strike two. double endHeight;
// Strike three. Refactor. @override void initState() {
super.initState();
animation = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, )..addListener(() {
setState(() {
currentHeight = lerpDouble( // Strike one. startHeight, endHeight, animation.value, );
});
});
startHeight = 0.0;
// Strike two. currentHeight = 0.0;
endHeight = dataSet.toDouble();
animation.forward();
} @override void dispose() {
animation.dispose();
super.dispose();
} void changeData() {
setState(() {
startHeight = currentHeight;
// Strike three. Refactor. dataSet = random.nextInt(100);
endHeight = dataSet.toDouble();
animation.forward(from: 0.0);
});
} @override Widget build(BuildContext context) {
return Scaffold( body: Center( child: CustomPaint( size: Size(200.0, 100.0), painter: BarChartPainter(currentHeight), ), ), floatingActionButton: FloatingActionButton( child: Icon(Icons.refresh), onPressed: changeData, ), );
}
}class BarChartPainter extends CustomPainter {
static const barWidth = 10.0;
BarChartPainter(this.barHeight);
final double barHeight;
@override void paint(Canvas canvas, Size size) {
final paint = Paint() ..color = Colors.blue[400] ..style = PaintingStyle.fill;
canvas.drawRect( Rect.fromLTWH( (size.width - barWidth) / 2.0, size.height - barHeight, barWidth, barHeight, ), paint, );
} @override bool shouldRepaint(BarChartPainter old) =>
barHeight != old.barHeight;
}複製程式碼
複雜性已經讓人頭疼,儘管我們的資料集只是一個數字!設定動畫控制元件所需的程式碼是一個次要問題,因為當我們獲得更多圖表資料時,它不會產生分支。真正的問題是變數 startHeight
,currentHeight
和 endHeight
,它們反映了對資料集和動畫值所做的更改,並在三個不同的地方進行了更新。
我們需要一個概念來處理這個爛攤子。
tweens,雖然遠非Flutter獨有,但它們是構造動畫程式碼的一個非常簡單的概念。他們的主要貢獻是用函式試方法取代上面的命令式方法。tween 是一個值。它描述了空間中的兩個點之間的路徑,如條形圖一樣,動畫值從 0 到 1 執行。
Tweens 是通用的,並且可以在 Dart 中表示為 “Tween ” 型別的物件:
abstract class Tween<
T>
{
final T begin;
final T end;
Tween(this.begin, this.end);
T lerp(double t);
}複製程式碼
專業術語 lerp
來自計算機圖形學領域,是 linear interpolation(作為名詞)和 linearly interpolate(作為動詞)的縮寫。引數 t
是動畫值,tween 應該從 begin
(當 t
為零時)到 end
(當 t
為 1 時)。
Flutter SDK 的 [Tween <
類與上面相似,但它支援
T>
](https://docs.flutter.io/flutter/animation/Tween-class.html)begin
和 end
突變。我不完全確定為什麼會做出這樣的選擇,但是在 SDK 動畫支援方面可能有很好的理由,這裡我還沒深入探索。在下面,我將使用 FlutterTween <
,假裝它是不可變的。
T>
我們可以使用 “Tween” 來代替程式碼中的條形圖高度 barHeight:
import 'dart:math';
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: ChartPage()));
}class ChartPage extends StatefulWidget {
@override ChartPageState createState() =>
ChartPageState();
}class ChartPageState extends State<
ChartPage>
with TickerProviderStateMixin {
final random = Random();
int dataSet = 50;
AnimationController animation;
Tween<
double>
tween;
@override void initState() {
super.initState();
animation = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, );
tween = Tween<
double>
(begin: 0.0, end: dataSet.toDouble());
animation.forward();
} @override void dispose() {
animation.dispose();
super.dispose();
} void changeData() {
setState(() {
dataSet = random.nextInt(100);
tween = Tween<
double>
( begin: tween.evaluate(animation), end: dataSet.toDouble(), );
animation.forward(from: 0.0);
});
} @override Widget build(BuildContext context) {
return Scaffold( body: Center( child: CustomPaint( size: Size(200.0, 100.0), painter: BarChartPainter(tween.animate(animation)), ), ), floatingActionButton: FloatingActionButton( child: Icon(Icons.refresh), onPressed: changeData, ), );
}
}class BarChartPainter extends CustomPainter {
static const barWidth = 10.0;
BarChartPainter(Animation<
double>
animation) : animation = animation, super(repaint: animation);
final Animation<
double>
animation;
@override void paint(Canvas canvas, Size size) {
final barHeight = animation.value;
final paint = Paint() ..color = Colors.blue[400] ..style = PaintingStyle.fill;
canvas.drawRect( Rect.fromLTWH( (size.width - barWidth) / 2.0, size.height - barHeight, barWidth, barHeight, ), paint, );
} @override bool shouldRepaint(BarChartPainter old) =>
false;
}複製程式碼
我們使用 Tween
將條形圖高度動畫端點打包在一個值中。它與 AnimationController
和 CustomPainter
靈活的交換,避免了動畫期間的 widgets 樹重建。Flutter 基礎架構現在標記 CustomPaint
用於在每個動畫刻度處重繪,而不是標記整個 ChartPage
子樹用於重建,重新佈局和重繪。這些都是明確的改進。但 tween 概念還有更多內容;
它提供 structure 來組織我們的想法和程式碼,但我們不用特意關注這些。Tween 動畫描述,
動畫值從0到1運動時,通過遍歷空間路徑中所有 _T_
的路徑進行動畫。用 _Tween <
對路徑建模。
T>
_
在上面的程式碼中,T
是一個 double
,但我們不想動畫是 double
,我們想要製作條形圖的動畫!嗯,好的,現在是單獨條形圖,但概念很強,如果我們有需要,可以擴充套件它。
(你可能想知道,為什麼我們不進一步討論這個問題,並且堅持資料集動畫化,而不是將其表示為條形圖。這是因為資料集與條形圖不同,條形圖是圖形物件。通常不會佔據平滑路徑存在的空間。條形圖的資料集通常涉及對映到離散資料類的數字資料。但如果沒有條形圖的空間表示,則涉及不同類別的兩個資料集之間沒有合理的平滑路徑概念。)
回到我們的程式碼,我們需要一個 Bar
型別和一個 BarTween
來為它設定動畫。讓我們將與 bar 相關的類提取到 main.dart
旁邊的 bar.dart
檔案中:
import 'dart:ui' show lerpDouble;
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';
class Bar {
Bar(this.height);
final double height;
static Bar lerp(Bar begin, Bar end, double t) {
return Bar(lerpDouble(begin.height, end.height, t));
}
}class BarTween extends Tween<
Bar>
{
BarTween(Bar begin, Bar end) : super(begin: begin, end: end);
@override Bar lerp(double t) =>
Bar.lerp(begin, end, t);
}class BarChartPainter extends CustomPainter {
static const barWidth = 10.0;
BarChartPainter(Animation<
Bar>
animation) : animation = animation, super(repaint: animation);
final Animation<
Bar>
animation;
@override void paint(Canvas canvas, Size size) {
final bar = animation.value;
final paint = Paint() ..color = Colors.blue[400] ..style = PaintingStyle.fill;
canvas.drawRect( Rect.fromLTWH( (size.width - barWidth) / 2.0, size.height - bar.height, barWidth, bar.height, ), paint, );
} @override bool shouldRepaint(BarChartPainter old) =>
false;
}複製程式碼
我在遵循一個 Flutter SDK 約定,在 Bar
類的靜態方法中定義 BarTween.lerp
。這適用於簡單型別,如 “Bar”,“Color”,“Rect” 等等,但我們需要重新考慮更多涉及圖表型別的方法。Dart SDK 中沒有 double.lerp
,所以我們使用 dart:ui
包中的 lerpDouble
函式來達到同樣的效果。
我們的應用程式現在可以用 Bar 重新表達,如下面的程式碼所示;我藉此機會呼叫 dataSet
。
import 'dart:math';
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';
import 'bar.dart';
void main() {
runApp(MaterialApp(home: ChartPage()));
}class ChartPage extends StatefulWidget {
@override ChartPageState createState() =>
ChartPageState();
}class ChartPageState extends State<
ChartPage>
with TickerProviderStateMixin {
final random = Random();
AnimationController animation;
BarTween tween;
@override void initState() {
super.initState();
animation = AnimationController( duration: const Duration(milliseconds: 300), vsync: this, );
tween = BarTween(Bar(0.0), Bar(50.0));
animation.forward();
} @override void dispose() {
animation.dispose();
super.dispose();
} void changeData() {
setState(() {
tween = BarTween( tween.evaluate(animation), Bar(random.nextDouble() * 100.0), );
animation.forward(from: 0.0);
});
} @override Widget build(BuildContext context) {
return Scaffold( body: Center( child: CustomPaint( size: Size(200.0, 100.0), painter: BarChartPainter(tween.animate(animation)), ), ), floatingActionButton: FloatingActionButton( child: Icon(Icons.refresh), onPressed: changeData, ), );
}
}複製程式碼
新版本更長,額外的程式碼被新增。這些程式碼將會出現,當我們在第二部分中解決增加的圖表複雜性時。我們的要求涉及彩條,多條,部分資料,堆疊條,分組條,堆疊和分組條,…所有這些都是動畫的。敬請關注。
我們將在第二部分中對其中一個動畫進行預覽。
如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。