[譯]Flutter for Android Developers - Views

catsuo發表於2018-02-28

先說兩句

關於Flutter就不多介紹了,它同樣是一個致力於開發跨平臺移動應用的SDK,使用Dart語言。Dart是Google家的語言,雖然比較低調,但也確實不算難用,學起來也不難。Flutter的實現參考了不少RN的思路。更多關於Flutter可參閱官方網站

前幾天Google釋出了Dart2了,並且聽說Google內部部分應用早已經轉用Flutter實現。我這琢磨著Flutter是不是得開始把玩一下了。於是開始一頓瞎操作。過程中在官網發現了這篇結合Android對比Flutter的文件,對之前做Android的同學挺有幫助的,於是決定譯之並重新整理為一個系列,也算是一個小結。這系列的文章更適合對Dart和Flutter已經有所瞭解的Android Developers。

華麗分割線後,這系列文章正式開始


View在Flutter中等價於什麼

  • in Android
    1. View是螢幕上可見的基礎元素,我們螢幕上的Button,Toolbars等等,每個東西都是一個View。
    2. 系統可以修改整個View的層級結構中的任意一個View。
    3. 一個View被畫完之後它不會重繪,除非invalidate方法被呼叫。
  • in Flutter
    1. View等價於Widget。這是Flutter中的一個概念。
    2. Widget是不可修改的,以至於Widget變得非常的輕量。
    3. Widget只維持一幀,每一幀Flutter框架都會重新建立一個由Widget例項組成的樹。

怎麼更新Widgets

  • in Android

    • 我們可以直接修改View的屬性來更新它們。
  • in Flutter

    • Widget是不可修改的,我們沒法直接去更新它們,取而代之我們可以用Widget的State來實現更新。

在Flutter中Widget分為兩個型別:

1.StatelessWidget 一個StatelessWidget沒有任何狀態資訊。當你正在描述的介面元素不依賴於任何除了自身物件內的配置外的其他東西時,StatelessWidgets就剛好派上用場。 比如在Android中我們將logo用一個ImageView來展示。這個logo在執行的過程中將不會再改變,因此放到Flutter中的話,我們將用一個StatelessWidget來實現它。

2.StatefulWidget 如果你想在執行的過程中動態的改變介面,比如在想網路請求了資料之後,或者應用與使用者發生了一系列互動之後。這個時候就必須要使用帶有狀態資訊的StatefulWidget,它可以告知Flutter框架Widget的狀態已經更新進而促使Flutter框架去更新Widget。

Note: StatelessWidget和StatefulWidget的核心邏輯是相同的,就是他們在每一幀都被rebuild,不同的是StatefulWidget有一個State物件來儲存狀態資訊,然後在幀與幀之間它可以通過這個State物件來恢復之前儲存的狀態資訊。

如果你還比較疑惑,那麼可以簡單記下這個規則:如果使用者會與一個Widget互動,那麼這個Widget就用StatefulWidget。如果一個Widget響應了一個互動事件,但是隻要包含它的Parent Widget沒有響應這個互動事件的話,它的Parent Widget依然是StatelessWidget。

接下來我們看下怎樣使用StatelessWidget。一個最常見的StatelessWidget就是Text Widget。如果你去看Text Widget的實現的話你會發現他是StatelessWidget的一個子類。

new Text(
  'I like Flutter!',
  style: new TextStyle(fontWeight: FontWeight.bold),
);
複製程式碼

就像你看到的一樣,Text Widget沒有與它關聯的State資訊,它只是簡單的渲染通過建構函式傳遞給它的資訊。 但是如果我們想使**"I like Flutter!"**動態的改變,比如通過點選一個FloatingActionButton,該怎麼辦呢? 其實很簡單,我們可以通過將Text Widget包裹在一個StatefulWidget裡面來實現,當FloatingActionButton點選的時候更新StatefulWidget中的狀態資訊。 程式碼如下:

import 'package:flutter/material.dart';

