學(xué)寫Flutter

一、Widget

Flutter設(shè)計思想,Everything is Widget。

Widget 是一個比較寬泛的概念,無論基本部件、布局、還是手勢等都是 Widget。
它是對視圖的一種包含配置及狀態(tài)信息的“描述數(shù)據(jù)”,用于約束具體的視圖元素

Widget樹

widget樹圖

StatelessWidget

StatelessWidget一旦創(chuàng)建就無法進行修改,這意味著它不會因為外部條件變化而重新繪制。

其生命周期:

  • 1、初始化
  • 2、通過build()方法進行渲染。

StatefulWidget

它可以在其生命周期中操作內(nèi)部持有數(shù)據(jù)的變化,這些數(shù)據(jù)被稱為State,這樣的Widget也叫做StatefulWidget。

  • createState():StatefulWidget類中提供了新的接口,用于創(chuàng)建State;

例:

class RoutePageA extends StatefulWidget {
  @override
  RoutePageAState createState() => RoutePageAState();
}

class RoutePageAState extends State<RoutePageA> {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Text("abc" );
  }
}

State

一個StatefulWidget類會對應(yīng)一個State類,State表示與其對應(yīng)的StatefulWidget要維護的狀態(tài),State中的保存的狀態(tài)信息可以:

  • 1、在widget 構(gòu)建時可以被同步讀取。

  • 2、在widget生命周期中可以被改變,當State被改變時,可以手動調(diào)用其setState()方法通知Flutter framework狀態(tài)發(fā)生改變,F(xiàn)lutter framework在收到消息后,會重新調(diào)用其build方法重新構(gòu)建widget樹,從而達到更新UI的目的。

State中有兩個常用屬性:

  • 1、widget,它表示與該State實例關(guān)聯(lián)的widget實例,由Flutter framework動態(tài)設(shè)置。注意,這種關(guān)聯(lián)并非永久的,因為在應(yīng)用生命周期中,UI樹上的某一個節(jié)點的widget實例在重新構(gòu)建時可能會變化,但State實例只會在第一次插入到樹中時被創(chuàng)建,當在重新構(gòu)建時,如果widget被修改了,F(xiàn)lutter framework會動態(tài)設(shè)置State.widget為新的widget實例。

  • 2、context, StatefulWidget對應(yīng)的BuildContext

StatefulWidget生命周期

State作為StatefulWidget的主體,它可以在多個節(jié)點對State進行調(diào)整。

其生命周期:

  • initState()當Widget第一次插入到Widget樹時會被調(diào)用,在生命周期中只被調(diào)用一次。重寫該方法主要完成一些初始化工作。注意:在該方法中context對象可以訪問但并不能拿來使用,因為此時state與context沒有建立關(guān)聯(lián)。

  • didChangeDependencies()當State對象的依賴發(fā)生變化時會被調(diào)用;例如:在之前build() 中包含了一個InheritedWidget,然后在之后的build() 中InheritedWidget發(fā)生了變化,那么此時InheritedWidget的子widget的didChangeDependencies()回調(diào)都會被調(diào)用

  • build():在didChangeDependencies()didUpdateWidget()setState()之后執(zhí)行,主要用于構(gòu)建Widget子樹(類似RN中render()方法)。

  • reassemble()此回調(diào)專門為開發(fā)調(diào)試而提供,在熱重載時會被調(diào)用,此回調(diào)在Release模式下永遠不會被調(diào)用。

  • didUpdateWidget():在widget重新構(gòu)建時,F(xiàn)lutter framework會調(diào)用Widget.canUpdate來檢測Widget樹中同一位置的新舊節(jié)點,然后決定是否需要更新,如果Widget.canUpdate返回true則會調(diào)用此回調(diào)。正如之前所述,Widget.canUpdate會在新舊widget的key和runtimeType同時相等時會返回true,也就是說在在新舊widget的key和runtimeType同時相等時didUpdateWidget()就會被調(diào)用。

  • deactivate()當State對象從樹中被移除時,會調(diào)用此回調(diào),在一些場景下,F(xiàn)lutter framework會將State對象重新插到樹中,如包含此State對象的子樹在樹的一個位置移動到另一個位置時(可以通過GlobalKey來實現(xiàn))。如果移除后沒有重新插入到樹中則緊接著會調(diào)用dispose()方法。

  • dispose():當State對象從樹中被永久移除時調(diào)用;通常在此回調(diào)中釋放資源
    [圖片上傳失敗...(image-64ecb1-1629883919829)]

Widget構(gòu)造函數(shù)

例:

  const WidgetPage({
    Key? key,
    required this.text,
    this.backgroundColor: Colors.grey,
    this.fontSize: 40,
    this.child,
  }) : super(key: key);
  final String text;
  final Color backgroundColor;
  final double fontSize;
  final Widget? child;
  
調(diào)用:
    return WidgetPage(
      text: "Widget Demo",
      fontSize: 100,//非必要參數(shù)
    );
  • widget的構(gòu)造函數(shù)應(yīng)使用命名參數(shù)

  • 命名參數(shù)中必要的參數(shù)要加required

  • 在繼承Widget時,第一個參數(shù)通常為Key,主要要的作用是決定是否在下一次build時復(fù)用舊的widget,決定的條件在canUpdate()方法中。

  • 如果需要接收子widget,child或children參數(shù)通常放在最后

  • widget的屬性盡可能的被聲明為final,防止被意外改變

BuildContext

Context 僅僅是已創(chuàng)建的所有 Widget 樹結(jié)構(gòu)中某個 Widget 的位置引用。

  • BuildContext是Widget樹結(jié)構(gòu)中每個Widget的上下文環(huán)境,每個BuildContext都只屬于一個Widget。

  • 如果Widget A和它的子Widget B,則Widget A的BuildContext是Widget B的BuildContext的父context。

  • context 提供了一些方法,比如沖當前widget開始向上遍歷widget樹以及按照類型查找widget的方法。

class ContextRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Context測試"),
      ),
      body: Container(
        child: Builder(builder: (context) {
          // 在Widget樹中向上查找最近的父級`Scaffold` widget
          Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>();
          // 直接返回 AppBar的title, 此處實際上是Text("Context測試")
          return (scaffold.appBar as AppBar).title;
        }),
      ),
    );
  }
}


在flutter中我們經(jīng)常會使用到這樣的代碼

//打開一個新的頁面
  Navigator.of(context).push
//打開Scaffold的Drawer
  Scaffold.of(context).openDrawer
//獲取display1樣式文字主題
  Theme.of(context).textTheme.display1
  • of(context)實際上對context跨組件獲取數(shù)據(jù)的一個分裝。

  • 需要注意的是,在 State 中 initState階段是無法跨組件拿數(shù)據(jù)的,只有在didChangeDependencies之后才可以使用這些方法。

二、基礎(chǔ)組件

flutter官方提供了一些組件,如下官網(wǎng)截圖。

文本

Text

Text用于顯示簡單樣式文本

Text(
  "文本",
  textAlign: TextAlign.left,
  overflow: TextOverflow.ellipsis,
  maxLines: 1,
  textScaleFactor: 1.5,
  style: TextStyle(
      color: Colors.cyan,
      fontSize: 20,
  ),
);
  • textAlign:文本的對齊方式
  • maxLines:文本顯示最大行數(shù)
  • overflow:指定截斷方式
  • textScaleFactor:代表文本相對于當前字體大小的縮放因子
  • style:通過TextStyle設(shè)置文本樣式

