[譯] Flutter 從 0 到 1, 第二部分

Flutter 從 0 到 1(第一部分)html

探索如何在跨平臺移動應用程序的上下文中爲複合圖形對象設置動畫。引入一個新的概念,如何將 tween 動畫應用於結構化值中,例如條形圖表。所有代碼,按步驟實現。前端

修訂:2018 年 8 月 8 日適配 Dart 2。GitHub repo 而且差別連接於 2018 年 10 月 17 日添加。android


如何進入一個新的編程領域 ?實踐是相當重要的,由於那是學習和模仿更有經驗同行的代碼。我我的喜歡挖掘概念:試着從最基本的原則出發,識別各類概念,探索它們的優點,有意識地尋求它們的本質。這是一種理性主義的方法,它不能獨立存在,而是一種智力刺激的方法,能夠更快地引導你得到更深刻的看法。ios

這是 Flutter 及其 widget 和 tween 概念介紹的第二部分也是最後一部分。在 Flutter 從 0 到 1 第一部分 最後,在咱們這麼多 widges 的選擇中,這個 tree 包含了下面兩個:git

  • 一個使用自定義動畫繪製代碼, 繪製單 一 Bar 的 widget
  • 初始化一個 Bar 的高度的 widget

高度動畫 製做 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.lerplerpDouble(一般爲 double.lerp)和 Color.lerp,咱們就必須經過爲高度建立 Tween <double> 同時爲顏色建立 Tween<Color> 來實現 BarTween。這些 tweens 將是 BarTween 的實例字段,由構造函數初始化,並在其 lerp 方法中使用。 咱們將在 Bar 類以外屢次重複訪問 Bar 的屬性。代碼維護者可能會發現這並非一個好主意。

爲條形的顏色和高度製做動畫。

爲了在應用程序中使用彩色條,咱們將更新 BarChartPainter 來從 Bar 得到條形圖顏色。在 main.dart 中,咱們須要有能力來建立一個空的 Bar 和一個隨機的 Bar。咱們將爲前者使用徹底透明的顏色,爲後者使用隨機顏色。 顏色將從一個簡單的 ColorPalette 類中獲取,咱們會在它本身的文件中快速實現它。 咱們將在 Bar 類中建立 Bar.emptyBar.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.emptyBarChart.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 以外 lerpDoublenull 視爲 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.lerpdiff)。


讓咱們總結一下到目前爲止咱們對 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 內容。咱們將使用 BarBarStackBarGroup 實例化泛型參數 T,而且實現 MergeTweenable <T>diff)。

stackeddiff)、groupeddiff)和 stacked+groupeddiff)已經完成實現。我建議你本身實踐一下:

  • 更改 BarChart.random建立的 groups、stacks 和 bars 的數量。
  • 更改調色板。對於 stacked+grouped,我使用了單色調色板,由於我以爲它看起來更好。你和你的 UX 設計師可能並不認同。
  • BarChart.random 和浮動操做按鈕替換爲年份選擇器,並以實際數據集建立 BarChart 實例。
  • 實現水平條形圖。
  • 實現其餘圖表類型(餅圖,線條,堆積區域)。使用 MergeTweenable <T> 或相似方法爲它們設置動畫。
  • 添加圖表圖例,標籤,座標軸,而後爲它們設置動畫。

最後兩個任務很是具備挑戰性。不妨試試。

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索