一個Android菜鳥入門Flutter 筆記(一)

1. Dart 基礎語法

1.1 hello world

先來看個hello world,入口依然是main方法.

printInteger(int a) {
  print('Hello world, this is $a.'); 
}

main() {
  var number = 2019; 
  printInteger(number); 
}

1.2 變量與類型

  • 定義變量使用var或者具體類型來定義
  • 未初始化的變量的值都是null
  • 所有東西都是對象,都是繼承自Object.包括數字、布爾值、函數和 null
  • 內置了一些基本數據類型num、bool、String、List 和 Map
  • int和double都繼承自num
  • 常見的運算符也是繼承自num
  • Dart 的 String 由 UTF-16 的字符串組成,可以使用$xx${xx}來把表達式放入字符串中
  • List和Map,可以顯式指定元素類型,也可以推斷
  • 常量定義使用final(運行時常量)或const(編譯期常量)
  • 儘量爲變量指定類型,這樣編輯器額編譯器能更好的理解你的意圖
var a = 1;
int b = 1;
num c = 3;

int x = 1;
int hex = 0xEEADBEEF;
double y = 1.1;
double exponents = 1.13e5;
int roundY = y.round();

//List
var arr1 = ["Tom", "Andy", "Jack"];
var arr3 = <String>["Tom", "Andy", "Jack"];
var arr2 = List.of([1, 2, 3]);
var arr4 = List<int>.of([1, 2, 3]);
arr2.add(499);
arr2.forEach((v) => print(v));

//Map
var map1 = {"name": "Tom", "sex": "male"};
var map2 = new Map();
//添加或修改
map2["name"] = "Tom";
map2["name"] = "Tom22";
map2["sex"] = "male";

print(map2);
map2.forEach((k, v) => print('k = $k v=$v'));

1.3 函數

  • 函數是一個對象,類型Function,可以定義爲變量
  • 函數可以只寫一句代碼,不要{}
  • 提供了可選命名參數和可選參數
  • 返回值類型可以省略
void main() {
  Function f = isZero;
  int x = 10;
  int y = 10;
  printInfo(x, f);
  printInfo(y, f);

  enable1Flags(bold: true);
}

bool isZero(int a) {
  return a == 0;
}

bool isNull(var a) => a == null;

void printInfo(int number, Function check) {
  print('$number is zero: ${check(number)}');
}

//可選命名參數   Flutter 中大量使用
void enable1Flags({bool bold, bool hidden}) => print("$bold $hidden");

//可選命名參數 加默認值
void enable2Flags({bool bold = true, bool hidden = false}) =>
    print('$bold $hidden');

//可忽略參數,也可以加默認值
void enable3Flags(bool bold, [bool hidden]) => print("$bold $hidden");

//返回值類型 省略
price() {
    double sum = 0.0;
    for (var i in booking) {
      sum += i.price;
    }
    return sum;
}

1.4 類

  • 沒有public、protected、private這些修飾符,可以在變量和方法前面加_,_的限制範圍並不是類級別的,而是庫訪問級別.
  • 對象調用方法時可以加?,加了之後如果對象是空,則跳過
  • 類A可以implements另一個類B,這時相當於implements B的方法和字段.
  • 當類A需要複用類B的方法時,可以使用混入(Mixin).class A with B{}
  • a ?? b a不爲null,返回a.否則返回b.
void main() {
  //類 都是繼承自Object
  //無修飾符關鍵字 變量與方法前面加"_"則表示private,不加則爲public
  //加"_"的限制範圍並不是類訪問級別的,而是庫訪問級別的
  Test test = Test();
  print(test.b);

  var p = Point(1, 2);
  p.printInfo();
  Point.factor = 10;
  Point.printZValue();
  //爲空時跳過執行
  Point?.printZValue();

  var p2 = Point2.test(1);
}

class Test {
  int _a = 1;
  int b = 2;
}

class Point {
  num x, y;
  static num factor = 0;

  //語法糖,等同於在函數體內:this.x = x;this.y = y;
  Point(this.x, this.y);

  void printInfo() => print('($x,$y)');

  static void printZValue() => print('factor=$factor');
}

class Point2 {
  num x, y, z;

  //z也得到了初始化
  Point2(this.x, this.y) : z = 0;

  //重定向構造函數
  Point2.test(num x) : this(x, 0);
}

class Point3 {
  num x = 0, y = 0;

  void printInfo() => print('($x,$y)');
}

class Vector extends Point3 {
  num z = 0;

  //覆寫了父類的方法
  @override
  void printInfo() => print('x = $x,y=$y');
}

class Coordinate implements Point3 {
  //成員變量需要重新聲明
  num x = 0, y = 0;

