- 原文地址: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 日新增。
如何進入一個新的程式設計領域 ?實踐是至關重要的,因為那是學習和模仿更有經驗同行的程式碼。我個人喜歡挖掘概念:試著從最基本的原則出發,識別各種概念,探索它們的優勢,有意識地尋求它們的本質。這是一種理性主義的方法,它不能獨立存在,而是一種智力刺激的方法,可以更快地引導你獲得更深入的見解。
這是 Flutter 及其 widget 和 tween 概念介紹的第二部分也是最後一部分。在 Flutter 從 0 到 1 第一部分 最後,在我們這麼多 widges 的選擇中,這個 tree 包含了下面兩個:
- 一個使用自定義動畫繪製程式碼, 繪製單 一 Bar 的 widget
- 初始化一個 Bar 的高度的 widget
高度動畫 製作 Bar 的高度的動畫
這個動畫是通過 BarTween
來實現的,在第一部分中我曾經表明 tween
的概念可以擴充套件開去解決更加複雜的問題,這裡我們會將會通過為更多屬性的和多種配置下的條形圖作出設計來證明這一點。
首先我們為單個條形圖新增顏色屬性。在 Bar
類的 height
欄位旁邊新增一個 color
欄位,並更新 Bar.lerp
對它們進行線性插值。這種模式很典型:
通過線性插值對應的元件,生成 tween 的合成值。
回想一下第一部分,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、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。