TextSpan

如果我們需要對一個Text內(nèi)容的不同部分按照不同樣式顯示,這時可以使用TextSpan,它代表文本的一個"片斷"
其定義如下:

const TextSpan({
  TextStyle style, 
  Sting text,
  List<TextSpan> children,
  GestureRecognizer recognizer,
});

例:

Text.rich(TextSpan(children: [
  TextSpan(
      text: "Hello",
      style: TextStyle(
        color: Colors.blue,
      )),
  TextSpan(
      text: " World",
      style: TextStyle(
        color: Colors.red,
      ),
      recognizer: _tapSpanText
        ..onTap = () {
          print("SpanTextAction");
        }),
]));

通過Text.rich 方法將TextSpan 添加到Text中,之所以可以這樣做,是因為Text其實就是RichText的一個包裝,而RichText是可以顯示多種樣式(富文本)的widget

  • text:文本
  • style:文本樣式
  • recognizer:手勢

DefaultTextStyle

在Widget樹中,文本的樣式默認是可以被繼承的,因此,如果在Widget樹的某一個節(jié)點處設(shè)置一個默認的文本樣式,那么該節(jié)點的子樹中所有文本都會默認使用這個樣式。
例:

DefaultTextStyle(
  //1.設(shè)置文本默認樣式
  style: TextStyle(
    color: Colors.red,
    fontSize: 20.0,
  ),
  child: Column(
    children: <Widget>[
      Text("hello world"),
      Text("I am Jack"),
      Text(
        "I am Jack",
        style: TextStyle(
            inherit: false, //2.不繼承默認樣式
            color: Colors.grey),
      ),
    ],
  ),
);
  • DefaultTextStyle通過style設(shè)置了默認樣式,其子Text都繼承其樣式
  • 例中最后一個Text使用inherit: false設(shè)置不繼承默認樣式

按鈕

Material組件庫中的按鈕

TextButton

簡單的扁平按鈕,按下后,會有背景色

TextButton(
  style: TextButton.styleFrom(
    textStyle: const TextStyle(fontSize: 20),
  ),
  child: Text("TextButton"),
  onPressed: () => print("TextButton"),
)
  • onPressed: null 會自動顯示不可點擊樣式

IconButton

可點擊的Icon,不包括文字,默認沒有背景,點擊后會出現(xiàn)背景

IconButton(
 onPressed: () => print("IconButton"),
 icon: Icon(Icons.thumb_up),
);

FloatingActionButton

懸浮按鈕,圓形、有陰影

FloatingActionButton(
    child: Text("漂"),
    tooltip: "點我",
    onPressed: () => print("FloatingActionButton"));
}

ElevatedButton

漂浮按鈕,它默認帶有陰影和灰色背景

Column(
  children: [
    ElevatedButton(
        child: Text("漂"), onPressed: () => print("ElevatedButton")),
    ElevatedButton(child: Text("漂"), onPressed: null),
  ],
);
  • onPressed: null 會自動顯示不可點擊樣式

OutlineButton

默認有一個邊框,不帶陰影且背景透明。按下后,邊框顏色會變亮、同時出現(xiàn)背景和陰影(較弱)

OutlinedButton(
    onPressed: () => print("OutlinedButton"),
    child: Text("OutlinedButton")
);

圖片

Flutter中,我們可以通過Image組件來加載并顯示圖片,Image的數(shù)據(jù)源可以是asset、文件、內(nèi)存以及網(wǎng)絡(luò)

ImageProvider

ImageProvider 是一個抽象類,主要定義了圖片數(shù)據(jù)獲取的接口load(),從不同的數(shù)據(jù)源獲取圖片需要實現(xiàn)不同的ImageProvider ,如AssetImage是實現(xiàn)了從Asset中加載圖片的ImageProvider,而NetworkImage實現(xiàn)了從網(wǎng)絡(luò)加載圖片的ImageProvider

Image

Image widget有一個必選的image參數(shù),它對應(yīng)一個ImageProvider。下面我們分別演示一下如何從asset和網(wǎng)絡(luò)加載圖片

在asset中加載圖片
  • 1、在工程根目錄下創(chuàng)建一個images目錄,并將圖片avatar.png拷貝到該目錄。

  • 2、在pubspec.yaml中的flutter部分添加如下內(nèi)容:

  assets:
    - images/avatar.png
  • 3、加載圖片
Image(
  image: AssetImage("images/avatar.png"),
);
在網(wǎng)絡(luò)中加載圖片
Image(
  image: NetworkImage(
      "https://avatars2.githubusercontent.com/u/20411648?s=460&v=4"),
)

Image也提供了一個快捷的構(gòu)造函數(shù)Image.network用于從網(wǎng)絡(luò)加載、顯示圖片:

Image.network(
  "https://avatars2.githubusercontent.com/u/20411648?s=460&v=4",
)

參數(shù):

  • image: 它對應(yīng)一個ImageProvider
  • width、height:設(shè)置圖片寬高
  • fit:圖片顯示適應(yīng)模式,在BoxFit中定義
    fill:會拉伸填充滿顯示空間,圖片本身長寬比會發(fā)生變化,圖片會變形。
    
    cover:會按圖片的長寬比放大后居中填滿顯示空間,圖片不會變形,超出顯示空間部分會被剪裁。
    
    contain:這是圖片的默認適應(yīng)規(guī)則,圖片會在保證圖片本身長寬比不變的情況下縮放以適應(yīng)當前顯示空間,圖片不會變形。
    
    fitWidth:圖片的寬度會縮放到顯示空間的寬度,高度會按比例縮放,然后居中顯示,圖片不會變形,超出顯示空間部分會被剪裁。
    
    fitHeight:圖片的高度會縮放到顯示空間的高度,寬度會按比例縮放,然后居中顯示,圖片不會變形,超出顯示空間部分會被剪裁。
    
    none:圖片沒有適應(yīng)策略,會在顯示空間內(nèi)顯示圖片,如果圖片比顯示空間大,則顯示空間只會顯示圖片中間部分。

  • colorcolorBlendMode:在圖片繪制時可以對每一個像素進行顏色混合處理,color指定混合色,而colorBlendMode指定混合模式
  • repeat:重復(fù)規(guī)則

輸入框

Material組件庫中提供了輸入框組件TextField和表單組件Form。

TextField

TextField用于文本輸入,它提供了很多屬性,如下:

const TextField({
  ...
  TextEditingController controller, 
  FocusNode focusNode,
  InputDecoration decoration = const InputDecoration(),
  TextInputType keyboardType,
  TextInputAction textInputAction,
  TextStyle style,
  TextAlign textAlign = TextAlign.start,
  bool autofocus = false,
  bool obscureText = false,
  int maxLines = 1,
  int maxLength,
  bool maxLengthEnforced = true,
  ValueChanged<String> onChanged,
  VoidCallback onEditingComplete,
  ValueChanged<String> onSubmitted,
  List<TextInputFormatter> inputFormatters,
  bool enabled,
  this.cursorWidth = 2.0,
  this.cursorRadius,
  this.cursorColor,
  ...
})

  • controller:編輯框的控制器,通過它可以設(shè)置/獲取編輯框的內(nèi)容、選擇編輯內(nèi)容、監(jiān)聽編輯文本改變事件。大多數(shù)情況下我們都需要顯式提供一個controller來與文本框交互。如果沒有提供controller,則TextField內(nèi)部會自動創(chuàng)建一個。

  • focusNode:用于控制TextField是否占有當前鍵盤的輸入焦點。它是我們和鍵盤交互的一個句柄(handle)。

  • InputDecoration:用于控制TextField的外觀顯示,如提示文本、背景顏色、邊框等。

  • keyboardType:用于設(shè)置該輸入框默認的鍵盤輸入類型

First Header Second Header
TextInputType枚舉值 含義
text 文本輸入鍵盤
multiline 多行文本,需和maxLines配合使用(設(shè)為null或大于1)
number 數(shù)字;會彈出數(shù)字鍵盤
phone 優(yōu)化后的電話號碼輸入鍵盤;會彈出數(shù)字鍵盤并顯示“* #”
datetime 優(yōu)化后的日期輸入鍵盤;Android上會顯示“: -”
emailAddress 優(yōu)化后的電子郵件地址;會顯示“@ .”
url 優(yōu)化后的url輸入鍵盤; 會顯示“/ .”
  • textInputAction:鍵盤動作按鈕圖標(即回車鍵位圖標),它是一個枚舉值,有多個可選值,全部的取值列表讀者可以查看API文檔,如:TextInputAction.search。

  • style:正在編輯的文本樣式。

  • textAlign: 輸入框內(nèi)編輯文本在水平方向的對齊方式。

  • autofocus: 是否自動獲取焦點。

  • obscureText:是否隱藏正在編輯的文本,如用于輸入密碼的場景等,文本內(nèi)容會用“?”替換。

  • maxLines:輸入框的最大行數(shù),默認為1;如果為null,則無行數(shù)限制。

  • maxLength和maxLengthEnforced:maxLength代表輸入框文本的最大長度,設(shè)置后輸入框右下角會顯示輸入的文本計數(shù)。maxLengthEnforced決定當輸入文本長度超過maxLength時是否阻止輸入,為true時會阻止輸入,為false時不會阻止輸入但輸入框會變紅。

  • onChange:輸入框內(nèi)容改變時的回調(diào)函數(shù);注:內(nèi)容改變事件也可以通過controller來監(jiān)聽。

  • onEditingComplete和onSubmitted:這兩個回調(diào)都是在輸入框輸入完成時觸發(fā),比如按了鍵盤的完成鍵(對號圖標)或搜索鍵(??圖標)。不同的是兩個回調(diào)簽名不同,onSubmitted回調(diào)是ValueChanged<String>類型,它接收當前輸入內(nèi)容做為參數(shù),而onEditingComplete不接收參數(shù)。

  • inputFormatters:用于指定輸入格式;當用戶輸入內(nèi)容改變時,會根據(jù)指定的格式來校驗。

  • enable:如果為false,則輸入框會被禁用,禁用狀態(tài)不接收輸入和事件,同時顯示禁用態(tài)樣式(在其decoration中定義)。

  • cursorWidth、cursorRadius和cursorColor:這三個屬性是用于自定義輸入框光標寬度、圓角和顏色的。

自定義樣式

可以通過decoration屬性來定義輸入框樣式

decoration: InputDecoration(
      hintText: "請輸入用戶名",//提示文字
      prefixIcon: Icon(Icons.lock),//左側(cè)圖標
        //邊框樣式
      border: OutlineInputBorder(
        borderRadius: BorderRadius.all(Radius.circular(10.0)),
      ),
      enabledBorder: OutlineInputBorder(
        borderRadius: BorderRadius.all(Radius.circular(10.0)),
        borderSide: BorderSide(
          color: Colors.grey,
        ),
      ),
      focusedBorder: OutlineInputBorder(
        borderRadius: BorderRadius.all(Radius.circular(10.0)),
        borderSide: BorderSide(
          color: Colors.red,
        ),
      ),
    ),
獲取&監(jiān)聽文本變化

獲取輸入內(nèi)容有兩種方式:

  • 1、通過onChange 獲取
onChanged: (value) => print(value),
  • 2、通過controller 獲取
    定義一個controller:
TextEditingController _unameController = TextEditingController();

然后設(shè)置輸入框controller:

TextField(
    autofocus: true,
    controller: _unameController, //設(shè)置controller
    ...
)

通過controller獲取輸入框內(nèi)容

@override
void initState() {
  //監(jiān)聽輸入改變  
  _unameController.addListener((){
    print(_unameController.text);
  });
}


兩種方式相比,onChanged是專門用于監(jiān)聽文本變化,而controller的功能卻多一些,除了能監(jiān)聽文本變化外,它還可以設(shè)置默認值、選擇文本等

監(jiān)聽焦點狀態(tài)改變事件:
...
// 創(chuàng)建 focusNode   
FocusNode focusNode = new FocusNode();
...
// focusNode綁定輸入框   
TextField(focusNode: focusNode);
...
// 監(jiān)聽焦點變化    
focusNode.addListener((){
   print(focusNode.hasFocus);
});

此章:Icon、表單Form、進度指示器ProgressIndicator 自己看一下吧

三、布局方式

布局類組件簡介

布局類組件都會包含一個或多個子組件,不同的布局類組件對子組件排版(layout)方式不同。

根據(jù)Widget是否包含子節(jié)點將其分為3類
  • 1、LeafRenderObjectWidget:Widget樹的葉子節(jié)點,用于沒有子節(jié)點的widget,通常基礎(chǔ)組件都屬于這一類,如Image。

  • 2、SingleChildRenderObjectWidget:包含一個子Widget,如:ConstrainedBox、DecoratedBox等

  • 3、MultiChildRenderObjectWidget:包含多個子Widget,一般都有一個children參數(shù),接受一個Widget數(shù)組。如Row、Column、Stack等

布局類組件就是指直接或間接繼承(包含)MultiChildRenderObjectWidget的Widget

線性布局(Row和Column)

所謂線性布局,即指沿水平或垂直方向排布子組件。Flutter中通過Row和Column來實現(xiàn)線性布局,類似于Android中的LinearLayout控件。Row和Column都繼承自Flex,我們將在彈性布局一節(jié)中詳細介紹Flex。

主軸和縱軸

對于線性布局,有主軸和縱軸之分,如果布局是沿水平方向,那么主軸就是指水平方向,而縱軸即垂直方向;如果布局沿垂直方向,那么主軸就是指垂直方向,而縱軸就是水平方向

Row

Row可以在水平方向排列其子widget