  //成員函數需要重新實現
  @override
  void printInfo() {}
}

//混入Mixin  可以視爲具有實現方法的接口
//非繼承的方法,使用其他類中的變量與方法
class Coordinate2 with Point3 {}

2. Flutter區別於其他方案的關鍵技術

  • Flutter 使用 Native 引擎渲染視圖
  • React Native 之類的框架,只是通過 JavaScript 虛擬機擴展調用系統組件,由 Android 和 iOS 系統進行組件的渲染;Flutter 則是自己完成了組件渲染的閉環。
  • UI線程使用Dart來構建視圖結構數據,這些數據會在GPU線程進行圖層合成,隨後交給Skia引擎加工成GPU數據,而這些數據會通過OpenGL最終提供給GPU渲染.
  • 使用Dart語言,同時支持JIT(動態編譯,需要用的時候編譯,開發的時候)和AOT(靜態編譯,先編譯好,正式包的時候)
  • J9Mu9S.png

3. Widget的設計思路和基本原理

  • 核心設計思想: 一切皆Widget
  • Widget是不可變的,當視圖渲染的配置信息發生變化時,Flutter會選擇重建Widget樹的方法進行數據更新.
  • 數據驅動UI構建
  • Widget本身不涉及實際渲染位圖,它是輕量級的數據結構,重建成本低.
  • Element是Widget的一個實例化對象,它承載了視圖構建的上下文數據,是連接結構化的配置信息到完成最終渲染的橋樑
  • Element同時持有Widget和RenderObject,最後負責渲染的是RenderObject
  • Flutter展示過程: 佈局,繪製,合成,渲染

4. State的選擇

  • Widget有StatelessWidget和StatefulWidget
  • StatefulWidget對應有交互,需要動態變化視覺效果的場景,StatelessWidget則用於處理靜態的,無狀態的視圖展示
  • Flutter的視圖開發是聲明式的,其核心設計思想就將視圖和數據分離
  • Widget生命週期內,State中的任何更改都將強制Widget重新構建
  • StatelessWidget,如Text,Container,Row,Column等.它們一旦創建成功就不再關心,也不相應任何數據變化進行重繪
  • 避免無謂的StatefulWidget使用,可以提高Flutter應用渲染性能.

5. 生命週期

5.1 Widget 視圖生命週期

  • 生命週期其實是State的
  • State生命週期分爲3個階段
    1. 創建(插入視圖樹)
    2. 更新(在視圖樹中存在)
    3. 銷燬(從視圖樹中移除)
  • 創建
    1. 構造方法
    2. initState
    3. didChangeDependencies
    4. build
  • 更新
    1. setState->build
    2. disUpdateWidget->build
    3. didChangeDependencies->build
  • 銷燬
    1. deactivate
    2. dispose
  • 構造方法: 接收父Widget傳遞的初始化UI配置數據
  • initState: State對象被插入視圖樹的時候被調用,在這裏做初始化工作
  • didChangeDependencies: 處理State對象依賴關係變化,initState()調用結束後會被調用
  • build: 構建視圖,在這裏根據父Widget傳遞過來的初始化配置數據,以及State狀態,創建一個Widget返回
  • setState: 當狀態數據發生變化時,調用這個方法,告訴Flutter,數據變了,根據更新後的數據重建UI
  • didChangeDependencies: State對象的依賴關係發生變化時(系統語言Locale或應用主題更改),系統會通知State調用此方法
  • didUpdateWidget: 當Widget的配置發生變化時,如父Widget觸發重建,熱重載時,會被調用.
  • deactivate: 組件的可見狀態發生變化,State會被暫時從視圖樹中移除. 頁面切換時,上一個頁面的State對象在視圖樹中的位置發生了變化,會先調用deactivate,再調用build.
  • dispose: 當State被永久地從視圖樹中移除,比如關閉頁面.到這裏時,組件就要銷燬了,這裏做最終的資源釋放,移除監聽,清理環境.
方法名 功能 調用時機 調用次數
構造方法 接收父Widget傳遞的初始化UI配置數據 創建State時 1
initState 與渲染相關的初始化工作 在State被插入視圖樹時 1
didChangeDependencies 處理State對象依賴關係變化 initState後及State對象依賴關係變化時 >=1
build 構建視圖 State準備好數據需要渲染時 >=1
setState 觸發視圖重建 需要刷新UI時 >=1
didUpdateWidget 處理Widget的配置變化 父Widget setState觸發子Widget重建時 >=1
deactivate 組件被移除 組件不可視 >=1
dispose 組件被銷燬 組件被永久移除 1

5.2 App(也是Widget) 生命週期

  • 利用WidgetsBindingObserver類
