Flutter學習之認知基礎元件

真丶深紅騎士發表於2019-03-14

一、前言

前一天,學習了Dart語法,對Dart的語法和特性有了更深一步的瞭解。今天,來學習Flutter的基礎控制元件,身為Android開發者都知道,一開始入坑Android就要熟悉學習其控制元件,如:TextViewImageViewButtonListViewRecycleView等。為什麼要學習呢?因為平時的開發都離不開這些控制元件,UI的呈現都是有這些控制元件組成的,因此,其重要性就不用說了。對於Flutter來講,基礎控制元件(widget)就更加重要了。FlutterAndroid有所不一樣,Android佈局包含佈局(RelativeLayout,LinearLayout,ConstrainLayou)和元件。Flutter的一切都是Widget,包括最頂層佈局也是Widget,一個頁面有很多很多的Widget組合而成,Widget也稱為裝飾品,視窗小部件。

二、Widget簡介

Flutter裡,UI控制元件就是WidgetWidget根據不同的功能可以分為結構元素(如按鈕或選單),文字樣式(字型或者顏色方案),佈局屬性(如填充,對齊,居中),可以這麼理解,一個flutter的頁面是有一棵樹型的Widget組成,包括根節點,樹枝和樹葉,全都是Widget,只是Widget巢狀Widget,那就可以用下面這張圖來表示:

樹形圖
Flutter中,Widget是一切的基礎,作為響應式渲染,屬於MVVM的實現機制,通過修改資料,再用setState設定資料,Flutter會自動通過繫結的資料更新Widget,所以在平時開發中,開發者需要的就是實現Widget介面,和資料繫結起來。在平時,用的最多就是StatelessWidgetStatefulWidget這兩種WidgetStatelessWidget表示無狀態的,StatefulWidget表示有狀態的。這裡怎麼理解呢?在Flutter中每個頁面都是一幀,無狀態就是保持在那一幀,總而言之就是不能跟使用者互動,當有狀態的Widget當資料更新時,其實是繪製了新的Widget,也就是UI發生了變化,只是State實現了跨幀資料同步儲存。這裡給大家說下,在Android Studio看原始碼的兩個工具:

原始碼圖
左邊一欄Structure結構(看當前檔案,win下的快捷鍵是(Alt+7))和右邊Hierarchy繼承關係(看當前類,win下快捷鍵是F4)都可以幫助你閱讀原始碼。因為StatelessWidgetStatefulWidget用的最多,現在只需要用到這兩個,就先學習這兩個Widget

1.StatelessWidget

原始碼StatelessWidget只有三個方法:

StatelessWidget

  • const StatelessWidget({Key key}):super(key:key):初始化子類的[key]。這個key類是WidgetElementSemanticsNode唯一識別符號,是用來控制Widget數中替換Widget的時候使用的。
  • StatelessElement createElement():建立一個[StatelessElement]來管理這個小部件在樹中的位置,原始碼解釋:子類重寫此方法是不常見的,那這個方法也不用管,只需要知道這個方法用來管理自身在Widget樹中的位置。
  • Widget build(BuildContext context):描述這部件呈現使用者介面的部分。對於StatelessWidget,當Widget第一次插入到樹中,或者父節點更改了配置和所依賴的[InheritedWidget]改變,都會被重新呼叫。

這裡說下如何啟動一個Flutter應用,並使用Flutter框架:

import 'package:flutter/material.dart';
void main() {
  return runApp(Widget app);
}
複製程式碼

其實就是在main()函式中呼叫runApp函式。下面直接直接上例子,繼承StatelessWidget,通過build方法返回一個控制元件:

import 'package:flutter/material.dart';
//使用`flutter/material.dart` 目的是使用Matrial風格的小控制元件
void main(){
  //執行程式
  runApp(MyApp(null));
}
//繼承無狀態的StatelessWidget 使程式自身變為Wiget
class MyApp extends StatelessWidget{

  //要顯示的內容
  final String text;

  //資料內容可以通過構造方法傳遞進來
  MyApp(this.text);

  //重寫build方法 返回你需要的控制元件
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Container(
      //紅色背景
      color: Colors.red,
      //高度 現在沒用 會撐滿整個螢幕
      height: 200,
      //寬度 執行效果會撐滿整個螢幕
      width: 200,
      //內容居中
      alignment: Alignment.center,
      //Text控制元件
      child: new Text(
          //Dart語法中 ?? 表示如果text為空,就會返回??號的內容
          text ?? "my name is Knight",
        textDirection: TextDirection.ltr,//需要加上這句不然報 RichText widgets require a Directionality widget ancestor.
      ),

    );
  }
}
複製程式碼

WidgetWidget之間通過child進行巢狀,有些Widget只能有一個child。就像上面的Container,有些Widget可以有多個child,像Colum佈局。上面例子根佈局是ContainerContainer巢狀了Text

2.StatefulWidget

什麼是有狀態的控制元件呢?狀態是在建立控制元件可以同步讀取資訊,並且在控制元件的生命週期內可以改變,當控制元件狀態發生改變時使用State.setState來及時更新,原始碼也是隻有三個方法:

StatefulWidget
前兩個方法和StatelessWidget一樣的,而createState()這個方法原始碼註釋是:在Widget樹中給定的位置建立此可變狀態的小部件,子類應該重寫此方法返回新建的,關聯子類的例項。當呼叫一個StatefulWidget,框架就會呼叫createState這個方法,當一個StatefulWidgetWidget樹中移除,再次插入樹中,那麼會再次呼叫createState來建立一個新的State物件,這樣做簡化了State物件的生命週期。 需要建立管理的是主要是StateStatefulWidget用起來麻煩一些,他需要一個State,例子如下:

//繼承StatefulWidget
class StateWidget extends StatefulWidget{
  
   @override
   State createState(){
     return _StateWidget();
   }
}



class _StateWidget extends State<StateWidget>{
  
  //重寫build方法
  @override
  Widget build(BuildContext context){

  }
}
複製程式碼

簡單觀察上面程式碼,大致流程還是和StatelessWidget一樣的,build方法照樣返回Widget,不過在StatefulWidget將這個方法放在createState裡面。這裡細想一下,也知道為什麼要這樣做,因為當狀態改變,就會回撥createState方法,重新呼叫build方法重新建立UI,下面通過每兩秒改變UI這個例子來加深理解:

import 'package:flutter/material.dart';
//使用`flutter/material.dart` 目的是使用Matrial風格的小控制元件
import 'dart:async';//記得導庫
void main(){
  //執行程式
  runApp(StateWidget());
}
//控制元件繼承State
class _StateWidget extends State<StateWidget>{
  int Number = 0;
  String text;
  //建構函式
  _StateWidget(this.text);

  @override
  void initState(){
    //初始化,這個函式在控制元件的生命週期內呼叫一次
    super.initState();
    print("進入initState");
    //3秒後改變text的內容
    new Future.delayed(const Duration(seconds: 3),(){
      setState(() {
        Number++;
        text = "已經改變數值,數值現在是$Number";
      });

    });
  }

  @override
  void dispose(){
    //銷燬
    super.dispose();
    print('銷燬');
  }

  @override
  void didChangeDependencies(){
    //在initState之後調
     super.didChangeDependencies();
     print('進入didChange');
  }

  //重寫build方法
  @override
  Widget build(BuildContext context){
    return Container(
      //紅色背景
      color: Colors.red,
      //內容居中
      alignment: Alignment.center,
      //Text控制元件
      child: new Text(
        //Dart語法中 ?? 表示如果text為空,就會返回??號的內容
        text ?? "沒改變數值",
        textDirection: TextDirection.ltr,//需要加上這句不然報 RichText widgets require a Directionality widget ancestor.
      ),

    );
  }
}
複製程式碼

上面例子可以知道知道:在State可以動態更改資料,在呼叫setState後,改變的資料會除法Widget重新構建,上面程式碼還寫了三個生命週期方法,這裡簡單說一下:

  • initState:初始化操作
  • didChangeDependencies:在initState之後呼叫,可以獲取其他State
  • dispose:銷燬

平時開發中在build實現佈局的擺放,把資料新增Widget,通過setState改變資料。那如果很高頻率取改變資料,效能肯定受影響,以下三點可以減少重新構建有狀態控制元件的影響:

  1. 樹根上儘量不用狀態控制元件,因為如果資料有變化樹根每次都更新,那就是整棵樹都要重建,把狀態用在樹葉上,這樣更新的時候只會更新自己。
  2. 減少build方法所建立的節點數量和控制元件數量。
  3. 利用快取,如果子樹中不更改,將子樹中快取起來,每次使用其子樹時重新使用它,學會重用思想。
  4. 儘可能使用const修飾控制元件。 怎麼去選擇有狀態和無狀態,最簡單就是可以跟使用者進行互動應該使用StatefulWidget,例如:點選,滑動螢幕資訊流資料更新,如果只是僅僅顯示資料,那就可以選擇使用StatelessWidget建立一個無狀態控制元件。

三、Flutter頁面

Flutter有顯示的Widget和完整頁面呈現的Widget,常見的有MaterialAppScaffoldAppbarTextImageFlatButton,下面以表格形式簡單列一下:

Flutter頁面元素
下面一個個簡單上例子介紹:

1.MaterialApp

import 'package:flutter/material.dart';
//使用`flutter/material.dart` 目的是使用Matrial風格的小控制元件
void main(){
  //執行程式
  runApp(MyApp());
}

//用無狀態控制元件顯示
class MyApp extends StatelessWidget{
  
  @override
  Widget build(BuildContext context){
    return MaterialApp(
      //標題
      title:'Widget_Demo',
      //主題色
      theme:ThemeData(
        //設定為藍色
        primarySwatch: Colors.blue
      ),
      //這是一個Widget物件,用來定義當前應用開啟的時候,所顯示的介面
      home:MyHomePage(),
    );
  }
}


class MyHomePage extends StatelessWidget{
  
  @override
  Widget build(BuildContext context){
     return Scaffold(
       //設定appbar
       appBar:new AppBar(
         title:new Text('This is a Demo'),
       ),
       //主體
       body:new Center(
         //在螢幕中央顯示一個文字
         child:new Text('Hello'),
       ),
     );
  }
}
複製程式碼

效果如下圖:

MaterialApp效果圖
上面可以看到MaterialApp作為了主介面入口。

2.Scaffold

上面例子home:MyHomePage()這裡返回了ScaffoldWidget,而這個Widget正是我們所看到的頁面,看到Scaffold包含了appBarbody,一開始說到,Scaffold也包含Drawers,下面實現一下:

  @override
  Widget build(BuildContext context){
     return Scaffold(
       //設定appbar
       appBar:new AppBar(
         title:new Text('This is a Demo'),
       ),
       //主體
       body:new Center(
         //在螢幕中央顯示一個文字
         child:new Text('Hello'),
       ),
       //左側抽屜
       drawer:Drawer(
         //新增一個空的ListView
         child:ListView(),
       ),
     );
  }
複製程式碼

效果如下:

抽屜實現一
下面往抽屜裡新增點東西,就新增ListView,程式碼如下:

       //左側抽屜
       drawer:Drawer(
         child:ListView(
           //設定padding
           padding:EdgeInsets.zero,
           children: <Widget>[
             //據說這裡可以替換自定義的header
             //userHeader,
             ListTile(
               //標題內容
               title: Text("This is Item_one"),
               //前置圖示
               leading: new CircleAvatar(child:new Icon(Icons.scanner),),
             ),
             ListTile(
               //標題內容
               title: Text("This is Item_two"),
               //前置圖示
               leading: new CircleAvatar(child:new Icon(Icons.list),),
             ),
             ListTile(
               //標題內容
               title: Text("This is Item_three"),
               //前置圖示
               leading: new CircleAvatar(child:new Icon(Icons.score),),
             ),
           ],
         ),
       ),
複製程式碼

執行效果就是抽屜里加了三行內容的ListView

3.AppBar