Row({
  ...  
  TextDirection textDirection,    
  MainAxisSize mainAxisSize = MainAxisSize.max,    
  MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
  VerticalDirection verticalDirection = VerticalDirection.down,  
  CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
  List<Widget> children = const <Widget>[],
})

  • children :子組件數(shù)組。

  • textDirection:表示水平方向子組件的布局順序(是從左往右還是從右往左),默認為從左往右,而阿拉伯語是從右往左。

  • mainAxisSize:表示Row在主軸(水平)方向占用的空間,默認是MainAxisSize.max,表示盡可能多的占用水平方向的空間;而MainAxisSize.min表示盡可能少的占用水平空間;

  • mainAxisAlignment:表示子組件在Row所占用的水平空間內(nèi)對齊方式(包含start、end、 center三個值),如果mainAxisSize值為MainAxisSize.min,則此屬性無意義。默認start。注意:textDirection是mainAxisAlignment的參考系。

  • verticalDirection:表示Row縱軸(垂直)的對齊方向,默認是VerticalDirection.down,表示從上到下。

  • crossAxisAlignment:表示子組件在縱軸方向的對齊方式,Row的高度等于子組件中最高的子元素高度,它的取值和MainAxisAlignment一樣(包含start、end、 center三個值),不同的是crossAxisAlignment的參考系是verticalDirection;

Column

Column可以在垂直方向排列其子widget

參數(shù)和Row一樣,不同的是布局方向為垂直,不同的是布局方向為垂直,主軸縱軸正好相反。

特殊情況

如果Row里面嵌套Row,或者Column里面再嵌套Column,那么只有最外面的Row或Column會占用盡可能大的空間,里面Row或Column所占用的空間為實際大小,若想內(nèi)部占用盡可能空間可以使用Expanded 組件。

彈性布局(Flex)

彈性布局允許子組件按照一定比例來分配父容器空間。彈性布局的概念在其它UI系統(tǒng)中也都存在,如H5中的彈性盒子布局,Android中的FlexboxLayout等。Flutter中的彈性布局主要通過FlexExpanded來配合實現(xiàn)。

Flex

Flex組件可以沿著水平或垂直方向排列子組件,如果你知道主軸方向,使用Row或Column會方便一些,因為Row和Column都繼承自Flex,參數(shù)基本相同,所以能使用Flex的地方基本上都可以使用Row或Column。

Flex({
  ...
  @required this.direction, //彈性布局的方向, Row默認為水平方向,Column默認為垂直方向
  List<Widget> children = const <Widget>[],
})

  • direction:Axis.vertical、Axis.horizontal

Expanded

可以按比例“擴伸” Row、Column和Flex子組件所占用的空間。

const Expanded({
  int flex = 1, 
  @required Widget child,
})

flex參數(shù)為彈性系數(shù),如果為0或null,則child是沒有彈性的,即不會被擴伸占用的空間。如果大于0,所有的Expanded按照其flex的比例來分割主軸的全部空閑空間。

Spacer

Spacer的功能是占用指定比例的空間,實際上它只是Expanded的一個包裝類

流式布局

在介紹Row和Colum時,如果子widget超出屏幕范圍,則會報溢出錯誤

我們把超出屏幕顯示范圍會自動折行的布局稱為流式布局。Flutter中通過Wrap和Flow來支持流式布局。

Wrap

我們可以看到Wrap的很多屬性在Row(包括Flex和Column)中也有,如direction、crossAxisAlignment、textDirection、verticalDirection等,這些參數(shù)意義是相同的。Wrap和Flex(包括Row和Column)除了超出顯示范圍后Wrap會折行外,其它行為基本相同。下面我們看一下Wrap特有的幾個屬性:

  • spacing:主軸方向子widget的間距

  • runSpacing:縱軸方向的間距

  • alignment: 主軸方向的對齊方式

  • runAlignment:縱軸方向的對齊方式

Wrap({
  ...
  this.direction = Axis.horizontal,
  this.alignment = WrapAlignment.start,
  this.spacing = 0.0,
  this.runAlignment = WrapAlignment.start,
  this.runSpacing = 0.0,
  this.crossAxisAlignment = WrapCrossAlignment.start,
  this.textDirection,
  this.verticalDirection = VerticalDirection.down,
  List<Widget> children = const <Widget>[],
})

Flow

我們一般很少會使用Flow,因為其過于復(fù)雜,需要自己實現(xiàn)子widget的位置轉(zhuǎn)換,在很多場景下首先要考慮的是Wrap是否滿足需求。

  • 性能好

  • 靈活

  • 使用復(fù)雜

  • 不能自適應(yīng)子組件大小,必須通過指定父容器大小或?qū)崿F(xiàn)TestFlowDelegate的getSize返回固定大小

總結(jié):復(fù)雜,先知道有這么個布局方式

層疊布局Stack、Positioned

層疊布局和Web中的絕對定位、Android中的Frame布局是相似的,子組件可以根據(jù)距父容器四個角的位置來確定自身的位置。絕對定位允許子組件堆疊起來(按照代碼中聲明的順序)

Flutter中使用StackPositioned兩個組件來配合實現(xiàn)絕對定位。Stack允許子組件堆疊,而Positioned用于根據(jù)Stack的四個角來確定子組件的位置。

Stack

Stack({
  this.alignment = AlignmentDirectional.topStart,
  this.textDirection,
  this.fit = StackFit.loose,
  this.overflow = Overflow.clip,
  List<Widget> children = const <Widget>[],
})

  • textDirection:和Row、Wrap的textDirection功能一樣,都用于確定alignment對齊的參考系。

  • alignment:此參數(shù)決定如何去對齊沒有定位(沒有使用Positioned)或部分定位的子組件。所謂部分定位,在這里特指沒有在某一個軸上定位:left、right為橫軸,top、bottom為縱軸,只要包含某個軸上的一個定位屬性就算在該軸上有定位。

  • fit:此參數(shù)用于確定沒有定位的子組件如何去適應(yīng)Stack的大小。StackFit.loose表示使用子組件的大小,StackFit.expand表示擴伸到Stack的大小。

Positioned

const Positioned({
  Key key,
  this.left, 
  this.top,
  this.right,
  this.bottom,
  this.width,
  this.height,
  @required Widget child,
})
  • left、top 、right、 bottom分別代表離Stack左、上、右、底四邊的距離

  • width、height 表示寬高

這里和約束布局一樣,如確定left、top、height、width或確定left、top 、right、 bottom及可確定一個組件的位置和大小

Stack子元素是堆疊的,所以后添加的widget在最上層

對齊與相對定位

Align

Align 組件可以調(diào)整子組件的位置,并且可以根據(jù)子組件的寬高來確定自身的的寬高,定義如下:

Align({
  Key key,
  this.alignment = Alignment.center,
  this.widthFactor,
  this.heightFactor,
  Widget child,
})

  • alignment : 需要一個AlignmentGeometry類型的值,表示子組件在父組件中的起始位置,AlignmentGeometry 是一個抽象類,它有兩個常用的子類:AlignmentFractionalOffset

  • widthFactorheightFactor是用于確定Align 組件本身寬高的屬性;它們是兩個縮放因子,會分別乘以子元素的寬、高,最終的結(jié)果就是Align 組件的寬高。如果值為null,則組件的寬高將會占用盡可能多的空間。

Alignment

Alignment繼承自AlignmentGeometry,表示矩形內(nèi)的一個點,他有兩個屬性x、y,分別表示在水平和垂直方向的偏移,Alignment定義如下:

Alignment(this.x, this.y)
  • Alignment Widget會以矩形的中心點作為坐標原點,即Alignment(0.0, 0.0)

  • x、y的值從-1到1分別代表矩形左邊到右邊的距離和頂部到底邊的距離

  • Alignment(-1.0, -1.0) 代表矩形的左側(cè)頂點,而Alignment(1.0, 1.0)代表右側(cè)底部終點,即Alignment.topRight

  • Alignment可以通過其坐標轉(zhuǎn)換公式將其坐標轉(zhuǎn)為子元素的具體偏移坐標。

(Alignment.x*childWidth/2+childWidth/2, Alignment.y*childHeight/2+childHeight/2)

其中childWidth為子元素的寬度,childHeight為子元素高度。

FractionalOffset

FractionalOffset 繼承自 Alignment,它和 Alignment唯一的區(qū)別就是坐標原點不同!FractionalOffset 的坐標原點為矩形的左側(cè)頂點,這和布局系統(tǒng)的一致,所以理解起來會比較容易。FractionalOffset的坐標轉(zhuǎn)換公式為:

實際偏移 = (FractionalOffse.x * childWidth, FractionalOffse.y * childHeight)

Align和Stack對比

Align和Stack/Positioned都可以用于指定子元素相對于父元素的偏移,但它們還是有兩個主要區(qū)別:

  • 1、定位參考系統(tǒng)不同;Stack/Positioned定位的的參考系可以是父容器矩形的四個頂點;而Align則需要先通過alignment 參數(shù)來確定坐標原點,不同的alignment會對應(yīng)不同原點,最終的偏移是需要通過alignment的轉(zhuǎn)換公式來計算出。

  • 2、Stack可以有多個子元素,并且子元素可以堆疊,而Align只能有一個子元素,不存在堆疊。

Center

Center組件其實是對齊方式確定(Alignment.center)了的Align

四、容器類組件

容器類Widget和布局類Widget都作用于其子Widget,不同的是:

  • 布局類Widget一般都需要接收一個widget數(shù)組(children),他們直接或間接繼承自(或包含)MultiChildRenderObjectWidget ;而容器類Widget一般只需要接收一個子Widget(child),他們直接或間接繼承自(或包含)SingleChildRenderObjectWidget。

  • 布局類Widget是按照一定的排列方式來對其子Widget進行排列;而容器類Widget一般只是包裝其子Widget,對其添加一些修飾(補白或背景色等)、變換(旋轉(zhuǎn)或剪裁等)、或限制(大小等)。

填充(Padding)

Padding可以給其子節(jié)點添加填充(留白),和邊距效果類似。我們在前面很多示例中都已經(jīng)使用過它了,現(xiàn)在來看看它的定義:

Padding({
  ...
  EdgeInsetsGeometry padding,
  Widget child,
})

EdgeInsetsGeometry是一個抽象類,開發(fā)中,我們一般都使用EdgeInsets類,它是EdgeInsetsGeometry的一個子類,定義了一些設(shè)置填充的便捷方法。

EdgeInsets

我們看看EdgeInsets提供的便捷方法:

  • fromLTRB(double left, double top, double right, double bottom):分別指定四個方向的填充。

  • all(double value) : 所有方向均使用相同數(shù)值的填充。

  • only({left, top, right ,bottom }):可以設(shè)置具體某個方向的填充(可以同時指定多個方向)。

  • symmetric({ vertical, horizontal }):用于設(shè)置對稱方向的填充,vertical指top和bottom,horizontal指left和right。

尺寸限制類容器

尺寸限制類容器用于限制容器大小,F(xiàn)lutter中提供了多種這樣的容器,如ConstrainedBoxSizedBoxUnconstrainedBoxAspectRatio等,本節(jié)將介紹一些常用的。

ConstrainedBox

ConstrainedBox用于對子組件添加額外的約束。例如,如果你想讓子組件的最小高度是80像素,你可以使用const BoxConstraints(minHeight: 80.0)作為子組件的約束。

實現(xiàn)一個最小高度為50,寬度盡可能大的容器

ConstrainedBox(
  constraints: BoxConstraints(
    minWidth: double.infinity, //寬度盡可能大
    minHeight: 50.0 //最小高度為50像素
  ),
  child: Container(
      height: 5.0, 
      color: Colors.red,
  ),
)

BoxConstraints用于設(shè)置限制條件,它的定義如下:

const BoxConstraints({
  this.minWidth = 0.0, //最小寬度
  this.maxWidth = double.infinity, //最大寬度
  this.minHeight = 0.0, //最小高度
  this.maxHeight = double.infinity //最大高度
})

BoxConstraints還定義了一些便捷的構(gòu)造函數(shù),用于快速生成特定限制規(guī)則的BoxConstraints,如BoxConstraints.tight(Size size),它可以生成給定大小的限制;const BoxConstraints.expand()可以生成一個盡可能大的用以填充另一個容器的BoxConstraints。除此之外還有一些其它的便捷函數(shù),讀者可以查看API文檔 (opens new window)。

SizedBox

SizedBox用于給子元素指定固定的寬高,如:

SizedBox(
  width: 80.0,
  height: 80.0,
  child: redBox
)

實際上SizedBox只是ConstrainedBox的一個定制,上面代碼等價于:

ConstrainedBox(
  constraints: BoxConstraints.tightFor(width: 80.0,height: 80.0),
  child: redBox, 
)

而BoxConstraints.tightFor(width: 80.0,height: 80.0)等價于:

BoxConstraints(minHeight: 80.0,maxHeight: 80.0,minWidth: 80.0,maxWidth: 80.0)

多重限制

如果某一個組件有多個父級ConstrainedBox限制,那么最終會是哪個生效?我們看一個例子:

ConstrainedBox(
    constraints: BoxConstraints(minWidth: 60.0, minHeight: 60.0), //父
    child: ConstrainedBox(
      constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),//子
      child: redBox,
    )
)

最終顯示效果是寬90,高60,也就是說是子ConstrainedBox的minWidth生效,而minHeight是父ConstrainedBox生效。單憑這個例子,我們還總結(jié)不出什么規(guī)律,我們將上例中父子限制條件換一下:

ConstrainedBox(
    constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),
    child: ConstrainedBox(
      constraints: BoxConstraints(minWidth: 60.0, minHeight: 60.0),
      child: redBox,
    )
)

最終的顯示效果仍然是90,高60

結(jié)論:有多重限制時,對于minWidth和minHeight來說,是取父子中相應(yīng)數(shù)值較大的。

思考題:對于maxWidth和maxHeight,多重限制的策略是什么樣的呢?

結(jié)論:有多重限制時,對于maxWidth和maxHeight來說,是取父子中相應(yīng)數(shù)值較小的。

UnconstrainedBox

UnconstrainedBox不會對子組件產(chǎn)生任何限制,它允許其子組件按照其本身大小繪制。一般情況下,我們會很少直接使用此組件,但在"去除"多重限制的時候也許會有幫助,我們看下下面的代碼:

ConstrainedBox(
    constraints: BoxConstraints(minWidth: 60.0, minHeight: 100.0),  //父
    child: UnconstrainedBox( //“去除”父級限制
      child: ConstrainedBox(
        constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),//子
        child: redBox,
      ),
    )
)

上面代碼中,如果沒有中間的UnconstrainedBox,那么根據(jù)上面所述的多重限制規(guī)則,那么最終將顯示一個90×100的紅色框。但是由于UnconstrainedBox “去除”了父ConstrainedBox的限制,則最終會按照子ConstrainedBox的限制來繪制redBox,即90×20