abstract class WidgetsBindingObserver {
  //頁面pop
  Future<bool> didPopRoute() => Future<bool>.value(false);
  //頁面push
  Future<bool> didPushRoute(String route) => Future<bool>.value(false);
  //系統窗口相關改變回調,如旋轉
  void didChangeMetrics() { }
  //文本縮放係數變化
  void didChangeTextScaleFactor() { }
  //系統亮度變化
  void didChangePlatformBrightness() { }
  //本地化語言變化
  void didChangeLocales(List<Locale> locale) { }
  //App生命週期變化
  void didChangeAppLifecycleState(AppLifecycleState state) { }
  //內存警告回調
  void didHaveMemoryPressure() { }
  //Accessibility相關特性回調
  void didChangeAccessibilityFeatures() {}
}
  • 在didChangeAppLifecycleState回調函數中,AppLifecycleState參數是枚舉類,它是Flutter對App生命週期狀態的封裝.
    • resumed 可見的,並能響應用戶輸入
    • inactive: 處在不活動狀態,無法處理用戶響應
    • paused: 不可見並不能響應用戶的輸入,但是在後臺繼續活動中
  • 在initState中註冊監聽器,在dispose中移除監聽器
class _MyHomePageState extends State<MyHomePage>  with WidgetsBindingObserver{
...
  @override
  @mustCallSuper
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);//註冊監聽器
  }
  @override
  @mustCallSuper
  void dispose(){
    super.dispose();
    WidgetsBinding.instance.removeObserver(this);//移除監聽器
  }
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) async {
    print("$state");
    if (state == AppLifecycleState.resumed) {
      //do sth
    }
  }
}
  • 後臺(paused)切入前臺: AppLifecycleState.inactive->AppLifecycleState.resumed
  • 前臺(resumed)退回到後臺: AppLifecycleState.inactive->AppLifecycleState.paused
  • WidgetsBingding提供了單次Frame繪製回調,以及實時Frame繪製回調兩種機制.
  • 單次

WidgetsBinding.instance.addPostFrameCallback((_){
    print("單次Frame繪製回調");//只回調一次
  });
  • 實時繪製
WidgetsBinding.instance.addPersistentFrameCallback((_){
  print("實時Frame繪製回調");//每幀都回調
});

6. 文本

  • Text,單一樣式. 構造參數分爲2類
    • 控制整體文本佈局的參數: 對齊方式textAlign,文本排版方向textDirection,文本顯示最大行數 maxLines、文本截斷規則 overflow 等
    • 控制文本展示樣式的參數: 統一封裝到style參數中,字體名稱fontFamily,字體大小fontSize,文本顏色color,文本陰影shadows等
Text(
  '文本是視圖系統中的常見控件,用來顯示一段特定樣式的字符串,就比如Android裏的TextView,或是iOS中的UILabel。',
  textAlign: TextAlign.center,//居中顯示
  style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20, color: Colors.red),//20號紅色粗體展示
);
  • TextSpan,可展示混合樣式.(類似SpannableString)

TextStyle blackStyle = TextStyle(fontWeight: FontWeight.normal, fontSize: 20, color: Colors.black); //黑色樣式

TextStyle redStyle = TextStyle(fontWeight: FontWeight.bold, fontSize: 20, color: Colors.red); //紅色樣式

Text.rich(
    TextSpan(
        children: <TextSpan>[
          TextSpan(text:'文本是視圖系統中常見的控件,它用來顯示一段特定樣式的字符串,類似', style: redStyle), //第1個片段,紅色樣式 
          TextSpan(text:'Android', style: blackStyle), //第1個片段,黑色樣式 
          TextSpan(text:'中的', style:redStyle), //第1個片段,紅色樣式 
          TextSpan(text:'TextView', style: blackStyle) //第1個片段,黑色樣式 
        ]),
  textAlign: TextAlign.center,
);