下面設定一些AppBar屬性,玩玩:

      //設定appbar
      appBar: new AppBar(
        //AppBar內容顯示
        title: new Text('This is a Demo'),
        //前置圖示
        leading: new Icon(Icons.home),
        //背景顏色 改為紅色
        backgroundColor: Colors.red,
        //設定為標題內容居中
        centerTitle: true,
        //一個 Widget 列表,代表 Toolbar 中所顯示的選單,
        // 對於常用的選單,通常使用 IconButton 來表示;對於不常用的選單通常使用 PopupMenuButton 來顯示為三個點,點選後彈出二級選單
        actions: <Widget>[
          //IconButton
          new IconButton(
            //圖示
            icon: new Icon(Icons.add_a_photo),
            //提示
            tooltip: 'Add photo',
            //點選事件
            onPressed: () {},
          ),
          //選單彈出按鈕
          new PopupMenuButton<String>(
            itemBuilder: (BuildContext context) {
              return <PopupMenuItem<String>>[
                new PopupMenuItem<String>(
                    value: "one", child: new Text('This one')),
                new PopupMenuItem<String>(
                    value: "two", child: new Text('This two')),
              ];
            },
            //選擇點選事件
            onSelected: (String action) {
              switch (action) {
                case "one":
                //增加點選邏輯
                  break;
                case "two":
                //增加點選邏輯
                  break;
              }
            },
          ),
        ],
      ),
複製程式碼

效果如下:

AppBar學習
可以看到,上面Appbar上加了前置圖示、拍照圖示、選單彈出按鈕、陰影。

4.Text

下面用Text來展示文字,把上面例子用文字顯示中間的Hello單獨抽出來,如下:

   //主體
 body: new Center(
      //在螢幕中央顯示一個文字 改為自定義樣式
      child: new CustomTextStyle('This is a Text'),
 ),
      
//單獨文字樣式
class CustomTextStyle extends StatelessWidget{
  String text;
  //建構函式 引數外部傳進來
  CustomTextStyle(this.text);
  @override
  Widget build(BuildContext context){
    return Text(text ?? "Hello");

  }
}
複製程式碼

下面把文字字型大小修改,字型樣式修改,背景顏色改改:

//文字 : 單獨文字樣式
class CustomTextStyle extends StatelessWidget {
  Paint pg = Paint();
  String text;

  //建構函式 引數外部傳進來
  CustomTextStyle(this.text);

  @override
  Widget build(BuildContext context) {
    //設定畫筆顏色為黑色
    pg.color = Color(0xFF000000);
    return Text(
      text ?? "Hello",
      style: TextStyle(
          //顏色
          color: Colors.blue,
          //字型大小
          fontSize: 14,
          //字型加粗
          fontWeight: FontWeight.bold,
          //文字背景顏色
          background: pg),
    );
  }
}
複製程式碼

上面效果是:

文字樣式
還有很多的屬性,根據需要去設定就行:

 const TextStyle({
    this.inherit = true,
    this.color,//文字樣式
    this.fontSize,//字型大小
    this.fontWeight,//繪製文字時的字型粗細
    this.fontStyle,//字型變體
    this.letterSpacing,//水平字母之間的空間間隔(邏輯畫素為單位),可以負值
    this.wordSpacing,//單詞之間新增的空間間隔(邏輯畫素為單位),可以負值
    this.textBaseline,//對齊文字的水平線
    this.height,//文字行與行的高度,作為字型代銷的倍數
    this.locale,//用於選擇區域定字形的語言環境
    this.foreground,//文字的前景色,不能與color共同設定
    this.background,//文字背景色
    this.shadows,//Flutter Decoration背景設定(邊框,圓角,陰影,漸變等)
    this.decoration,//繪製文字裝飾,新增上下劃線,刪除線
    this.decorationColor,//文字裝飾的顏色
    this.decorationStyle,//文字裝飾的樣式,控制畫虛線,點,波浪線
    this.debugLabel,
    String fontFamily,//使用字型的名稱
    String package,
  })
複製程式碼

5.RichText

這是顯示豐富樣式的文字,這什麼意思呢?Text只能顯示一種樣式的文字,如果想在一段文字中顯示多種樣式,就好像Android裡面的SpannableString,就需要使用RichText,直接上例子:

//富文字樣式
class RichWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RichText(
        text: TextSpan(
          text: 'This is RichText',
          style: new TextStyle(
            //false的時候不顯示
              inherit: true,
              //字型大小
              fontSize: 16,
              //黑色
              color: Colors.black
          ),
          children: <TextSpan>[
            new TextSpan(
              text: 'Android藝術探索',
              style: new TextStyle(
                color: Colors.redAccent,
                //字型粗細
                fontWeight: FontWeight.bold,
              ),

            ),

            new TextSpan(text: '第一行程式碼'),
            new TextSpan(
              text: 'Android進階之光',
              style: new TextStyle(
                color: Colors.indigo,
                //字型樣式
                fontSize: 20,
              ),
            )
          ],
        )
    );
  }
}

//螢幕中間改為富文字widget
  //主體
      body: new Center(
        //Text在螢幕中央顯示一個文字 改為自定義樣式
        //child: new CustomTextStyle('This is a Text'),
        //富文字
          child:new RichWidget()
      ),
複製程式碼

效果如下:

富文字

6.TextField

下面看看文字輸入框,文字輸入框平時會經常用到:

body: new Center(
   //Text在螢幕中央顯示一個文字 改為自定義樣式
   //child: new CustomTextStyle('This is a Text'),
   //富文字
   //child:new RichWidget()
   //文字輸入框
     child:new TextFieldWidget()
),

//文字輸入框
class TextFieldWidget extends StatelessWidget{
  @override
  Widget build(BuildContext context){
      return TextField();
  }
}
複製程式碼

上面例子只能輸入文字內容,如果想要獲取輸入框內容,就要新增一個controller,通過這個controller新增通知來獲取TextField的值,我們一般點選按鈕或者需要跟後臺互動就要讀取controller.text的值:

class MyHomePage extends StatelessWidget {
  //獲取TextEditingController
  final editController = TextEditingController();
            //IconButton
          new IconButton(
            //圖示
            icon: new Icon(Icons.add_a_photo),
            //提示
            tooltip: 'Add photo',
            //點選事件
            onPressed: () {
              //輸出
              print('text inputted: ${editController.text}');
              //Toast
              Fluttertoast.showToast(
                  msg:'text inputted: ${editController.text}',
                  toastLength: Toast.LENGTH_SHORT,
                  gravity: ToastGravity.CENTER,
                  timeInSecForIos: 1,
              );
            },
          ),
     ....
       //主體
      body: new Center(
        //Text在螢幕中央顯示一個文字 改為自定義樣式
        //child: new CustomTextStyle('This is a Text'),

        //富文字
        //child:new RichWidget()

        //文字輸入框 以建構函式傳遞controller
          child:new TextFieldWidget(editController)
      ),
}
//文字輸入框
class TextFieldWidget extends StatelessWidget{