裝飾容器DecoratedBox

DecoratedBox可以在其子組件繪制前(或后)繪制一些裝飾(Decoration),如背景、邊框、漸變等。DecoratedBox定義如下:

const DecoratedBox({
  Decoration decoration,
  DecorationPosition position = DecorationPosition.background,
  Widget child
})

  • decoration:代表將要繪制的裝飾,它的類型為Decoration。Decoration是一個抽象類,它定義了一個接口 createBoxPainter(),子類的主要職責(zé)是需要通過實現(xiàn)它來創(chuàng)建一個畫筆,該畫筆用于繪制裝飾。

  • position:此屬性決定在哪里繪制Decoration,它接收DecorationPosition的枚舉類型,該枚舉類有兩個值:

    1、 background:在子組件之后繪制,即背景裝飾

    2、foreground:在子組件之上繪制,即前景。

BoxDecoration

我們通常會直接使用BoxDecoration類,它是一個Decoration的子類,實現(xiàn)了常用的裝飾元素的繪制。

BoxDecoration({
  Color color, //顏色
  DecorationImage image,//圖片
  BoxBorder border, //邊框
  BorderRadiusGeometry borderRadius, //圓角
  List<BoxShadow> boxShadow, //陰影,可以指定多個
  Gradient gradient, //漸變
  BlendMode backgroundBlendMode, //背景混合模式
  BoxShape shape = BoxShape.rectangle, //形狀
})

變換(Transform)

Transform可以在其子組件繪制時對其應(yīng)用一些矩陣變換來實現(xiàn)一些特效。

平移

Transform.translate接收一個offset參數(shù),可以在繪制時沿x、y軸對子組件平移指定的距離。

  Widget _createTransform_translate() {
    return Center(
      child: Transform.translate(
        offset: Offset(10, 100),
        child: Text(
          "哈哈哈哈哈哈",
          style: TextStyle(
            backgroundColor: Colors.grey,
          ),
        ),
      ),
    );
  }
旋轉(zhuǎn)

Transform.rotate可以對子組件進行旋轉(zhuǎn)變換,如:

  Widget _createTransform_rotate() {
    return Center(
      child: Transform.rotate(
        angle: pi / 2,
        child: Text(
          "哈哈哈哈哈哈",
          style: TextStyle(
            backgroundColor: Colors.grey,
          ),
        ),
      ),
    );
  }
縮放

Transform.scale可以對子組件進行縮小或放大,如:

  Widget _createTransform_scale() {
    return Row(
      children: [
        Text(
          "哈哈哈哈哈哈",
          style: TextStyle(
            backgroundColor: Colors.grey,
          ),
        ),
        Transform.scale(
          scale: 2,
          child: Text(
            "嘿嘿嘿嘿嘿",
            style: TextStyle(
              backgroundColor: Colors.red,
            ),
          ),
        ),
      ],
    );
  }

由于使用transform只會在繪制時放大,其占用空間依然為原來部分,所以會出現(xiàn)重合現(xiàn)象。

由于矩陣變化只會作用在繪制階段,所以在某些場景下,在UI需要變化時,可以直接通過矩陣變化來達到視覺上的UI改變,而不需要去重新觸發(fā)build流程,這樣會節(jié)省layout的開銷,所以性能會比較好。如之前介紹的Flow組件,它內(nèi)部就是用矩陣變換來更新UI,除此之外,F(xiàn)lutter的動畫組件中也大量使用了Transform以提高性能。

RotatedBox

RotatedBox和Transform.rotate功能相似,它們都可以對子組件進行旋轉(zhuǎn)變換,但是有一點不同:RotatedBox的變換是在layout階段,會影響在子組件的位置和大小。

  Widget _createRotatedBox() {
    return Padding(
      padding: EdgeInsets.symmetric(vertical: 100),
      child: Row(
        children: [
          Text(
            "哈哈哈哈哈哈",
            style: TextStyle(
              backgroundColor: Colors.grey,
            ),
          ),
          RotatedBox(
            quarterTurns: 1,
            child: Text(
              "嘿嘿嘿嘿嘿",
              style: TextStyle(
                backgroundColor: Colors.red,
              ),
            ),
          ),
        ],
      ),
    );
  }

Container

Container是一個組合類容器,它本身不對應(yīng)具體的RenderObject,它是DecoratedBox、ConstrainedBox、Transform、Padding、Align等組件組合的一個多功能容器,所以我們只需通過一個Container組件可以實現(xiàn)同時需要裝飾、變換、限制的場景。下面是Container的定義:

Container({
  this.alignment,
  this.padding, //容器內(nèi)補白,屬于decoration的裝飾范圍
  Color color, // 背景色
  Decoration decoration, // 背景裝飾
  Decoration foregroundDecoration, //前景裝飾
  double width,//容器的寬度
  double height, //容器的高度
  BoxConstraints constraints, //容器大小的限制條件
  this.margin,//容器外補白,不屬于decoration的裝飾范圍
  this.transform, //變換
  this.child,
})

  • 容器的大小可以通過width、height屬性來指定,也可以通過constraints來指定;如果它們同時存在時,width、height優(yōu)先。實際上Container內(nèi)部會根據(jù)width、height來生成一個constraints。

  • color和decoration是互斥的,如果同時設(shè)置它們則會報錯!實際上,當指定color時,Container內(nèi)會自動創(chuàng)建一個decoration。

Padding和Margin

接下來我們來研究一下Container組件margin和padding屬性的區(qū)別:

Container(
  margin: EdgeInsets.all(20.0), //容器外補白
  color: Colors.orange,
  child: Text("Hello world!"),
),
Container(
  padding: EdgeInsets.all(20.0), //容器內(nèi)補白
  color: Colors.orange,
  child: Text("Hello world!"),
),

總結(jié):margin在容器外留白,padding是容器內(nèi)留白

事實上,Container內(nèi)margin和padding都是通過Padding 組件來實現(xiàn)的,上面的示例代碼實際上等價于:

...
Padding(
  padding: EdgeInsets.all(20.0),
  child: DecoratedBox(
    decoration: BoxDecoration(color: Colors.orange),
    child: Text("Hello world!"),
  ),
),
DecoratedBox(
  decoration: BoxDecoration(color: Colors.orange),
  child: Padding(
    padding: const EdgeInsets.all(20.0),
    child: Text("Hello world!"),
  ),
),
 

Scaffold

一個完整的路由頁可能會包含導(dǎo)航欄、抽屜菜單(Drawer)以及底部Tab導(dǎo)航菜單等。如果每個路由頁面都需要開發(fā)者自己手動去實現(xiàn)這些,這會是一件非常麻煩且無聊的事。幸運的是,F(xiàn)lutter Material組件庫提供了一些現(xiàn)成的組件來減少我們的開發(fā)任務(wù)。Scaffold是一個路由頁的骨架,我們使用它可以很容易地拼裝出一個完整的頁面。

它主要包括:

  • appBar:導(dǎo)航欄
  • drawer:抽屜
  • bottomNavigationBar:底部導(dǎo)航
  • floatingActionButton:懸浮按鈕
  • body

AppBar