7. 圖片

  • Image
    • 加載本地資源圖片,如 Image.asset(‘images/logo.png’);
    • 加載本地(File 文件)圖片,如 Image.file(new File(’/storage/xxx/xxx/test.jpg’));
    • 加載網絡圖片,如 Image.network(‘http://xxx/xxx/test.gif’)
  • 填充模式fit,拉伸 centerSlice,重複模式repeat
  • Image通過內部ImageProvider根據緩存狀態,觸發異步加載流程,通知_imageState(Image這種控件肯定不是靜態的撒,得需要一個State)刷新UI.
  • FadeInImage,可以提供佔位圖,加載動畫等.

FadeInImage.assetNetwork(
  placeholder: 'assets/loading.gif', //gif佔位
  image: 'https://xxx/xxx/xxx.jpg',
  fit: BoxFit.cover, //圖片拉伸模式
  width: 200,
  height: 200,
)
  • 圖片默認緩存到內存,LRU(最近最少使用),如需緩存到本地則需要使用第三方的CachedNetworkImage(還提供了錯誤展示圖片)控件

8. 按鈕

  • FloatingActionButton 圓形按鈕
  • RaisedButton,凸起的按鈕,和Android默認的Button長得一樣醜
  • FlatButton,扁平的按鈕,默認透明背景,被點擊後呈現灰色背景
FloatingActionButton(onPressed: () => print('FloatingActionButton pressed'),child: Text('Btn'),);
FlatButton(onPressed: () => print('FlatButton pressed'),child: Text('Btn'),);
RaisedButton(onPressed: () => print('RaisedButton pressed'),child: Text('Btn'),);
  • onPressed參數用於設置回調,如果參數爲空,則按鈕會被禁用
  • child參數用於控制控件長什麼樣子
  • 其他豐富api
FlatButton(
    color: Colors.yellow, //設置背景色爲黃色
    shape:BeveledRectangleBorder(borderRadius: BorderRadius.circular(20.0)), //設置斜角矩形邊框
    colorBrightness: Brightness.light, //確保文字按鈕爲深色
    onPressed: () => print('FlatButton pressed'), 
    child: Row(children: <Widget>[Icon(Icons.add), Text("Add")],)
);
  • Button都是由RawMaterialButton承載視覺,Image都是RawImage,Text是RichText。它們都繼承自RenderObjectWidget,而RenderObjectWidget的父類就是Widget。

9. ListView

9.1 ListView

  • 同時支持垂直方向和水平方向滾動
  • 創建子視圖方式
構造函數名 特點 適用場景 適用頻次
ListView 一次性創建好全部子Widget 適用於展示少量連續子Widget的場景
ListView.builder 提供子Widget創建方法,僅在需要展示的時候才創建 適用於子Widget較多,且視覺效果呈現某種規律性的場景
ListView.separated 與ListView.builder類似,並提供了自定義分割線的功能 與ListView.builder場景類似
  • 第一種 ListView 直接構建
ListView(
  children: <Widget>[
    //設置ListTile組件的標題與圖標 
    ListTile(leading: Icon(Icons.map),  title: Text('Map')),
    ListTile(leading: Icon(Icons.mail), title: Text('Mail')),
    ListTile(leading: Icon(Icons.message), title: Text('Message')),
  ]);
  • 第二種 ListView.builder.itemExtent 並不是一個必填參數。但,對於定高的列表項元素,我強烈建議你提前設置好這個參數的值。
ListView.builder(
    //itemCount,表示列表項的數量,如果爲空,則表示 ListView 爲無限列表
    itemCount: 100, //元素個數
    itemExtent: 50.0, //列表項高度
    itemBuilder: (BuildContext context, int index) => ListTile(title: Text("title $index"), subtitle: Text("body $index"))
);
  • 第三種 ListView.separated
//使用ListView.separated設置分割線
ListView.separated(
    itemCount: 100,
    separatorBuilder: (BuildContext context, int index) => index %2 ==0? Divider(color: Colors.green) : Divider(color: Colors.red),//index爲偶數,創建綠色分割線;index爲奇數,則創建紅色分割線
    itemBuilder: (BuildContext context, int index) => ListTile(title: Text("title $index"), subtitle: Text("body $index"))//創建子Widget
)

9.2 CustomScrollView

  • CustomScrollView是用來處理多個需要自定義滑動效果的Widget.在CustomScrollView中,這些彼此獨立的,可滑動的Widget被統稱爲Sliver.
  • 比如ListView 的 Sliver 實現爲 SliverList,AppBar 的 Sliver 實現爲 SliverAppBar
  • 這些Sliver不再維護各自的滾動狀態,交由CustomScrollView統一管理,最終實現滑動效果的一致性

CustomScrollView(
  slivers: <Widget>[
    SliverAppBar(//SliverAppBar作爲頭圖控件
      title: Text('CustomScrollView Demo'),//標題
      floating: true,//設置懸浮樣式
      flexibleSpace: Image.network("https://xx.jpg",fit:BoxFit.cover),//設置懸浮頭圖背景
      expandedHeight: 300,//頭圖控件高度
    ),
    SliverList(//SliverList作爲列表控件
      delegate: SliverChildBuilderDelegate(
            (context, index) => ListTile(title: Text('Item #$index')),//列表項創建方法
        childCount: 100,//列表元素個數
      ),
    ),
  ]);

9.3 ScrollController

  • ScrollController用於對ListView進行滾動信息的監聽,以及相應的滾動控制.
class MyControllerAppState extends State<MyControllerApp> {
  //ListView控制器
  ScrollController _controller;
  //標識目前是否需要啓用top按鈕
  bool isToTop = false;

  @override
  void initState() {
    _controller = ScrollController();
    _controller.addListener(() {
      //ListView向下滾動1000 則啓用top按鈕
      if (_controller.offset > 1000) {
        setState(() {
          isToTop = true;
        });
      } else if (_controller.offset < 300) {
        //向下滾動不足300,則禁用按鈕
        setState(() {
          isToTop = false;
        });
      }
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: ListView.builder(
            //將控制器傳入
            controller: _controller,
            itemCount: 100,
            itemExtent: 100,
            itemBuilder: (context, index) =>
                ListTile(title: Text('index $index'))),
        floatingActionButton: RaisedButton(
          //如果isToTop是true則滑動到頂部,否則禁用按鈕
          onPressed: isToTop
              ? () {
                  //滑動到頂部
                  _controller.animateTo(0.0,
                      duration: Duration(microseconds: 200),
                      curve: Curves.ease);
                }
              : null,
          child: Text('top'),
        ),
      ),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

}

9.4 NotificationListener

  • NotificationListener是一個Widget,需要將ListView添加到NotificationListener中
class MyListenerApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: NotificationListener<ScrollNotification>(  
          //添加NotificationListener作爲父容器
          //註冊通知回調
          onNotification: (scrollNotification) {
            //開始滑動
            if (scrollNotification is ScrollStartNotification) {
              //scrollNotification.metrics.pixels 滑動的位置
              print('scroll start ${scrollNotification.metrics.pixels}');
            } else if (scrollNotification is ScrollUpdateNotification) {
              //滑動中
              print('scroll update');
            } else if (scrollNotification is ScrollEndNotification) {
              //滑動結束
              print('scroll end');
            }
            return null;
          },
          child: ListView.builder(
              itemCount: 100,
              itemExtent: 70,
              itemBuilder: (context, index) => ListTile(
                    title: Text('index $index'),
                  )),
        ),
      ),
    );
  }
}