  final controller;
  //建構函式傳值
  TextFieldWidget(this.controller);
  @override
  Widget build(BuildContext context){
      return TextField(
        controller: controller,
      );
  }
}
複製程式碼

注意上面用到了ToastToast庫這裡很簡單需要兩步:

  1. pubspec.yaml新增依賴庫fluttertoast: ^2.1.1
  2. 匯入import 'package:fluttertoast/fluttertoast.dart';

重新執行即可,熱過載可能會出現異常。執行在iOS模擬器需要裝brewCocoaPods,有問題執行flutter doctor,它真是如名字一樣,就是幫你診斷有沒有錯誤資訊,會顯示具體資訊。效果如下:

TextField輸入框
下面改一下樣式:

      return TextField(
        controller: controller,
        //最大長度,右下角會顯示一個輸入數量的字串
        maxLength: 26,
        //最大行數
        maxLines: 1,
        //是否自動更正
        autocorrect: true,
        //是否自動對焦
        autofocus: true,
        //設定密碼 true:是密碼 false:不是祕密
        obscureText: true,
        //文字對齊樣式
        textAlign: TextAlign.center,

      );
複製程式碼

效果如下:

TextField樣式

7.Image

Image很好理解就是在介面上區域顯示一張圖片,而這張圖片的來源可以是:本地,網路,資源圖片等。下面一一演示一下:

7.1.專案圖片資源

首先新建一個資源目錄:

配置資源目錄
pubspec.yaml中配置圖片路徑,來識別應用程式所需的assets:

配置資源圖片

class MyHomePage extends StatelessWidget {
       //主體
      body: new Center(
        .....
        //圖片載入
        child:new ImageWidget()
      ),
}
//圖片
class ImageWidget extends StatelessWidget{
    @override
    Widget build(BuildContext context){
      //專案資源圖片 方式一
      return Image(
        image: new AssetImage('images/Image_fluttericon.jpeg'),
      );
      //專案資源圖片 方式二
//    return Image.asset('images/Image_fluttericon.jpeg');
    }
}
複製程式碼

效果如下:

專案本地資源

7.2.網路圖片載入

下面進行網路圖片載入,也是很簡單:

class MyHomePage extends StatelessWidget {
  //圖片路徑
  String image_url = "https://ws1.sinaimg.cn/large/0065oQSqgy1fze94uew3jj30qo10cdka.jpg";
       //主體
      body: new Center(
        .....
        //圖片載入
        child:new ImageWidget(image_url)
      ),
}
//圖片
class ImageWidget extends StatelessWidget{
    String image_url;
    ImageWidget(this.image_url);
    @override
    Widget build(BuildContext context){
        return Image.network(image_url);
    }
}
複製程式碼

效果如下:

網路載入圖片
下面用一個庫來載入和快取網路影象,也可以與佔位符和錯誤小部件一起使用,在pubspec.yaml新增依賴cached_network_image: ^0.4.1+1,在Dart檔案匯入這個庫import 'package:cached_network_image/cached_network_image.dart';

//圖片
class ImageWidget extends StatelessWidget{

    String image_url;
    ImageWidget(this.image_url);
    @override
    Widget build(BuildContext context){
        return new CachedNetworkImage(
            imageUrl: image_url,
            //佔位符
            placeholder: new CircularProgressIndicator(),
            //載入錯誤時顯示的圖片
            errorWidget: new Icon(Icons.error),
            //寬高
            width:200,
            height: 200,
        );
    }
}
複製程式碼

當圖片還沒載入出來的時候會顯示佔位符,當如果載入出錯會顯示errorWidget的圖片。

7.3.宣告解析度相關的圖片

另外Flutter可以為當前裝置新增合適其解析度的影象,其實對於Android原生來說,就是在不同解析度目錄下放置不同解析度的圖片,只不過flutter並不是建立drawable-xxdpi檔案,而是建立以下資料夾:

.../logo.png
.../Mx/logo.png
.../Nx/logo.png
複製程式碼

其中M和N是數字識別符號,對應於其中包含的影象解析度,它們指定不同素裝置像比例的圖片,主資源預設對應於1.0倍的解析度圖片。看下面例子:

不同解析度的圖
在裝置畫素比率為1.8的裝置上,images/2.0x/logo.png 將被選擇。對於2.7的裝置畫素比率,images/3.0x/logo.png將被選擇。如果未在Image控制元件上指定渲染影象的寬度和高度,以便它將佔用與主資源相同的螢幕空間量(並不是相同的物理畫素),只是解析度更高。 也就是說,如果images/logo.png是72px乘72px,那麼images/3.0x/logo.png應該是216px乘216px; 但如果未指定寬度和高度,它們都將渲染為72畫素×72畫素(以邏輯畫素為單位)。pubspec.yaml中asset部分中的每一項都應與實際檔案相對應,但主資源項除外。當主資源缺少某個資源時,會按解析度從低到的順序去選擇,也就是說1.0x中沒有的話會在2.0x中找,2.0x中還沒有的話就在3.0x中找。

      return Image(
        // 系統會根據解析度自動選擇不同大小的圖片
        image: AssetImage('images/logo.png'),
        // ...
      ),
複製程式碼

8.FlatButton

Flutter預先定義了一些按鈕控制元件,如FlatButtonRaisedButtonOutlineButtonIconButton

  1. FlatButton:扁平化按鈕,繼承自MaterialButton
  2. RaisedButton:凸起按鈕,繼承自MaterialButton
  3. OutlineButton:帶邊框按鈕,繼承自MaterialButton
  4. IconButton:圖示按鈕,繼承自StatelessWidget

下面看看FlatButton,其他的只是樣式稍微不一樣,大致用法一樣。

//按鈕
class FlatButtonWidget extends StatelessWidget{