AppBar是一個Material風(fēng)格的導(dǎo)航欄,通過它可以設(shè)置導(dǎo)航欄標題、導(dǎo)航欄菜單、導(dǎo)航欄底部的Tab標題等。下面我們看看AppBar的定義:

AppBar({
  Key key,
  this.leading, //導(dǎo)航欄最左側(cè)Widget,常見為抽屜菜單按鈕或返回按鈕。
  this.automaticallyImplyLeading = true, //如果leading為null,是否自動實現(xiàn)默認的leading按鈕
  this.title,// 頁面標題
  this.actions, // 導(dǎo)航欄右側(cè)菜單
  this.bottom, // 導(dǎo)航欄底部菜單,通常為Tab按鈕組
  this.elevation = 4.0, // 導(dǎo)航欄陰影
  this.centerTitle, //標題是否居中 
  this.backgroundColor,
  ...   //其它屬性見源碼注釋
})

抽屜菜單Drawer

Scaffold的drawer和endDrawer屬性可以分別接受一個Widget來作為頁面的左、右抽屜菜單。如果開發(fā)者提供了抽屜菜單,那么當用戶手指從屏幕左(或右)側(cè)向里滑動時便可打開抽屜菜單。

return Scaffold(
  appBar: createAppBar(),
  endDrawer: Drawer(
    child: Container(
      color: Colors.red,
    ),
  ),
);

底部Tab導(dǎo)航欄

我們可以通過Scaffold的bottomNavigationBar屬性來設(shè)置底部導(dǎo)航,如本節(jié)開始示例所示,我們通過Material組件庫提供的BottomNavigationBar和BottomNavigationBarItem兩種組件來實現(xiàn)Material風(fēng)格的底部導(dǎo)航欄。

  bottomNavigationBar: BottomNavigationBar(
    items: [
      BottomNavigationBarItem(
        icon: Icon(Icons.home),
        label: "首頁",
      ),
      BottomNavigationBarItem(
        icon: Icon(Icons.history),
        label: "歷史",
      ),
      BottomNavigationBarItem(
        icon: Icon(Icons.search),
        label: "搜索",
      ),
    ],
    currentIndex: selectedIndex,
    onTap: _itemTapAction,
  ),

實現(xiàn)中間按鈕浮動效果

  floatingActionButton: FloatingActionButton(onPressed: null),
  floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
  bottomNavigationBar: BottomAppBar(
    color: Colors.white,
    shape: CircularNotchedRectangle(), // 底部導(dǎo)航欄打一個圓形的洞
    child: Row(
      children: [
        IconButton(
          onPressed: null,
          icon: Icon(Icons.home),
        ),
        SizedBox(), //中間位置空出
        IconButton(
          onPressed: null,
          icon: Icon(Icons.business),
        ),
      ],
      mainAxisAlignment: MainAxisAlignment.spaceAround, //均分底部導(dǎo)航欄橫向空間
    ),
  ),

剪裁(Clip)

Flutter中提供了一些剪裁函數(shù),用于對組件進行剪裁。

  • ClipOval:子組件為正方形時剪裁為內(nèi)貼圓形,為矩形時,剪裁為內(nèi)貼橢圓

  • ClipRRect:將子組件剪裁為圓角矩形

  • ClipRect:剪裁子組件到實際占用的矩形大小(溢出部分剪裁)

  Widget _createClip() {
    const netWorkImage = NetworkImage(
        "https://book.flutterchina.club/assets/img/3-17.a063365a.png");
    return Column(
      children: [
        Padding(
          padding: EdgeInsets.all(10.0),
          //裁剪為圓形
          child: ClipOval(
            child: SizedBox(
              height: 100,
              width: 100,
              child: DecoratedBox(
                decoration: BoxDecoration(color: Colors.blue),
              ),
            ),
          ),
        ),
        Padding(
          padding: EdgeInsets.all(10.0),
          //裁剪為圓角
          child: ClipRRect(
            borderRadius: BorderRadius.circular(10.0),
            child: SizedBox(
              height: 100,
              width: 100,
              child: DecoratedBox(
                decoration: BoxDecoration(color: Colors.blue),
              ),
            ),
          ),
        ),
        Padding(
          padding: EdgeInsets.all(10.0),
          //裁剪為圓角
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              ClipRect(
                //將溢出部分剪裁
                child: Align(
                  alignment: Alignment.topLeft,
                  widthFactor: .5, //寬度設(shè)為原來寬度一半
                  child: Image(
                    image: netWorkImage,
                  ),
                ),
              ),
              Text(
                "你好世界",
                style: TextStyle(color: Colors.green),
              )
            ],
          ),
        ),
      ],
    );
  }

CustomClipper

如果我們想剪裁子組件的特定區(qū)域,如果我們只想截取圖片中部40×30像素的范圍應(yīng)該怎么做?這時我們可以使用CustomClipper來自定義剪裁區(qū)域,實現(xiàn)代碼如下:
首先,自定義一個CustomClipper:

class MyClipper extends CustomClipper<Rect> {
  @override
  Rect getClip(Size size) => Rect.fromLTWH(10.0, 15.0, 40.0, 30.0);

  @override
  bool shouldReclip(CustomClipper<Rect> oldClipper) => false;
}

  • getClip()是用于獲取剪裁區(qū)域的接口,由于圖片大小是60×60,我們返回剪裁區(qū)域為Rect.fromLTWH(10.0, 15.0, 40.0, 30.0),即圖片中部40×30像素的范圍。

  • shouldReclip() 接口決定是否重新剪裁。如果在應(yīng)用中,剪裁區(qū)域始終不會發(fā)生變化時應(yīng)該返回false,這樣就不會觸發(fā)重新剪裁,避免不必要的性能開銷。如果剪裁區(qū)域會發(fā)生變化(比如在對剪裁區(qū)域執(zhí)行一個動畫),那么變化后應(yīng)該返回true來重新執(zhí)行剪裁。

然后,我們通過ClipRect來執(zhí)行剪裁,為了看清圖片實際所占用的位置,我們設(shè)置一個紅色背景:

DecoratedBox(
  decoration: BoxDecoration(
    color: Colors.red
  ),
  child: ClipRect(
      clipper: MyClipper(), //使用自定義的clipper
      child: avatar
  ),
)

可以看到我們的剪裁成功了,但是圖片所占用的空間大小仍然是60×60(紅色區(qū)域),這是因為剪裁是在layout完成后的繪制階段進行的,所以不會影響組件的大小,這和Transform原理是相似的。

五、路由管理

路由(Route)在移動開發(fā)中通常指頁面(Page),這跟web開發(fā)中單頁應(yīng)用的Route概念意義是相同的,Route在Android中通常指一個Activity,在iOS中指一個ViewController。所謂路由管理,就是管理頁面之間如何跳轉(zhuǎn),通常也可被稱為導(dǎo)航管理。Flutter中的路由管理和原生開發(fā)類似,無論是Android還是iOS,導(dǎo)航管理都會維護一個路由棧,路由入棧(push)操作對應(yīng)打開一個新頁面,路由出棧(pop)操作對應(yīng)頁面關(guān)閉操作,而路由管理主要是指如何來管理路由棧。

MaterialPageRoute