10. 佈局容器

10.1 Container,Padding,Center

  • Container內部提供了間距,背景樣式,圓角邊框等基礎屬性,可以控制子Widget的擺放方式(居中,左,右)
  • Padding 設置間距,將Widget放裏面
  • Center 設置居中,將Widget放裏面
getContainer() {
return Container(
  child: Center(
    child: Text('Container(容器)在UI框架中是一個很常見的概念,Flutter也不例外。'),
  ),
  //內邊距
  padding: EdgeInsets.all(18.0),
  //外邊距
  margin: EdgeInsets.all(44.0),
  width: 180.0,
  height: 240,
  //子Widget居中對齊
  /* alignment: Alignment.center,*/
  //Container樣式
  decoration: BoxDecoration(
    //背景色
    color: Colors.red,
    //圓角邊框
    borderRadius: BorderRadius.circular(10.0),
  ),
);
}

getPadding() {
//只需要設置邊距 可以使用Padding
return Padding(
  padding: EdgeInsets.all(44.0),
  child: Text('我是Padding'),
);
}

getCenter() {
//直接居中
return Center(
  child: Text('center text'),
);
}

10.2 Row,Column,Expanded

  • Row是水平佈局
  • Column是垂直佈局
  • Expanded表示將剩餘的空間,如何分配
  • Row 與 Column 自身的大小由父widget的大小、子widget的大小、以及mainSize設置共同決定(mainAxisSize和crossAxisSize)
    • 主軸(縱軸)值爲max:主軸(縱軸)大小等於屏幕主軸(縱軸)方向大小或者父widget主軸(縱軸)方向大小
    • 主軸(縱軸)值爲min: 所有子widget組合在一起的主軸(縱軸)大小

//Row的用法示範
Row(
  children: <Widget>[
    Container(color: Colors.yellow, width: 60, height: 80,),
    Container(color: Colors.red, width: 100, height: 180,),
    Container(color: Colors.black, width: 60, height: 80,),
    Container(color: Colors.green, width: 60, height: 80,),
  ],
);

//Column的用法示範
Column(
  children: <Widget>[
    Container(color: Colors.yellow, width: 60, height: 80,),
    Container(color: Colors.red, width: 100, height: 180,),
    Container(color: Colors.black, width: 60, height: 80,),
    Container(color: Colors.green, width: 60, height: 80,),
  ],
);


//第一個和最後一個平分
Row(
  children: <Widget>[
    Expanded(flex: 1, child: Container(color: Colors.yellow, height: 60)), //設置了flex=1,因此寬度由Expanded來分配
    Container(color: Colors.red, width: 100, height: 180,),
    Container(color: Colors.black, width: 60, height: 80,),
    Expanded(flex: 1, child: Container(color: Colors.green,height: 60),)/設置了flex=1,因此寬度由Expanded來分配
  ],
);