void main() {
  runApp(new SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Sample App',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => new _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  // Default placeholder text
  String textToShow = "I Like Flutter";

  void _updateText() {
    setState(() {
      // update the text
      textToShow = "Flutter is Awesome!";
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("Sample App"),
      ),
      body: new Center(child: new Text(textToShow)),
      floatingActionButton: new FloatingActionButton(
        onPressed: _updateText,
        tooltip: 'Update Text',
        child: new Icon(Icons.update),
      ),
    );
  }
}
複製程式碼

上面的程式碼中可以看到我們將展示**"I Like Flutter"**的Text Widget在了一個繼承於State的_SampleAppPageState類中渲染。我們自定義了一個繼承於StatefulWidget的類SampleAppPage,它是一個StatefulWidget,與它關聯的State就是_SampleAppPageState。當FloatingActionButton被點選時會回撥_updateText方法,_updateText方法通過呼叫State類的setState方法來修改Text Widget的內容。setState由於修改了狀態資訊會觸發Flutter框架對Widget的更新。

小結: 在Flutter中Widgets Tree是不可變的,並且每一幀Widget都會rebuild。無法直接更新Widget。所以需要使用StatefulWidget來記錄State,記錄的State可以在幀與幀之間共享(在下一幀時恢復上一幀儲存的State資訊)。通過改變State來觸發Flutter更新Widget。

怎樣對Widgets佈局,xml佈局檔案在哪裡

  • in Android

    • 我們一般通過xml來寫佈局。
  • in Flutter

    • 我們通過Widget Tree來寫我們的佈局。

這有一個例子描述了怎樣在螢幕上展示一個Widget,並且給他新增一些padding。

override
Widget build(BuildContext context) {
  return new Scaffold(
    appBar: new AppBar(
      title: new Text("Sample App"),
    ),
    body: new Center(
      child: new MaterialButton(
        onPressed: () {},
        child: new Text('Hello'),
        padding: new EdgeInsets.only(left: 10.0, right: 10.0),
      ),
    ),
  );
}
複製程式碼

這裡看到其實在Flutter中是沒有xml佈局檔案的存在了,取而代之的是直接在override的build方法中去佈局,這其實有點類似RN,這裡的build方法就類似RN中的render方法,只不過RN通過JSX使得render方法中通過xml語法來完成佈局,而Flutter則是完全通過Dart語法來完成佈局。可讀性上我個人還是更喜歡Flutter,xml與js混寫還是覺得有點彆扭。 這裡列出了Flutter提供的所有的佈局。

小結: 在Flutter中不存在xml的佈局形式,Widget的佈局在build方法中直接構建。

怎麼從佈局中新增或者刪除一個元件

  • in Android

    • 我們可以呼叫addChild或者removeChild方法去動態的新增或者刪除一個ViewGroup中的View。
  • in Flutter

    • 因為widget是不可變的所以不能直接的addChild或者removeChild。但是可以傳遞一個返回Widget的方法給它的Parent,然後通過一個boolean值在該方法中控制要返回的Widget。

下面的程式碼展示瞭如何通過點選FloatingActionButton來觸發在兩個Widget之間切換:

import 'package:flutter/material.dart';

void main() {
  runApp(new SampleApp());
}

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Sample App',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => new _SampleAppPageState();
}

class _SampleAppPageState extends State<SampleAppPage> {
  // Default value for toggle
  bool toggle = true;
  void _toggle() {
    setState(() {
      toggle = !toggle;
    });
  }

  _getToggleChild() {
    if (toggle) {
      return new Text('Toggle One');
    } else {
      return new MaterialButton(onPressed: () {}, child: new Text('Toggle Two'));
    }
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("Sample App"),
      ),
      body: new Center(
        child: _getToggleChild(),
      ),
      floatingActionButton: new FloatingActionButton(
        onPressed: _toggle,
        tooltip: 'Update Text',
        child: new Icon(Icons.update),
      ),
    );
  }
}
複製程式碼

