- 原文地址:Zero to One with Flutter, Part Two
- 原文做者:Mikkel Ravn
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:hongruqi
- 校對者:Fengziyin1234
探索如何在跨平臺移動應用程序的上下文中爲複合圖形對象設置動畫。引入一個新的概念,如何將 tween 動畫應用於結構化值中,例如條形圖表。所有代碼,按步驟實現。前端
修訂:2018 年 8 月 8 日適配 Dart 2。GitHub repo 而且差別連接於 2018 年 10 月 17 日添加。android
如何進入一個新的編程領域 ?實踐是相當重要的,由於那是學習和模仿更有經驗同行的代碼。我我的喜歡挖掘概念:試着從最基本的原則出發,識別各類概念,探索它們的優點,有意識地尋求它們的本質。這是一種理性主義的方法,它不能獨立存在,而是一種智力刺激的方法,能夠更快地引導你得到更深刻的看法。ios
這是 Flutter 及其 widget 和 tween 概念介紹的第二部分也是最後一部分。在 Flutter 從 0 到 1 第一部分 最後,在咱們這麼多 widges 的選擇中,這個 tree 包含了下面兩個:git
高度動畫 製做 Bar 的高度的動畫github
這個動畫是經過 BarTween
來實現的,在第一部分中我曾經代表 tween
的概念能夠擴展開去解決更加複雜的問題,這裏咱們會將會經過爲更多屬性的和多種配置下的條形圖做出設計來證實這一點。算法
首先咱們爲單個條形圖添加顏色屬性。在 Bar
類的 height
字段旁邊添加一個 color
字段,並更新 Bar.lerp
對它們進行線性插值。這種模式很典型:編程
經過線性插值對應的元件,生成 tween 的合成值。canvas
回想一下第一部分,lerp
是 線性插值
的縮寫。後端
import 'dart:ui' show lerpDouble;
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';
class Bar {
Bar(this.height, this.color);
final double height;
final Color color;
static Bar lerp(Bar begin, Bar end, double t) {
return Bar(
lerpDouble(begin.height, end.height, t),
Color.lerp(begin.color, end.color, 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);
}
複製代碼
注意這裏對與 lerp
的使用。若是沒有 Bar.lerp
,lerpDouble
(一般爲 double.lerp
)和 Color.lerp
,咱們就必須經過爲高度建立 Tween <double>
同時爲顏色建立 Tween<Color>
來實現 BarTween
。這些 tweens 將是 BarTween
的實例字段,由構造函數初始化,並在其 lerp
方法中使用。 咱們將在 Bar
類以外屢次重複訪問 Bar
的屬性。代碼維護者可能會發現這並非一個好主意。
爲條形的顏色和高度製做動畫。
爲了在應用程序中使用彩色條,咱們將更新 BarChartPainter
來從 Bar
得到條形圖顏色。在 main.dart
中,咱們須要有能力來建立一個空的 Bar
和一個隨機的 Bar
。咱們將爲前者使用徹底透明的顏色,爲後者使用隨機顏色。 顏色將從一個簡單的 ColorPalette
類中獲取,咱們會在它本身的文件中快速實現它。 咱們將在 Bar
類中建立 Bar.empty
和 Bar.random
兩個工廠構造函數 (code listing, diff).
條形圖涉及各類配置的多種形式。爲了緩慢地引入複雜性,咱們的第一個實現將適用於顯示固定類別數的值條形圖。示例包括每一個工做日的訪問者或每季度的銷售額。對於此類圖表,將數據集更改成另外一週或另外一年不會更改使用的類別,只會更改每一個類別顯示的欄。
咱們首先更新 main.dart
,用 BarChart
替換 Bar
,用 BarChartTween
替換 BarTween
(代碼列表,差分)。
爲了更好體現 Dart 語言優點,咱們在 bar.dart
中建立 BarChart
類,並使用固定數目的 Bar
實例列表來實現它。咱們將使用五個條形圖,表示一週中的工做日。而後,咱們須要將建立空條和隨機條的函數從 Bar
類中轉移到 BarChart
類中。對於固定類別,空條形圖合理地被視爲空條的集合。另外一方面,讓隨機條形圖成爲隨機條形圖的集合會使咱們的圖表變得多種多樣。相反,咱們將爲圖表選擇一種隨機顏色,讓每一個仍然具備隨機高度的條形繼承該圖形。
import 'dart:math';
import 'dart:ui' show lerpDouble;
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';
import 'color_palette.dart';
class BarChart {
static const int barCount = 5;
BarChart(this.bars) {
assert(bars.length == barCount);
}
factory BarChart.empty() {
return BarChart(List.filled(
barCount,
Bar(0.0, Colors.transparent),
));
}
factory BarChart.random(Random random) {
final Color color = ColorPalette.primary.random(random);
return BarChart(List.generate(
barCount,
(i) => Bar(random.nextDouble() * 100.0, color),
));
}
final List<Bar> bars;
static BarChart lerp(BarChart begin, BarChart end, double t) {
return BarChart(List.generate(
barCount,
(i) => Bar.lerp(begin.bars[i], end.bars[i], t),
));
}
}
class BarChartTween extends Tween<BarChart> {
BarChartTween(BarChart begin, BarChart end) : super(begin: begin, end: end);
@override
BarChart lerp(double t) => BarChart.lerp(begin, end, t);
}
class Bar {
Bar(this.height, this.color);
final double height;
final Color color;
static Bar lerp(Bar begin, Bar end, double t) {
return Bar(
lerpDouble(begin.height, end.height, t),
Color.lerp(begin.color, end.color, 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 barWidthFraction = 0.75;
BarChartPainter(Animation<BarChart> animation)
: animation = animation,
super(repaint: animation);
final Animation<BarChart> animation;
@override
void paint(Canvas canvas, Size size) {
void drawBar(Bar bar, double x, double width, Paint paint) {
paint.color = bar.color;
canvas.drawRect(
Rect.fromLTWH(x, size.height - bar.height, width, bar.height),
paint,
);
}
final paint = Paint()..style = PaintingStyle.fill;
final chart = animation.value;
final barDistance = size.width / (1 + chart.bars.length);
final barWidth = barDistance * barWidthFraction;
var x = barDistance - barWidth / 2;
for (final bar in chart.bars) {
drawBar(bar, x, barWidth, paint);
x += barDistance;
}
}
@override
bool shouldRepaint(BarChartPainter old) => false;
}
複製代碼
BarChartPainter
在條形圖中寬度均勻分佈,使每一個條形佔據可用寬度的 75%。
固定類別條形圖。
注意 BarChart.lerp
是如何調用 Bar.lerp
實現的,使用 List.generate
生產列表結構。固定類別條形圖是複合值,對於這些複合值,直接使用 lerp
進行有意義的組合,正如具備多個屬性的單個條形圖同樣(diff)。
這裏有一種模式。當 Dart 類的構造函數採用多個參數時,你一般能夠線性插值單個參數或多個。你能夠任意地嵌套這種模式:在 dashboard
中插入 bar charts
,在 bar charts
中插入 bars
,在 bars
中插入它們的高度和顏色。顏色 RGB 和 alpha 經過線性插值來組合。整個過程,就是遞歸葉節點上的值,進行線性插值。
在數學上傾向於用 _C_(_x_, _y_)
來表達複合的線性插值結構,而編程實踐中咱們用 _lerp_(_C_(_x_1, _y_1), _C_(_x_2, _y_2), _t_) == _C_(_lerp_(_x_1, _x_2, _t_), _lerp_(_y_1, _y_2, _t_))
正如咱們所看到的,這很好地歸納了兩個元件(條形圖的高度和顏色)到任意多個元件(固定類別 n 條條形圖)。
固然,(這個表示方法)也有一些這個解決不了的問題。咱們但願在兩個不以徹底相同的方式組成的值之間進行動畫處理。舉個簡單的例子,考慮動畫圖表處理從包含工做日,到包括週末的狀況。
你可能很容易想出這個問題的幾種不一樣的臨時解決方案,而後可能會要求你的UX設計師在它們之間進行選擇。這是一種有效的方法,但我認爲在討論過程當中要記住這些不一樣解決方案共有的基本結構:tween
。回憶第一部分:
**動畫值從 0 到 1 運動時,經過遍歷空間路徑中全部 _T_
的路徑進行動畫。用 Tween_ _<T>_
對路徑建模。_
用戶體驗設計師要回答的核心問題是:圖表有五個條形圖和一個有七個條形圖的中間值是多少? 顯而易見的選擇是六個條形圖。 可是要使他的動畫平滑,咱們須要比六個條形圖更多中間值。咱們須要以不一樣方式繪製條形圖,跳出等寬,均勻間隔,適合的 200 像素設置 這些具體的設置。換句話說,T
的值必須是通用的。
經過將值嵌入到通用數據中,在具備不一樣結構的值之間進行線性插值,包括動畫端點和全部中間值所需的特殊狀況。
咱們能夠分兩步完成。第一步,在 Bar
類中包含 x 座標屬性和寬度屬性:
class Bar {
Bar(this.x, this.width, this.height, this.color);
final double x;
final double width;
final double height;
final Color color;
static Bar lerp(Bar begin, Bar end, double t) {
return Bar(
lerpDouble(begin.x, end.x, t),
lerpDouble(begin.width, end.width, t),
lerpDouble(begin.height, end.height, t),
Color.lerp(begin.color, end.color, t),
);
}
}
複製代碼
第二步,咱們使 BarChart
支持具備不一樣條形數的圖表。咱們的新圖表將適用於數據集,其中條形圖 i 表明某些系列中的第 i 個值,例如產品發佈後的第 i 天的銷售額。Counting as programmers,任何這樣的圖表都涉及每一個整數值 0..n 的條形圖,但條形圖數 n 可能在各個圖表中表示的意義不一樣。
考慮兩個圖表分別有五個和七個條形圖。五個常見類別的條形圖 0..5 像上面咱們看到的那樣進行動畫。索引爲5和6的條形在另外一個動畫終點沒有對應條,但因爲咱們如今能夠自由地給每一個條形圖設置位置和寬度,咱們能夠引入兩個不可見的條形來扮演這個角色。視覺效果是當動畫進行時,第 5 和第 6 條會減弱或淡化爲隱形的。
經過線性插值對應的元件,生成 tween 的合成值。若是某個端點缺乏元件,在其位置使用不可見元件。
一般有幾種方法能夠選擇隱形元件。假設咱們友好的用戶體驗設計師決定使用零寬度,零高度的條形圖,其中 x 座標和顏色從它們的可見元件繼承而來。咱們將爲 Bar
類添加一個方法,用於處理這樣的實例。
class BarChart {
BarChart(this.bars);
final List<Bar> bars;
static BarChart lerp(BarChart begin, BarChart end, double t) {
final barCount = max(begin.bars.length, end.bars.length);
final bars = List.generate(
barCount,
(i) => Bar.lerp(
begin._barOrNull(i) ?? end.bars[i].collapsed,
end._barOrNull(i) ?? begin.bars[i].collapsed,
t,
),
);
return BarChart(bars);
}
Bar _barOrNull(int index) => (index < bars.length ? bars[index] : null);
}
class BarChartTween extends Tween<BarChart> {
BarChartTween(BarChart begin, BarChart end) : super(begin: begin, end: end);
@override
BarChart lerp(double t) => BarChart.lerp(begin, end, t);
}
class Bar {
Bar(this.x, this.width, this.height, this.color);
final double x;
final double width;
final double height;
final Color color;
Bar get collapsed => Bar(x, 0.0, 0.0, color);
static Bar lerp(Bar begin, Bar end, double t) {
return Bar(
lerpDouble(begin.x, end.x, t),
lerpDouble(begin.width, end.width, t),
lerpDouble(begin.height, end.height, t),
Color.lerp(begin.color, end.color, t),
);
}
}
複製代碼
將上述代碼集成到咱們的應用程序中,涉及從新定義 BarChart.empty
和 BarChart.random
。如今能夠合理地將空條形圖設置包含零條,而隨機條形圖能夠包含隨機數量的條,全部條都具備相同的隨機選擇顏色,而且每一個條具備隨機選擇的高度。但因爲位置和寬度如今是 Bar
類定義的,咱們須要 BarChart.random
來指定這些屬性。用圖表 Size
做爲BarChart.random
的參數彷佛是合理的,這樣能夠解除 BarChartPainter.paint
大部分計算(代碼列表,差分)。
隱藏條形圖線性插值。
大多數讀者可能已經注意 BarChart.lerp
有潛在的效率問題。咱們建立 Bar
實例只是做爲參數提供給 Bar.lerp
函數,而且對於每一個動畫參數的 t
值都是重複調用。每秒 60 幀,即便是相對較短的動畫,也意味着不少 Bar
實例被送到垃圾收集器。咱們還有其餘選擇:
Bar
實例能夠經過在 Bar
類中建立一次而不是每次調用 collapsed
來從新生成。這種方法適用於此,但並不通用。
能夠用 BarChartTween
來處理重用問題,方法是讓 BarChartTween
的構造函數建立條形圖列表時使用的 BarTween
實例的列表 _tween
:(i)=> _tweens [i] .lerp(t )
。這種方法打破了整個使用靜態lerp
方法的慣例。靜態BarChart.lerp
不會在動畫持續時間內存儲 tween 列表的對象。相比之下,BarChartTween
對象很是適合這種狀況。
假設處理邏輯在 Bar.lerp
中,null
條可用於表示摺疊條。這種方法既靈活又高效,但須要注意避免引用或誤解 null
。在 Flutter SDK 中,靜態 lerp
方法傾向於接受 null
做爲動畫終點,一般將其解釋爲某種不可見元件,如徹底透明的顏色或零大小的圖形元件。做爲最基本的例子,除非兩個動畫端點都是 null
以外 lerpDouble
將 null
視爲 0。
下面的代碼段顯示了咱們如何處理 null
:
class BarChart {
BarChart(this.bars);
final List<Bar> bars;
static BarChart lerp(BarChart begin, BarChart end, double t) {
final barCount = max(begin.bars.length, end.bars.length);
final bars = List.generate(
barCount,
(i) => Bar.lerp(begin._barOrNull(i), end._barOrNull(i), t),
);
return BarChart(bars);
}
Bar _barOrNull(int index) => (index < bars.length ? bars[index] : null);
}
class BarChartTween extends Tween<BarChart> {
BarChartTween(BarChart begin, BarChart end) : super(begin: begin, end: end);
@override
BarChart lerp(double t) => BarChart.lerp(begin, end, t);
}
class Bar {
Bar(this.x, this.width, this.height, this.color);
final double x;
final double width;
final double height;
final Color color;
static Bar lerp(Bar begin, Bar end, double t) {
if (begin == null && end == null)
return null;
return Bar(
lerpDouble((begin ?? end).x, (end ?? begin).x, t),
lerpDouble(begin?.width, end?.width, t),
lerpDouble(begin?.height, end?.height, t),
Color.lerp((begin ?? end).color, (end ?? begin).color, t),
);
}
}
複製代碼
我認爲公正的說 Dart 的 ?
語法很是適合這項任務。但請注意,使用摺疊(而不是透明)條形圖做爲不可見元件的決定如今隱藏在 Bar.lerp
中。這是我以前選擇看似效率較低的解決方案的主要緣由。與性能與可維護性同樣,你的選擇應基於實踐。
在完整地處理條形圖動畫以前,咱們還有一個步要作。考慮使用條形圖的應用程序,按給定年份的產品類別顯示銷售額。用戶能夠選擇另外一年,而後應用應該爲該年的條形圖設置動畫。若是兩年的產品類別相同,或者剛好相同,除了其中一個圖表右側顯示的其餘類別,咱們可使用上面的現有代碼。可是,若是公司在 2016 年擁有 A、B、C 和 X 類產品,可是已經停產 B 並在 2017 年引入了 D,那該怎麼辦?咱們現有的代碼動畫以下:
2016 2017
A -> A
B -> C
C -> D
X -> X
複製代碼
動畫多是美麗而流暢的,但它仍然會讓用戶感到困惑。爲何?由於它不保留語義。它將表示產品類別 B 的圖形元件轉換爲表示類別 C 的圖形元件,而將 C 表示元件轉移到其餘地方。僅僅由於 2016 B 剛好被繪製在 2017 C 後來出現的相同位置,並不意味着前者應該變成後者。相反,2016 B 應該消失,2016 C 應該向左移動並變爲 2017 C,2017 D 應該出如今右邊。咱們可使用書中最古老的算法之一來實現這種融合:合併排序列表。
經過線性插值對應的元件,生成 tween 的合成值。當元素造成排序列表時,合併算法可使這些元素處於同等水平,根據須要使用不可見元素來處理單側合併。
咱們所須要的只是使 Bar
實例按線性順序相互比較。而後咱們能夠合併它們,以下:
static BarChart lerp(BarChart begin, BarChart end, double t) {
final bars = <Bar>[];
final bMax = begin.bars.length;
final eMax = end.bars.length;
var b = 0;
var e = 0;
while (b + e < bMax + eMax) {
if (b < bMax && (e == eMax || begin.bars[b] < end.bars[e])) {
bars.add(Bar.lerp(begin.bars[b], begin.bars[b].collapsed, t));
b++;
} else if (e < eMax && (b == bMax || end.bars[e] < begin.bars[b])) {
bars.add(Bar.lerp(end.bars[e].collapsed, end.bars[e], t));
e++;
} else {
bars.add(Bar.lerp(begin.bars[b], end.bars[e], t));
b++;
e++;
}
}
return BarChart(bars);
}
複製代碼
具體地說,咱們將爲 bar 添加 rank
屬性做一個排序鍵。rank
也能夠方便地用於爲每一個欄分配調色板中的顏色,從而容許咱們跟蹤動畫演示中各個小節的移動。
隨機條形圖如今將基於隨機選擇的 rank
來包括(代碼列表,diff)。
任意類別。合併基礎,線性插值。
乾的不錯,但也許不是最有效的解決方案。 咱們在 BarChart.lerp
中重複執行合併算法,對於 t
的每一個值都執行一次。爲了解決這個問題,咱們將實現前面提到的想法,將可重用信息存儲在 BarChartTween
中。
class BarChartTween extends Tween<BarChart> {
BarChartTween(BarChart begin, BarChart end) : super(begin: begin, end: end) {
final bMax = begin.bars.length;
final eMax = end.bars.length;
var b = 0;
var e = 0;
while (b + e < bMax + eMax) {
if (b < bMax && (e == eMax || begin.bars[b] < end.bars[e])) {
_tweens.add(BarTween(begin.bars[b], begin.bars[b].collapsed));
b++;
} else if (e < eMax && (b == bMax || end.bars[e] < begin.bars[b])) {
_tweens.add(BarTween(end.bars[e].collapsed, end.bars[e]));
e++;
} else {
_tweens.add(BarTween(begin.bars[b], end.bars[e]));
b++;
e++;
}
}
}
final _tweens = <BarTween>[];
@override
BarChart lerp(double t) => BarChart(
List.generate(
_tweens.length,
(i) => _tweens[i].lerp(t),
),
);
}
複製代碼
咱們如今能夠刪除靜態方法 BarChart.lerp
(diff)。
讓咱們總結一下到目前爲止咱們對 tween
概念的理解:
動畫 T 經過在全部 T 的空間中描繪出一條路徑做爲動畫值,在 0 到 1 之間運行。使用 _Tween <T> _
路徑建模。
先泛化 _T_
的概念,直到它包含全部動畫端點和中間值。
經過線性插值對應的元件,生成 tween 的合成值。
考慮使用靜態方法 _Xxx.lerp_
實現 tweens
,以便在實現複合 tween
實現時重用。對單個動畫路徑調用 _Xxx.lerp_
進行重要的從新計算,請考慮將計算移動到 _XxxTween_
類的構造函數,並讓其實例承載計算結果。 。_
有了這些看法,咱們終於有了將更復雜的圖表動畫化的能力。咱們將快速連續地實現堆疊條形圖,分組條形圖和堆疊 + 分組條形圖:
堆疊條形圖。
分組條形圖。
堆疊 + 分組條形圖。
在全部三種變體中,動畫可用於可視化數據集更改,從而引入額外的維度(一般是時間)而不會使圖表混亂。
爲了使動畫有用而不只僅是漂亮,咱們須要確保咱們只在語義相應的元件之間進 lerp
。所以,用於表示 2016 年特定產品/地區/渠道收入的條形段,應變爲 2017 年相同產品/區域/渠道(若是存在)的收入。
合併算法可用於確保這一點。 正如你在前面的討論中所猜想的那樣,合併將被用於多個層面,來反應類別的維度。咱們將在堆積圖表中組合堆和條形圖,在分組圖表中合併組和條形圖,以及堆疊 + 分組圖表中組合上面三個。
爲了減小重複代碼,咱們將合併算法抽象爲通用工具,並將其放在本身的文件 tween.dart
中:
import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';
abstract class MergeTweenable<T> {
T get empty;
Tween<T> tweenTo(T other);
bool operator <(T other);
}
class MergeTween<T extends MergeTweenable<T>> extends Tween<List<T>> {
MergeTween(List<T> begin, List<T> end) : super(begin: begin, end: end) {
final bMax = begin.length;
final eMax = end.length;
var b = 0;
var e = 0;
while (b + e < bMax + eMax) {
if (b < bMax && (e == eMax || begin[b] < end[e])) {
_tweens.add(begin[b].tweenTo(begin[b].empty));
b++;
} else if (e < eMax && (b == bMax || end[e] < begin[b])) {
_tweens.add(end[e].empty.tweenTo(end[e]));
e++;
} else {
_tweens.add(begin[b].tweenTo(end[e]));
b++;
e++;
}
}
}
final _tweens = <Tween<T>>[];
@override
List<T> lerp(double t) => List.generate(
_tweens.length,
(i) => _tweens[i].lerp(t),
);
}
複製代碼
MergeTweenable <T>
接口精確得到合併兩個有序的 T
列表的所需的 tween
內容。咱們將使用 Bar
,BarStack
和 BarGroup
實例化泛型參數 T
,而且實現 MergeTweenable <T>
(diff)。
stacked(diff)、grouped(diff)和 stacked+grouped(diff)已經完成實現。我建議你本身實踐一下:
BarChart.random
建立的 groups、stacks 和 bars 的數量。stacked+grouped
,我使用了單色調色板,由於我以爲它看起來更好。你和你的 UX 設計師可能並不認同。BarChart.random
和浮動操做按鈕替換爲年份選擇器,並以實際數據集建立 BarChart
實例。MergeTweenable <T>
或相似方法爲它們設置動畫。最後兩個任務很是具備挑戰性。不妨試試。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。