MaterialPageRoute繼承自PageRoute類,PageRoute類是一個抽象類,表示占有整個屏幕空間的一個模態(tài)路由頁面,它還定義了路由構(gòu)建及切換時過渡動畫的相關(guān)接口及屬性。MaterialPageRoute 是Material組件庫提供的組件,它可以針對不同平臺,實現(xiàn)與平臺頁面切換動畫風(fēng)格一致的路由切換動畫:

  • 對于Android,當打開新頁面時,新的頁面會從屏幕底部滑動到屏幕頂部;當關(guān)閉頁面時,當前頁面會從屏幕頂部滑動到屏幕底部后消失,同時上一個頁面會顯示到屏幕上。

  • 對于iOS,當打開頁面時,新的頁面會從屏幕右側(cè)邊緣一致滑動到屏幕左邊,直到新頁面全部顯示到屏幕上,而上一個頁面則會從當前屏幕滑動到屏幕左側(cè)而消失;當關(guān)閉頁面時,正好相反,當前頁面會從屏幕右側(cè)滑出,同時上一個頁面會從屏幕左側(cè)滑入。

MaterialPageRoute 構(gòu)造函數(shù)的各個參數(shù):

  MaterialPageRoute({
    WidgetBuilder builder,
    RouteSettings settings,
    bool maintainState = true,
    bool fullscreenDialog = false,
  })

  • builder 是一個WidgetBuilder類型的回調(diào)函數(shù),它的作用是構(gòu)建路由頁面的具體內(nèi)容,返回值是一個widget。我們通常要實現(xiàn)此回調(diào),返回新路由的實例。

  • settings 包含路由的配置信息,如路由名稱、是否初始路由(首頁)。

  • maintainState:默認情況下,當入棧一個新路由時,原來的路由仍然會被保存在內(nèi)存中,如果想在路由沒用的時候釋放其所占用的所有資源,可以設(shè)置maintainState為false。

  • fullscreenDialog表示新的路由頁面是否是一個全屏的模態(tài)對話框,在iOS中,如果fullscreenDialog為true,新頁面將會從屏幕底部滑入(而不是水平方向)。

Navigator

Navigator是一個路由管理的組件,它提供了打開和退出路由頁方法。Navigator通過一個棧來管理活動路由集合。通常當前屏幕顯示的頁面就是棧頂?shù)穆酚伞avigator提供了一系列方法來管理路由棧,在此我們只介紹其最常用的兩個方法:

Future push(BuildContext context, Route route)

將給定的路由入棧(即打開新的頁面),返回值是一個Future對象,用以接收新路由出棧(即關(guān)閉)時的返回數(shù)據(jù)。

bool pop(BuildContext context, [ result ])

將棧頂路由出棧,result為頁面關(guān)閉時返回給上一個頁面的數(shù)據(jù)。

Navigator 還有很多其它方法,如Navigator.replace、Navigator.popUntil等,詳情請參考API文檔或SDK源碼注釋。

實例方法

Navigator類中第一個參數(shù)為context的靜態(tài)方法都對應(yīng)一個Navigator的實例方法, 比如Navigator.push(BuildContext context, Route route)等價于Navigator.of(context).push(Route route)

路由傳值

很多時候,在路由跳轉(zhuǎn)時我們需要帶一些參數(shù),并且返回上一個路由頁面時同時會帶上一個返回參數(shù)。

例:

_jumpToNextPage() {
    //跳轉(zhuǎn)返回的是一個Future
    var result =
        Navigator.of(this.context).push(MaterialPageRoute(builder: (context) {
      return RoutePageA(title: "PageA");
    }));
    //使用Future取得回調(diào)值value
    result.then((value) {
      this.setState(() {
        nextActionName = value;
      });
      print(value);
    });
}

//pop時傳回調(diào)參數(shù)
  leading: IconButton(
            icon: BackButtonIcon(),
            onPressed: () {
              Navigator.pop(context, "我是返回值");
            }),
      ),

命名路由

所謂“命名路由”(Named Route)即有名字的路由,我們可以先給路由起一個名字,然后就可以通過路由名字直接打開新的路由了,這為路由管理帶來了一種直觀、簡單的方式。

路由表

要想使用命名路由,我們必須先提供并注冊一個路由表(routing table),這樣應(yīng)用程序才知道哪個名字與哪個路由組件相對應(yīng)。其實注冊路由表就是給路由起名字,路由表的定義如下:

Map<String, WidgetBuilder> routes;

它是一個Map,key為路由的名字,是個字符串;value是個builder回調(diào)函數(shù),用于生成相應(yīng)的路由widget。我們在通過路由名字打開新路由時,應(yīng)用會根據(jù)路由名字在路由表中查找到對應(yīng)的WidgetBuilder回調(diào)函數(shù),然后調(diào)用該回調(diào)函數(shù)生成路由widget并返回。

注冊路由表

路由表的注冊方式很簡單,我們回到之前“計數(shù)器”的示例,然后在MyApp類的build方法中找到MaterialApp,添加routes屬性,代碼如下:

MaterialApp(
  title: 'Flutter Demo',
  initialRoute:"/", //名為"/"的路由作為應(yīng)用的home(首頁)
  theme: ThemeData(
    primarySwatch: Colors.blue,
  ),
  //注冊路由表
  routes:{
   "new_page":(context) => NewRoute(),
   "/":(context) => MyHomePage(title: 'Flutter Demo Home Page'), //注冊首頁路由
  } 
);

通過路由名打開新路由頁

要通過路由名稱來打開新路由,可以使用Navigator 的pushNamed方法:

Future pushNamed(BuildContext context, String routeName,{Object arguments})
命名路由參數(shù)傳遞
Navigator.of(context).pushNamed("new_page", arguments: "hi");

在路由頁通過RouteSetting對象獲取路由參數(shù):

class EchoRoute extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    //獲取路由參數(shù)  
    var args=ModalRoute.of(context).settings.arguments;
    //...省略無關(guān)代碼
  }
}

路由鉤子(onGenerateRoute)

MaterialApp有一個onGenerateRoute屬性,它在打開命名路由時可能會被調(diào)用,之所以說可能,是因為當調(diào)用Navigator.pushNamed(...)打開命名路由時,如果指定的路由名在路由表中已注冊,則會調(diào)用路由表中的builder函數(shù)來生成路由組件;如果路由表中沒有注冊,才會調(diào)用onGenerateRoute來生成路由。

可以對某些指定的路由進行攔截,有時候不想改變頁面結(jié)構(gòu),但是又想要求跳轉(zhuǎn)到這個頁面的時候可以用到

return MaterialApp(
  title: "Welcome to Flutter1",
  initialRoute: '/',
  // home: HomePage(),
  routes: RoutesMap.instance.initRoutes(context),
  onGenerateRoute: (RouteSettings settings) {
    String? routeName = settings.name;
    return MaterialPageRoute(
        fullscreenDialog: true,
        builder: (context) {
          return LoginPage();
        });
  },
);

 Navigator.pushNamed(context, "沒注冊的page");

會直接跳轉(zhuǎn)到LoginPage

其他

initialRoute:是項目的根路由,初始化的時候最先展示的頁面

onUnknownRoute(RouteFactory類型函數(shù)):在路由匹配不到的時候用到,一般都返回一個統(tǒng)一的錯誤頁面或404頁面

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容