  @override
  Widget build(BuildContext context){
      return FlatButton(
        onPressed: (){
          Fluttertoast.showToast(
            msg:'你點選了FlatButton',
            toastLength: Toast.LENGTH_SHORT,
            gravity: ToastGravity.CENTER,
            timeInSecForIos: 1,
          );
        },
        child: Text('FlatButton'),
        color: Colors.blue,//按鈕背景色
        textColor: Colors.white,//文字的顏色
        onHighlightChanged: (bool b){//水波紋變化回撥

        },
        disabledColor: Colors.black,//按鈕禁用時的顯示的顏色
        disabledTextColor: Colors.black38,//按鈕被禁用的時候文字顯示的顏色
        splashColor: Colors.white,//水波紋的顏色
      );

  }
}
複製程式碼

上面也設定了一些屬性,效果圖如下:

FlatButton

四、Flutter佈局

Flutter中擁有30多種預定義的佈局widget,常用的有ContainerPaddingCenterFlexRowColumListViewGridView。用一個表格列出它們的特性和使用。

Flutter佈局
下面一一介紹簡單用法:

1.Container

一個擁有繪製、定位、調整大小的widget,示意圖如下:

Container示意圖
下面直接上例子:

class MyHomePage extends StatelessWidget {
  ....
  body:new ContainWidget(),
  ...
}
//Container佈局
class ContainWidget extends StatelessWidget{

  @override
  Widget build(BuildContext context){
     return Container(
       child:Text("My name is Knight"),
       color: Colors.indigo,
       width:200,//寬
       height:200,//高
       margin:EdgeInsets.fromLTRB(5,5,5,5),//設定外邊距
       padding:EdgeInsets.all(30),//內邊距
     );
  }
}
複製程式碼

下面設定邊框,新增圓角:

//Container佈局
class ContainWidget extends StatelessWidget{

  @override
  Widget build(BuildContext context){
     return Container(
       ....
       padding:EdgeInsets.all(30),//內邊距
       decoration: BoxDecoration(//設定邊框
         //背景色
         color:Colors.redAccent,
         //圓角
         borderRadius: BorderRadius.circular(6),
       ),
     );
  }
}
複製程式碼

執行效果如下:

Contianer圓角

2.Padding

一個Widget,會給其子Widget新增指定的填充,示意圖如下:

Padding示意圖

class MyHomePage extends StatelessWidget {
  ....
  body: new PaddingWidget(),
  ...
}
//Padding佈局
class PaddingWidget extends StatelessWidget{

  @override
  Widget build(BuildContext context){
    return Padding(
      //設定左上右下內邊距為4,10,6,8
      padding:EdgeInsets.fromLTRB(4, 10, 6, 8),
      child: Text('My name is Knight'),
    );
  }
}
複製程式碼

效果圖如下:

Padding效果圖
下面實現Container巢狀Padding:

//Container巢狀Padding
class ContainPaddWidget extends StatelessWidget{
  @override
  Widget build(BuildContext context){
      return Container(
        width:200,//寬
        height:200,//高
        child: Padding(
          padding:EdgeInsets.fromLTRB(4, 10, 6, 8),
          child: Text("My name is Knight"),
        ),
        decoration: BoxDecoration(//設定邊框
          //背景色
          color:Colors.redAccent,
          //圓角
          borderRadius: BorderRadius.circular(6),
        ),
      );
  }
}
複製程式碼

效果圖如下:

Container巢狀Padding

3.Center

將其子widget居中顯示在自身內部的widget,示意圖:

Center示意圖

//Center
class CenterWidget extends StatelessWidget{

  @override
  Widget build(BuildContext context){
    return Container(
      width:200,//寬
      height:200,//高
      child: Center(
        child: Text("My name is Knight"),
      ),
      decoration: BoxDecoration(//設定邊框
        //背景色
        color:Colors.redAccent,
        //圓角
        borderRadius: BorderRadius.circular(6),
      ),
    );
  }
}
複製程式碼

執行效果如下:

Center執行效果圖
Center作為Container的孩子,Text所以在佈局的中間。

4.Stack

可以允許其子Widget簡單的堆疊在一起,層疊佈局,示意圖:

Stack示意圖
下面直接上程式碼,把之前的佈局全部用上試試:

class MyHomePage extends StatelessWidget {
  ....
   body:new Center(
              child:new StackWidget()
            ),
  ...
}
//層疊佈局
class StackWidget extends StatelessWidget{

  @override
  Widget build(BuildContext context){
    return Stack(
      children: <Widget>[
          new Image.network('https://ws1.sinaimg.cn/large/0065oQSqgy1fze94uew3jj30qo10cdka.jpg',
            width:300.0,//寬
            height:300.0,//高
            ),
        new Opacity(
          opacity: 0.6,//不透明度
          child:new Container(
            width:100.0,
            height:100.0,
            color:Colors.redAccent,
          ),
        ),
        new Opacity(
          opacity: 0.6,
          child:new Container(
            width: 200.0,
            height:200.0,
            color:Colors.indigo,
          ),
        ),
      ],
    );
  }
}
複製程式碼

執行效果:

stack執行效果圖
可以看到控制元件都按Stack左上角對齊,疊在一起,下面改一下顯示位置:

//層疊佈局
class StackWidget extends StatelessWidget{

  @override
  Widget build(BuildContext context){
    return Stack(
      //Aliginment的範圍是[-1,1],中心是[0,0].註釋有寫
      //和Android一樣,左移的取值是往1取,右移是往-1取
      //這裡注意,它是取stack裡範圍最大的佈局為基準,下面是以Container為//基準對齊
      alignment: new Alignment(-0.6, -0.6),
     ...
    );
  }
}
複製程式碼

執行效果圖:

Stack執行效果圖二

5.Colum

在垂直方向上排列子Widget,示意圖如下:

Column示意圖
直接上程式碼:

class MyHomePage extends StatelessWidget {
    ...
    body:new ColumnWidget(),
    ....
    
}
//Column佈局
class ColumnWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        Container(
          color:Colors.blue,
          width: 50,
          height: 50,
        ),
        Container(
          color:Colors.black,
          width:50,
          height:50,
        ),
        Container(
          color:Colors.green,
          width:50,
          height:50,
        ),
      ],
    );
  }
}
複製程式碼