對齊方式

  • 根據主軸與縱軸,設置子Widget在這兩個方向上的對齊規則mainAxisAlignment與crossAxisAlignment.比如主軸方向start表示靠左對齊,center表示橫向居中對齊,end表示靠右對齊,spaceEvenly表示按固定間距對齊;而縱軸方向start則表示靠上對齊,center表示縱向居中對齊,end表示靠下對齊.

控制大小

  • 如果想讓容器與子Widget在主軸上完全匹配,需要通過設置Row的mainAxisSize參數爲MainAxisSize.min,由所有子Widget來決定主軸方向的容器長度,即主軸方向的長度儘可能小.類似wrap_content. mainAxisSize: MainAxisSize.min, //讓容器寬度與所有子Widget的寬度一致

10.3 Stack,Positioned

  • Stack,類似FrameLayout.
  • Stack提供了層疊佈局的容器,而Positioned則提供了設置子Widget位置的能力.

Stack(
  children: <Widget>[
    Container(color: Colors.yellow, width: 300, height: 300),//黃色容器
    Positioned(
      left: 18.0,
      top: 18.0,
      child: Container(color: Colors.green, width: 50, height: 50),//疊加在黃色容器之上的綠色控件
    ),
    Positioned(
      left: 18.0,
      top:70.0,
      child: Text("Stack提供了層疊佈局的容器"),//疊加在黃色容器之上的文本
    )
  ],
)
  • Positioned只能在Stack中使用.

11. 自定義控件

11.1 組合控件

  • 將多個控件組合在一起

11.2 自定義控件

  • CustomPaint是用來承接自繪控件的容器,並不負責真正的繪製.
  • 畫布是canvas,畫筆是Paint.
  • 畫成什麼樣子由CustomPainter來控制,將CustomPainter設置給容器CustomPaint的painter屬性,我們就完成了一個自繪組件的封裝
  • Paint,其實和Android中的差不多,可以配置它的各種屬性,比如顏色、樣式、粗細等;而畫布 Canvas,則提供了各種常見的繪製方法,比如畫線 drawLine、畫矩形 drawRect、畫點 DrawPoint、畫路徑 drawPath、畫圓 drawCircle、畫圓弧 drawArc 等。
class WheelPainter extends CustomPainter {
  Paint getColoredPaint(Color color) {
    Paint paint = Paint();
    paint.color = color;
    return paint;
  }

  @override
  void paint(Canvas canvas, Size size) {
    //半徑
    double wheelSize = min(size.width, size.height) / 2;
    //分成6份
    double nbElem = 6;
    //角度
    double radius = (2 * pi) / nbElem;
    //包裹餅圖的矩形框  center:相對於原點的偏移量
    Rect boundingRect = Rect.fromCircle(
        center: Offset(wheelSize, wheelSize), radius: wheelSize);

    //每次畫1/6圓
    canvas.drawArc(
        boundingRect, 0, radius, true, getColoredPaint(Colors.orange));
    canvas.drawArc(
        boundingRect, radius, radius, true, getColoredPaint(Colors.green));
    canvas.drawArc(
        boundingRect, radius * 2, radius, true, getColoredPaint(Colors.red));
    canvas.drawArc(
        boundingRect, radius * 3, radius, true, getColoredPaint(Colors.blue));
    canvas.drawArc(
        boundingRect, radius * 4, radius, true, getColoredPaint(Colors.pink));
    canvas.drawArc(boundingRect, radius * 5, radius, true,
        getColoredPaint(Colors.deepOrange));
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    //判斷是否需要重繪,簡單做下比較
    return oldDelegate != this;
  }
}

class Cake extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //CustomPaint是用來承載自定義View的容器,需要自定義一個畫筆,得繼承自CustomPainter
    return CustomPaint(
      size: Size(200, 200),
      painter: WheelPainter(),
    );
  }
}

12. 主題定製

  • 視覺效果是易變的,我們將這些變化的部分抽離出來,把提供不同視覺效果的資源和配置按照主題進行歸類,整合到一個統一的中間層去管理,這樣我們就能實現主題的管理和切換.
  • Flutter中由ThemeData來統一管理主題的配置信息
  • ThemeData中涵蓋了Material Design規範的可自定義部分樣式,比如應用明暗模式 brightness、應用主色調 primaryColor、應用次級色調 accentColor、文本字體 fontFamily、輸入框光標顏色 cursorColor 等。
  • 全局統一的視覺風格:

MaterialApp(
  title: 'Flutter Demo',//標題
  theme: ThemeData(//設置主題
      brightness: Brightness.dark,//設置明暗模式爲暗色
      accentColor: Colors.black,//(按鈕)Widget前景色爲黑色
      primaryColor: Colors.cyan,//主色調爲青色
      iconTheme:IconThemeData(color: Colors.yellow),//設置icon主題色爲黃色
      textTheme: TextTheme(body1: TextStyle(color: Colors.red))//設置文本顏色爲紅色
  ),
  home: MyHomePage(title: 'Flutter Demo Home Page'),
);
  • 局部主題: 需要使用Theme來對App的主題進行局部覆蓋,Theme是一個單子Widget容器,將控件放裏面就可以控制主題了.
  • 局部新建主題: 如果不想繼承任何App全局的顏色或字體樣式,可以直接新建一個ThemeData實例,依次設置對應的樣式.
// 新建主題
Theme(
    data: ThemeData(iconTheme: IconThemeData(color: Colors.red)),
    child: Icon(Icons.favorite)
);
  • 繼承主題: 如果不想在局部重寫所有的樣式,則可以繼承App的主題,使用copyWith方法,只更新部分樣式
// 繼承主題
Theme(
    data: Theme.of(context).copyWith(iconTheme: IconThemeData(color: Colors.green)),
    child: Icon(Icons.feedback)
);
  • 主題另一個用途是樣式複用.
Container(
    color: Theme.of(context).primaryColor,//容器背景色複用應用主題色
    child: Text(
      'Text with a background color',
      style: Theme.of(context).textTheme.title,//Text組件文本樣式複用應用文本樣式
    ));

13. 依賴管理

  • 可以把資源放任意目錄,只需要使用根目錄下的pubspec.yaml文件,對這些資源的所在位置進行顯示聲明就行.

13.1 圖片

flutter:
  assets:
    - assets/background.jpg   #挨個指定資源路徑
    - assets/loading.gif  #挨個指定資源路徑
    - assets/result.json  #挨個指定資源路徑
    - assets/icons/    #子目錄批量指定
    - assets/ #根目錄也是可以批量指定的
  • Flutter遵循了基於像素密度的管理方式,如1.0x,2.0x,3.0x.Flutter會根據當前設備分辨率加載最接近設備像素比例的圖片資源
  • 想讓Flutter適配不同的分辨率,只需要將其他分辨率的圖片放到對應的分辨率子目錄中.
目錄如下

assets
├── background.jpg    //1.0x圖
├── 2.0x
│   └── background.jpg  //2.0x圖
└── 3.0x

在pubspec.yaml文件聲明:

flutter:
  assets:
    - assets/background.jpg   #1.0x圖資源

13.2 字體


fonts:
  - family: RobotoCondensed  #字體名字
    fonts:
      - asset: assets/fonts/RobotoCondensed-Regular.ttf #普通字體
      - asset: assets/fonts/RobotoCondensed-Italic.ttf 
        style: italic  #斜體
      - asset: assets/fonts/RobotoCondensed-Bold.ttf 
        weight: 700  #粗體

13.3 三方庫 三方組件庫

  • Dart提供包管理工具: Pub,管理代碼和資源
  • 對於包,通常是指定版本區間,而很少直接指定特定版本.
  • 多人協作時,建議將Dart和Flutter的SDK環境寫死,統一團隊的開發環境.避免因爲跨SDK版本出現的API差異而導致工程問題.
dependencies:
  //1. #路徑依賴
  package1:
    path: ../package1/  
  //2. github
  date_format:
    git:
      url: https://github.com/xxx/package2.git #git依賴
  //3. pub上面的
  date_format: 1.0.6

14. 手勢識別

  • 底層原始指針事件: 用戶的觸摸數據,如手指接觸屏幕 PointerDownEvent、手指在屏幕上移動 PointerMoveEvent、手指擡起 PointerUpEvent,以及觸摸取消 PointerCancelEvent.
Listener(
  child: Container(
    color: Colors.red,//背景色紅色
    width: 300,
    height: 300,
  ),
  onPointerDown: (event) => print("down $event"),//手勢按下回調
  onPointerMove:  (event) => print("move $event"),//手勢移動回調
  onPointerUp:  (event) => print("up $event"),//手勢擡起回調
);
  • 冒泡分發機制: 將觸摸事件交給最內層的組件去響應,事件會從這個最內層的組件開始,沿着組件樹向根節點向上冒泡分發.
  • 封裝了底層指針事件手勢語義的Gesture,平常一般使用GestureDetector.如點擊 onTap、雙擊 onDoubleTap、長按 onLongPress、拖拽 onPanUpdate、縮放 onScaleUpdate 等。