程式碼也比較簡單,關鍵在於_SampleAppPageState的build方法中Center的構造方法中的child引數傳的是一個_getToggleChild方法。該方法通過一個toggle變數來決定返回給Center的是一個怎樣的Widget。而toggle的賦值同樣是由點選FloatingActionButton後呼叫setState來改變的。也就是說將toggle作為SampleAppPage的狀態儲存下來,在展示的時候由toggle的值來動態決定要展示的是什麼Widget。

小結: 在Flutter中不能直接動態的去新增或者刪除一個Widget到Widgets Tree中,因為Flutter中的Widget是不可變的。但我們可以依賴StatefulWidget根據State的不同來靈活的構建不同的Widget。

怎樣對一個Widget做動畫

  • in Android
    • 我們可以通過通過xml檔案或者呼叫View.animate()方法建立一個動畫。
  • in Flutter
    • 我們將需要做動畫的Widget包裹到一個Transition中來實現。

像Android一樣,在Flutter中我們也有AnimationController和Interpolator,Interpolator通過繼承Animation類實現,比如下面例子中用到的CurvedAnimation。我們傳遞AnimationController和Animation到一個Widget中,然後通過AnimationController來啟動動畫。 下面的例子展示了使用FadeTransition來實現當按下按鈕時將展示Logo的FlutterLogo Widget淡出的效果:

import 'package:flutter/material.dart';

void main() {
  runApp(new FadeAppTest());
}

class FadeAppTest extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Fade Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new MyFadeTest(title: 'Fade Demo'),
    );
  }
}

class MyFadeTest extends StatefulWidget {
  MyFadeTest({Key key, this.title}) : super(key: key);
  final String title;
  @override
  _MyFadeTest createState() => new _MyFadeTest();
}

class _MyFadeTest extends State<MyFadeTest> with TickerProviderStateMixin {
  AnimationController controller;
  CurvedAnimation curve;

  @override
  void initState() {
    controller = new AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
    curve = new CurvedAnimation(parent: controller, curve: Curves.easeIn);
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Center(
          child: new Container(
              child: new FadeTransition(
                  opacity: curve,
                  child: new FlutterLogo(
                    size: 100.0,
                  )))),
      floatingActionButton: new FloatingActionButton(
        tooltip: 'Fade',
        child: new Icon(Icons.brush),
        onPressed: () {
          controller.forward();
        },
      ),
    );
  }
}
複製程式碼

整個Widget Tree還是跟之前類似,MaterialApp的建構函式中home引數傳入的依然是我們自定義的一個繼承自StatefulWidget的MyFadeTest,_MyFadeTest是其對應的State,其中定義了AnimationController和CurvedAnimation,AnimationController用於控制動畫,CurvedAnimation是一個插值器實現。接著在build方法中通過將我們需要動畫的FlutterLogo Widget包裹在一個FadeTransition中來讓FlutterLogo Widget產生動畫,最後在按下FloatingActionButton的回撥中使用AnimationController.forward()方法來觸發動畫。 這裡或者那裡檢視更多關於動畫的具體細節。

小結: 在Flutter中也有AnimationController和插值器,通過AnimationController來控制動畫的播放,插值器改變動畫播放的加速度。 使用時先構造AnimationController,然後將構造好的AnimationController作為引數構造插值器,最後將構造好的插值器作為引數構造Transition。之後就可以通過AnimationController來控制Transition中包含的Widget的動畫執行。

怎樣使用Canvas去畫內容

  • in Android
    • 我們可以用Canvas去畫一些自定義的圖形在螢幕上。
  • in Flutter
    • CustomPaint和CustomPainter這兩個類可以幫助我們在Canvas上作畫。

下面的程式碼實現一個可自由簽名的Widget:

import 'package:flutter/material.dart';
class SignaturePainter extends CustomPainter {
  SignaturePainter(this.points);
  final List<Offset> points;
  void paint(Canvas canvas, Size size) {
    Paint paint = new Paint()
      ..color = Colors.black
      ..strokeCap = StrokeCap.round
      ..strokeWidth = 5.0;
    for (int i = 0; i < points.length - 1; i++) {
      if (points[i] != null && points[i + 1] != null)
        canvas.drawLine(points[i], points[i + 1], paint);
    }
  }
  bool shouldRepaint(SignaturePainter other) => other.points != points;
}
class Signature extends StatefulWidget {
  SignatureState createState() => new SignatureState();
}
class SignatureState extends State<Signature> {
  List<Offset> _points = <Offset>[];
  Widget build(BuildContext context) {
    return new GestureDetector(
      onPanUpdate: (DragUpdateDetails details) {
        setState(() {
          RenderBox referenceBox = context.findRenderObject();
          Offset localPosition =
          referenceBox.globalToLocal(details.globalPosition);
          _points = new List.from(_points)..add(localPosition);
        });
      },
      onPanEnd: (DragEndDetails details) => _points.add(null),
      child: new CustomPaint(painter: new SignaturePainter(_points)),
    );
  }
}
class DemoApp extends StatelessWidget {
  Widget build(BuildContext context) => new Scaffold(body: new Signature());
}
void main() => runApp(new MaterialApp(home: new DemoApp()));
複製程式碼

可以看到CustomPaint和CustomPainter搭配使用來實現了向Canvas上繪製自定義內容的目的。首先自定義SignaturePainter繼承於CustomPainter,並重寫paint方法,在本例中paint方法首先採用鏈式寫法構造一個Paint例項paint,接著遍歷points列表的內容來畫線。points列表是構造SignaturePainter時傳入的,裡面儲存了觸控螢幕的事件點的資訊。其他部分其實與之前的結構都差不多。關鍵在SignatureState的build方法中返回的是一個GestureDetector,這是一個能夠幫助我們捕獲手勢資訊的Widget,在它的建構函式中child引數傳入的是一個CustomPaint,CustomPaint的建構函式又傳入了一個我們自定義的SignaturePainter,並且將手勢捕獲事件時捕獲到的_points列表在這個時候傳遞給SignaturePainter。於是CustomPaint就和CustomPainter產生化學反應,相互配合完成在Canvas上作畫的效果。

效果如下:

[譯]Flutter for Android Developers - Views

小結: 在Flutter中實現在Canvas上作畫需要CustomPaint和CustomPainter相互配合,首先繼承CustomPainter自定義一個Painter並重寫paint方法實現繪製邏輯。然後將自定義的CustomPainter傳遞給CustomPaint的構造方法,CustomPaint作為Widget Tree中的一個Widget使用自定義的CustomPainter完成繪製。

怎樣構建自定義Widget

  • in Android
    • 自定義View一般通過繼承View或者已經存在的其他元件並重寫一些關鍵方法來實現。
  • in Flutter
    • 自定義一個Widget不是通過繼承而是通過組合其他widgets。

讓我們來看一個栗子:

class CustomButton extends StatelessWidget {
  final String label;
  CustomButton(this.label);

  @override
  Widget build(BuildContext context) {
    return new RaisedButton(onPressed: () {}, child: new Text(label));
  }
}
複製程式碼

程式碼很簡單,看到CustomButton一樣還是繼承於StatelessWidget。建構函式接受一個字串引數並儲存在內部成員變數label中。在build方法中返回的是一個RaisedButton(一個Flutter提供的Widget),巧妙的地方是在RaisedButton的建構函式中傳入了一個Text(一個Flutter提供的Widget)作為其child引數。這個Text Widget顯示的就是CustomButton的label成員中的內容。

在使用CustomButton的時候可以像使用其他Widget一樣直接使用:

override
  Widget build(BuildContext context) {
    return new Center(
      child: new CustomButton("Hello"),
    );
  }
}
複製程式碼

小結: 在Flutter中自定義Widget是通過組合不同的Widget來實現的,自定義的Widget只繼承於StatelessWidget或者StatefulWidget,通過build方法中組合其他的Widget來實現自定義Widget。

英文原版傳送

相關文章