執行效果:

Column效果圖
下面簡單設定一下排列方式屬性:

return Column(
      //設定垂直方向的對齊方式
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      ...
    );
複製程式碼

執行效果如下:

Column執行效果圖二
垂直方向(主軸上)屬性:

Column垂直屬性

  1. MainAxisAlignment.start這是預設值:垂直方向頂部對齊
  2. MainAxisAlignment.end:垂直方向底部對齊
  3. MainAxisAlignment.center:垂直方向居中對齊
  4. MainAxisAlignment.spaceBetween:垂直方向平分剩餘空間
  5. MainAxisAlignment.spaceAround:放置控制元件後,剩餘空間平分成n份,n是子widget的數量,然後把其中一份空間分成2份,放在第一個child的前面,和最後一個child的後面,也就是子widget的之前之後之間均勻分割空閒的一半空間
  6. MainAxisAlignment.spaceEvenly:放置控制元件後,把剩餘空間平分n+1份,然後平分所有的空間,在子widget之前之後之間均勻的分割空閒的空間

下面列一下水平方向(交叉軸)的屬性:

Column水平方向

  1. CrossAxisAlignment.center這是預設值,水平居中
  2. CrossAxisAlignment.end:水平方向右側對齊
  3. CrossAxisAlignment.start:水平方向左側對齊
  4. CrossAxisAlignment.stretch:水平方向拉伸子child填充滿布局
  5. CrossAxisAlignment.baseline:和textBaseline一起使用

6.Row

在水平方向上排列子widget的列表,示意圖:

Row示意圖
直接上程式碼:

class MyHomePage extends StatelessWidget {
  ....
  body:new RowWidget(),
  ...
}
//Row
class RowWidget extends StatelessWidget{
  @override
  Widget build(BuildContext context){
    return Row(
      children: <Widget>[
        Container(
          color:Colors.blue,
          width: 50.0,
          height:50.0,
        ),
        Container(
          color:Colors.black,
          width:50.0,
          height:50.0,
        ),
        Container(
          color:Colors.green,
          width:50.0,
          height:50.0,
        ),
      ],
    );
  }
}
複製程式碼

效果圖:

Row執行效果圖
下面簡單設定一些屬性,和Column沒多大差別:

return Row(
    //把剩餘空間平分n+1份,然後平分所有的空間
    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
    ...
 );
複製程式碼

效果圖:

Row執行效果圖
水平方向上(主軸上)屬性:

Row上主軸屬性

  1. MainAxisAlignment.start這是預設值,水平方向頂部對齊
  2. MainAxisAlignment.center:水平方向居中對齊
  3. MainAxisAlignment.end:水平方向底部對齊
  4. MainAxisAlignment.spaceBetween:水平方向上平分剩餘空間
  5. MainAxisAlignment.spaceAround:放置控制元件後,剩餘空間平分成n份,n是子widget的數量,然後把其中一份空間分成2份,放在第一個child的前面,和最後一個child的後面,也就是子widget的之前之後之間均勻分割空閒的一半空間
  6. MainAxisAlignment.spaceEvenly:放置控制元件後,把剩餘空間平分n+1份,然後平分所有的空間,在子widget之前之後之間均勻的分割空閒的空間 而交叉軸(垂直方向)的屬性:

Row上垂直方向

  1. CrossAxisAlignment.center這是預設,垂直居中
  2. CrossAxisAlignment.end:垂直方向右側對齊
  3. CrossAxisAlignment.start:垂直方向左側對齊
  4. CrossAxisAlignment.stretch:垂直方向拉伸子child填充滿布局
  5. CrossAxisAlignment.baseline:和textBaseline一起使用

7.Expanded

Expanded元件可以使RowColumnFiex等子元件在其主軸上方向展開並填充可用的空間,這裡注意:Expanded元件必須用在RowColumnFiex內,並且從Expanded到封裝它的RowColumnFlex的路徑必須只包括StatelessWidgets或者StatefulWidgets(不能是其他型別的元件,像RenderObjectWidget,它是渲染物件,不再改變尺寸,因此Expanded不能放進RenderObjectWidget),示意圖如下:

Expanded示意圖

class MyHomePage extends StatelessWidget {
  ....
  body:new RowWidget(),
  ...
}
class RowWidget extends StatelessWidget{
  @override
  Widget build(BuildContext context){
    return Row(
        children: <Widget>[
          new RaisedButton(
              onPressed: (){

              },
              color:Colors.green,
              child:new Text('綠色按鈕1')
          ),
          new Expanded(
            child:new RaisedButton(
              onPressed: (){

              },
              color:Colors.yellow,
              child:new Text('黃色按鈕2')
            ),
          ),
          new RaisedButton(
              onPressed:(){

              },
              color:Colors.red,
              child:new Text('黑色按鈕3')),
      ],
    );
  }
}
複製程式碼

執行效果如下:

Expanded效果圖

class MyHomePage extends StatelessWidget {
  ....
  body:new RowWidget(),
  ...
}
class RowWidget extends StatelessWidget{
  @override
  Widget build(BuildContext context){
    return Row(
        children: <Widget>[
         Expanded(
         child:Container(
           color:Colors.green,
           padding:EdgeInsets.all(8),
           height: 40.0,
         ),
         flex:1,
       ),
       Expanded(
         child:Container(
           color:Colors.yellow,
           padding:EdgeInsets.all(8),
           height: 40.0,
         ),
         flex:2,
       ),
       Expanded(
         child:Container(
           color:Colors.red,
           padding:EdgeInsets.all(8),
           height: 40.0,
         ),
       ),
      ],
    );
  }
}
複製程式碼

上面程式碼設定了flex,將一行的寬度分成四等分,第一、三child佔1/4的區域,第二個child佔1/2區域。 效果如下:

Expanded執行效果圖二

8.ListView

我相信這個佈局在平時開發會經常用到,這是可滾動的列表控制元件,ListView是最常用的滾動widget,它在滾動方向上一個接一個地顯示它的孩子。在縱軸上,孩子沒被要求填充ListView,並且內建ListTitle,示意圖如下:

ListView示意圖