//紅色container座標
double _top = 0.0;
double _left = 0.0;
Stack(//使用Stack組件去疊加視圖,便於直接控制視圖座標
  children: <Widget>[
    Positioned(
      top: _top,
      left: _left,
      child: GestureDetector(//手勢識別
        child: Container(color: Colors.red,width: 50,height: 50),//紅色子視圖
        onTap: ()=>print("Tap"),//點擊回調
        onDoubleTap: ()=>print("Double Tap"),//雙擊回調
        onLongPress: ()=>print("Long Press"),//長按回調
        onPanUpdate: (e) {//拖動回調
          setState(() {
            //更新位置
            _left += e.delta.dx;
            _top += e.delta.dy;
          });
        },
      ),
    )
  ],
);
  • 事件處理機制: Flutter會使用手勢競技場來進行各個手勢的PK,以保證最後只有一個手勢能夠響應用戶行爲.
  • 手勢衝突只是手勢的語義化識別過程,對於底層指針事件是不會衝突的.
  • 父子都有點擊事件的情況 因爲子視圖在父視圖的上面,所以如果點擊區域在子視圖區域,子視圖響應事件.

15. 跨組件共享數據

視圖層級比較深的UI樣式,直接通過屬性傳值會導致很多中間層增加冗餘屬性.

  • 代碼太多,見demo https://github.com/xfhy/FlutterBasic/tree/master/lib/data

15.1 InheritedWidget

  • 共享父Widget的屬性

15.2 Notification

  • 從下往上的數據傳遞,在父Widget中監聽來自子Widget的事件

15.3 EventBus

  • EventBus 不依賴Widget樹 這是事件總線,666
  • 遵循發佈訂閱 模式

15.4 對比

方式 數據流動方式 使用場景
屬性傳值 父到子 簡單數據傳遞
InheritedWidget 父到子 跨層數據傳遞
Notification 子到父 狀態通知
EventBus 發佈訂閱 消息批量同步

16. 路由管理

  • Route是頁面的抽象,主要負責創建對應的界面,接收參數,響應Navigator打開和關閉
  • Navigator則會維護一個路由棧管理Route,Route打開即入棧,Route關閉即出棧.
  • 基本路由: 創建一個MaterialPageRoute實例,調用Navigator.push方法將新頁面壓到堆棧的頂部
class FirstScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      //打開頁面
      onPressed: ()=> Navigator.push(context, MaterialPageRoute(builder: (context) => SecondScreen()));
    );
  }
}

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      // 回退頁面
      onPressed: ()=> Navigator.pop(context)
    );
  }
}
  • 命名路由: 簡化路由管理,命名路由.給頁面起一個名字,然後通過名字打開
MaterialApp(
    ...
    //註冊路由
    routes:{
      "second_page":(context)=>SecondPage(),
    },
);
//使用名字打開頁面
Navigator.pushNamed(context,"second_page");
  • 錯誤路由處理,統一返回UnknownPage

MaterialApp(
    ...
    //註冊路由
    routes:{
      "second_page":(context)=>SecondPage(),
    },
    //錯誤路由處理,統一返回UnknownPage
    onUnknownRoute: (RouteSettings setting) => MaterialPageRoute(builder: (context) => UnknownPage()),
);

//使用錯誤名字打開頁面
Navigator.pushNamed(context,"unknown_page");
  • 頁面參數: Flutter提供了路由參數的機制,可以在打開路由時傳遞相關參數,在目標頁面通過RouteSettings來獲取頁面參數

//打開頁面時傳遞字符串參數
Navigator.of(context).pushNamed("second_page", arguments: "Hey");

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //取出路由參數
    String msg = ModalRoute.of(context).settings.arguments as String;
    return Text(msg);
  }
}
  • 返回參數(類似startActivityForResult): 在push目標頁面時,可以設置目標頁面關閉時監聽函數,以獲取返回參數.而目標頁面可以在關閉路由時傳遞相關參數.

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: <Widget>[
          Text('Message from first screen: $msg'),
          RaisedButton(
            child: Text('back'),
            //頁面關閉時傳遞參數
            onPressed: ()=> Navigator.pop(context,"Hi")
          )
        ]
      ));
  }
}

class _FirstPageState extends State<FirstPage> {
  String _msg='';
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      body: Column(children: <Widget>[
        RaisedButton(
            child: Text('命名路由(參數&回調)'),
            //打開頁面,並監聽頁面關閉時傳遞的參數
            onPressed: ()=> Navigator.pushNamed(context, "third_page",arguments: "Hey").then((msg)=>setState(()=>_msg=msg)),
        ),
        Text('Message from Second screen: $_msg'),

      ],),
    );
  }
}
  • Navigator.push
    A->B->C->D,如何從 D頁面 pop 到 B 呢? Navigator.popUntil(context,ModalRoute.withName('B'));
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章