class MyHomePage extends StatelessWidget {
  ....
  body: new ListViewWidget(
          new List<String>.generate(1000,(i){
            return 'Item &i';
          }),
      ),
  ...
}
//ListView
class ListViewWidget extends StatelessWidget {
  final List<String> items;
  ListViewWidget(this.items);
  @override
  Widget build(BuildContext context) {
    return new ListView.builder(
      itemCount: items.length,
      itemBuilder: (context, index) {
        return new ListTile(
          title: new Text('This is $index'),
        );
      },
    );
  }
}
複製程式碼

效果圖如下:

垂直的ListView
下面設定水平的ListView:

class MyHomePage extends StatelessWidget {
  ....
 body: new ListViewWidget(                       
  new List<String>.generate(1000, (i) {         
    return 'Item &i';                           
  }),                                            
),                                          
  ...
}
  Widget build(BuildContext context) {      
   return new ListView.builder(            
     itemCount: items.length,              
     //設定水平方向                              
     scrollDirection:Axis.horizontal,      
     //豎直時:確定每一個item的高度                    
     //水平時:確定每一個item的寬度 得要設定 不然不顯示         
     itemExtent: 110.0,                    
     itemBuilder: (context, index) {       
       return new ListTile(                
         title: new Text('This is $index'),
       );                                   
     },                                    
   );                                        
複製程式碼

效果如下:

ListView水平效果

9.GridView

GridView是一個網格佈局的列元件。GridView繼承至CustomScrollView,示意圖如下:

網格示意圖
直接豎直上例子:

//GridView                                           
class GridViewWidget extends StatelessWidget{        
                                                     
  @override                                          
  Widget build(BuildContext context){                
      return new GridView.count(                     
          crossAxisCount: 3, //3列                        
          children: List.generate(40,                
      (i){                                           
           return Card(                              
             child: Center(                          
               child:Text('This is $i'),             
             ),                                       
           );                                         
      })                                              
      );                                              
  }                                                  
}                                                    
複製程式碼

垂直的網格
下面上水平例子:

    return new GridView.count(         
      //3行                             
      crossAxisCount: 3,               
      //設定水平                           
      scrollDirection: Axis.horizontal,
      children: List.generate(40, (i) {
        return Card(                   
          child: Center(               
            child: Text('This is $i'), 
          ),                            
        );                              
      }),                               
    );                                  
複製程式碼

效果圖如下:

水平的GridView

10.TabBar

移動開發中tab切換是一個很常用的功能,那麼Flutter有沒有提供這個Widget呢?答案是有的,Flutter通過Material庫提供了很方便的API來使用tab切換。

10.1.建立TabController

TabBarViewTabBar都有一個TabController的引數,TabbarViewTabBar就是由TabController來控制同步,點選某個Tab後,要同步顯示對應的TabBarView,建立TabController有兩種方式:

  1. 使用系統自帶的DefaultTabController,在Scaffold套一層DefaultTabController,這種方式TabBarView會自動查詢這個tabController
  2. 自己定義一個TabController,實現SingleTickerProviderStateMixin

下面就列一下第一種方式:

 @override
  Widget build(BuildContext context) {
    return new DefaultTabController();
 }
複製程式碼

10.2.構建Tab資料

final List<Tab> myTabs = <Tab>[
    new Tab(text: 'Android'),
    new Tab(text: 'IOS'),
    new Tab(text: 'Flutter'),
    new Tab(text: 'RN'),
    new Tab(text: 'Java'),
    new Tab(text: 'C'),
    new Tab(text: 'C++'),
    new Tab(text: 'Go'),
  ];

複製程式碼

10.3.建立TabBar

TabBar在哪裡都可以建立,在AppBar裡有一個bottom引數可以接受TabBar,就放在AppBar下:

        //設定appbar
        appBar: new AppBar(
          //底部
          bottom: new TabBar(
            indicatorColor: Colors.red, //指示器顏色 如果和標題欄顏色一樣會白色
            tabs: myTabs,//繫結資料
            isScrollable: true, //是否可以滑動
          ),
    ),
複製程式碼

10.4.繫結TabBar和TabBarView

class MyHomePage extends StatelessWidget {
final List<Tab> myTabs = <Tab>[
    new Tab(text: 'Android'),
    new Tab(text: 'IOS'),
    new Tab(text: 'Flutter'),
    new Tab(text: 'RN'),
    new Tab(text: 'Java'),
    new Tab(text: 'C'),
    new Tab(text: 'C++'),
    new Tab(text: 'Go'),
  ];
  @override
  Widget build(BuildContext context) {
    return new DefaultTabController(
      length: myTabs.length, //Tab長度
      child: new Scaffold(
        //設定appbar
        appBar: new AppBar(
          //底部
          bottom: new TabBar(
            indicatorColor: Colors.red, //指示器顏色 如果和標題欄顏色一樣會白色
            tabs: myTabs,//繫結資料
            isScrollable: true, //是否可以滑動
          ),
         ....
        ),
          body: new TabBarView(
          //選中哪個Tabs,body就會顯示
          children: myTabs.map((Tab tab) {
            return new Center(child: new Text(tab.text));
          }).toList(),
        ),
        ....
    );
  }
}
複製程式碼

效果如下圖:

TabBar效果圖

11.BottomNavigationBar

BottomNavigationBar即是底部導航欄控制元件,顯示在頁面底部的設計控制元件,用於在試圖切換,底部導航欄包含多個標籤、圖示或者兩者搭配的形式,簡而言之提供了頂級檢視之間的快速導航。

11.1.構建底部標籤

 //底部資料
  final Map bottomMap ={
    "首頁":Icon(Icons.home),
    "朋友圈":Icon(Icons.camera),
    "資訊":Icon(Icons.message),
    "其他":Icon(Icons.devices_other),
  };
複製程式碼

11.2.建立導航欄

因為點選導航欄需要對應的字型顯示,所以MyHomePage需要繼承StatefulWidget,增加State

//用無狀態控制元件顯示
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      //主題色
      theme: ThemeData(
          //設定為紅色
          primarySwatch: Colors.red),
      //這是一個Widget物件,用來定義當前應用開啟的時候,所顯示的介面
      home: MyHomePageWidget(),
    );
  }
}

class MyHomePageWidget extends StatefulWidget{
  @override
  State<StatefulWidget> createState(){
     return new MyHomePage();
  }
}
class MyHomePage extends State<MyHomePageWidget> {
 //底部資料
  final Map bottomMap ={
    "首頁":Icon(Icons.home),
    "朋友圈":Icon(Icons.camera),
    "資訊":Icon(Icons.message),
    "其他":Icon(Icons.devices_other),
  };

  int _index = 0;
 bottomNavigationBar: BottomNavigationBar(
            items: (){
              var items = <BottomNavigationBarItem>[];
              bottomMap.forEach((k,v){
                items.add(BottomNavigationBarItem(
                  title:Text(k),//取map的值
                  icon : v,//取map的圖示
                  backgroundColor:Colors.red,//背景紅色
                ));
              });
              return items;
            }(),
             currentIndex: _index,//選中第幾個
             onTap:(position){
               Fluttertoast.showToast(
                   msg: 'text inputted: $position',
                   toastLength: Toast.LENGTH_SHORT,
                   gravity: ToastGravity.CENTER,
                   timeInSecForIos: 1,
               );
               setState(() {
                 _index = position;//狀態更新
               });
             }
            ),
   }
複製程式碼

最終效果如下:

第三天最後成品

五、實踐

下面實踐Flutter中文網的例子:

第三天例子
先上佈局分析圖:

例子分析圖

1.實現影象

再說一下如何配置影象

影象存放步驟

class MyApp extends StatelessWidget{
  @override
  Widget build(BuildContext context){
    return MaterialApp(
       home:new MyHomeWidget(),
    );
  }
}


class MyHomeWidget extends StatelessWidget{
  @override
  Widget build(BuildContext context){
     return new Scaffold(
       //設定標題欄
       appBar: new AppBar(
           title:new Text('Flutter Demo'),
       ),
       //主體用ListView
       body:new ListView(
         children: <Widget>[
           //圖片
           new Image.asset(
             'images/lake.jpg',
             width:600.0,
             height:240.0,
             //順便設定圖片屬性
             fit:BoxFit.cover,
           )
         ],
       ),
     );
  }
}
複製程式碼

2.實現標題欄

    //實現標題欄
     Widget titleWidget = new Container(
       //內邊距
       padding:const EdgeInsets.all(30.0),
       //整體是一個水平的佈局
       child:new Row(
         //只有一個孩子
         children: <Widget>[
           //用Expanded 會佔用icon之外剩餘空間
           new Expanded(
               //垂直佈局 放置兩個文字
               child: new Column(
                 //設定文字一起始端對齊
                 crossAxisAlignment: CrossAxisAlignment.start,
                 //有兩個孩子
                 children: <Widget>[
                   new Container(
                     //底部內邊距
                     padding:const EdgeInsets.only(bottom:10.0),
                     //孩子 設定字型樣式
                     child:new Text(
                       'Oeschinen Lake Campground',
                       style: new TextStyle(fontWeight: FontWeight.bold),
                     ),
                   ),
                   new Text(
                     'Kandersteg, Switzerland',
                     style: new TextStyle(
                       color:Colors.grey[450],//設定顏色透明度
                     ),
                   )
                 ],
               ),
           ),
           new Icon(
             Icons.star,
             color:Colors.red[400],
           ),

           new Text('41'),
         ],
       ),
     );
複製程式碼

3.實現按鈕行

因為三個按鈕樣式都是一樣的,所以抽取公共部分:

     /**
      * 抽取button行的程式碼複用
      *
      */
     Column getText(IconData icon,String text){
         return new Column(
           //聚集widgets
           mainAxisSize:MainAxisSize.min,
           //child居中
           mainAxisAlignment: MainAxisAlignment.center,
           children: <Widget>[
             new Icon(icon,color:Colors.blue[500]),
             new Container(
               //上部外邊距
               margin: const EdgeInsets.only(top:8.0),
               //Text內容樣式設定
               child:new Text(
                 text,
                 style:new TextStyle(
                   color:Colors.blue[500],
                 ),
               ),
             )
           ],

         );

     }

     /**
      * 按鈕實現
      */
     Widget buttonWidget = new Container(
       //三列
       child:new Row(
         //用MainAxisAlignment.spaceEvenly平均分配子空間
         mainAxisAlignment: MainAxisAlignment.spaceEvenly,
         //孩子們
         children: <Widget>[
           getText(Icons.call, "CALL"),
           getText(Icons.near_me, "ROUTE"),
           getText(Icons.share, "SHARE"),
         ],
       ),
     );
複製程式碼

4.實現文字

     /**
      * 文字實現
      */
     Widget textWidget = new Container(
       alignment: Alignment.center,
       //設定內邊距
        padding:const EdgeInsets.all(10.0),
        child:new Text(
           'Lake Oeschinen lies at the foot of the Blüemlisalp in the Bernese Alps. Situated 1,578 meters above sea level, '
               'it is one of the larger Alpine Lakes. A gondola ride from Kandersteg, '
               'followed by a half-hour walk through pastures and pine forest, '
               'leads you to the lake, which warms to 20 degrees Celsius in the summer. '
               'Activities enjoyed here include rowing, and riding the summer toboggan run.',
          // softWrap: true,//屬性表示文字是否應在軟換行符(例如句點或逗號)之間斷開。
          // textAlign: TextAlign.center,
         ),

     );
複製程式碼

5.整合

   return new Scaffold(
       //設定標題欄
       appBar: new AppBar(
           title:new Text('Flutter Demo'),
       ),
       //主體用ListView
       body:new ListView(
         children: <Widget>[
           //圖片
           new Image.asset(
             'images/lake.jpg',
             width:600.0,
             height:240.0,
             //順便設定圖片屬性
             fit:BoxFit.cover,
           ),
           //標題欄
           titleWidget,
           //按鈕欄
           buttonWidget,
           //文字欄
           textWidget,
         ],

       ),
     );

複製程式碼

執行效果圖:

最後成果

六、總結

Flutter還有很多Widget上面沒有說到,就只能自己有空再去學習了,下面直接上一張圖,今天學到的內容:

Widget總結圖

學習連結:flutterchina.club/widgets/

如有不正之處歡迎大家批評指正~

相關文章