Flutter學習之佈局、互動、動畫

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

一、前言

前一天學習了Flutter基本控制元件和基本佈局,我是覺得蠻有意思的。作為前端開發者,如何開發出好看,使用者體驗好的介面尤其重要。今天學習的方向主要有三:

  1. 加深佈局的熟練度。
  2. 學習手勢,頁面跳轉互動。
  3. 學習動畫。

二、佈局

因為我是從事Android開發,學習了Flutter之後,發現其佈局和在Android下佈局是不一樣的,Android佈局是在XML檔案下,直觀性強一點,基本是整體到區域性,首先是確定根佈局是用LinearLayout還是RelativeLayout或者是constraintLayout等。而在Flutter下,都是由Widget來拼接起來,很多時候都是Row+Column合成,我自己是在草稿上畫出用什麼Widget來拼出需求佈局,然後才去實現。

1.佈局一

直接上需求:

需求圖
很容易看出三塊豎直排列,跟WidgetColumn來實現,區域性第一行是Text,第二行是Row行,但是Row並不是都是統一樣式,多執行緒和Java深入是帶圓角背景的,下面再仔細講解,第三行是兩個文字(作者文字和時間文字),一個圖示,第一個文字很容易想到Expanded,當s時間文字和圖示擺放後,其會佔滿剩餘主軸空間。

分析佈局一

1.1.封裝TextStyle和Padding

首先我看到整個佈局下字型的顏色至少四種,有加粗和不加粗的,並且有部分加了padding,還是封裝TextStylepadding把:

    /**
     * TextStyle:封裝
     * colors:顏色
     * fontsizes:字型大小
     * isFontWeight:是否加粗
     */
    TextStyle getTextStyle(Color colors,double fontsizes,bool isFontWeight){
      return TextStyle(
        color:colors,
        fontSize: fontsizes,
        fontWeight: isFontWeight == true ? FontWeight.bold : FontWeight.normal ,
      );
    }
        /**
     * 元件加上下左右padding
     * w:所要加padding的元件
     * all:加多少padding
     */
    Widget getPadding(Widget w,double all){
      return Padding(
        child:w,
        padding:EdgeInsets.all(all),
      );
    }

    /**
     * 元件選擇性加padding
     * 這裡用了位置可選命名引數{param1,param2,...}來命名引數,也呼叫的時候可以不傳
     *
     */
    Widget getPaddingfromLTRB(Widget w,{double l,double t,double,r,double b}){
      return Padding(
        child:w,
        padding:EdgeInsets.fromLTRB(l ?? 0,t ?? 0,r ?? 0,b ?? 0),
      );
    }
複製程式碼

1.2.實現第一行

因為上面分析,整體是用Column來實現,下面實現第一行Java synchronized原理總結

    Widget ColumnWidget = Column(
      //主軸上設定居中
      mainAxisAlignment: MainAxisAlignment.center,
      //交叉軸(水平方向)設定從左開始
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        //第一行
        getPaddingfromLTRB(Text('Java synchronized原理總結',
          style: getTextStyle(Colors.black, 16,true),
        ),t:0.0),
      ],
    );
複製程式碼

1.3.實現第二行

1.3.1實現漸變圓角Text

第二行可以看到多執行緒Java深入是帶漸變效果的圓角,一看到這,我是沒有頭緒的,查了網上的資料發現Container是有設定圓角漸變屬性的:

    //抽取第二行漸變text效果
    Container getText(String text,LinearGradient linearGradient){
      return Container(
        //距離左邊距離10dp
        margin: const EdgeInsets.only(left: 10),
        //約束 相當於直接制定了該Container的寬和高,且它的優先順序要高於width和height
        constraints: new BoxConstraints.expand(
          width: 70.0, height: 30.0,),
        //文字居中
        alignment: Alignment.center,
        child: new Text(
            text,
            style:getTextStyle(Colors.white,14,false),
        ),
        decoration: new BoxDecoration(
          color: Colors.blue,
          //圓角
          borderRadius: new BorderRadius.all(new Radius.circular(6.0)),
          //新增漸變
          gradient:linearGradient,
        ),
      );

    }
複製程式碼
1.3.2.整合第二行
//第二行
    Widget rowWidget = Row(
      //主軸左邊對齊
      mainAxisAlignment: MainAxisAlignment.start,
      //交叉軸(豎直方向)居中
      crossAxisAlignment: CrossAxisAlignment.center,
      children: <Widget>[
        Text("分類:",
          style: getTextStyle(Colors.blue,14,true),

        ),
        getText("多執行緒", l1),
        getText("Java深入", l2),
      ],

    );
    
    //根Widget
    Widget ColumnWidget = Column(
      //主軸上設定居中
      mainAxisAlignment: MainAxisAlignment.center,
      //交叉軸(水平方向)設定從左開始
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        //第一行
        getPaddingfromLTRB(Text('Java synchronized原理總結',
          style: getTextStyle(Colors.black, 16,true),
        ),t:0.0),
        //第二行
        getPaddingfromLTRB(rowWidget,t:10.0),
      ],
    );

複製程式碼

1.4.實現第三行

第三行就簡單了,直接一個RowWidget,內部巢狀ExpandedTextIcon就Ok了,程式碼如下:

  //第三行
    Widget rowthreeWidget = Row(
      mainAxisAlignment: MainAxisAlignment.start,
      crossAxisAlignment: CrossAxisAlignment.center,
      children: <Widget>[
         new Expanded(
             child: Text(
                 "作者:EnjoyMoving",
                 style: getTextStyle(Colors.grey[400], 14, true),
             ),
         ),
         getPaddingfromLTRB(Text(
           '時間:2019-02-02',
           style: getTextStyle(Colors.black, 14, true),
         ), r :10.0),
         getPaddingfromLTRB(Icon(
           Icons.favorite_border,
           color:Colors.grey[400],
         ),r:0.0)
      ],
    );
複製程式碼

1.5.整體

    //根Widget
    Widget ColumnWidget = Column(
      //主軸上設定居中
      mainAxisAlignment: MainAxisAlignment.center,
      //交叉軸(水平方向)設定從左開始
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        //第一行
        getPaddingfromLTRB(Text('Java synchronized原理總結',
          style: getTextStyle(Colors.black, 16,true),
        ),t:0.0),
        //第二行
        getPaddingfromLTRB(rowWidget,t:10.0),
        //第三行
        getPaddingfromLTRB(rowthreeWidget,t:10.0),

      ],
    );
    return new Scaffold(
        appBar: new AppBar(
          title: new Text('Flutter Demo'),
        ),
        //用card裹住
        body: Card(
              child: Container(
                //高度
                height: 160.0,
                //顏色
                color: Colors.white,
                padding: EdgeInsets.all(10.0),
                child:  Center(
                  child: ColumnWidget,
                )
              ),
          ),
    );
複製程式碼

最終效果如下:

佈局一實現效果

2.佈局二

直接上電影卡片佈局,如下:

佈局二需求圖
大致把圖看了一遍,大致框架是最外層是用Row,左孩子是圖片,右孩子是Column,其孩子分為五行,最後一行主演還是用Row來實現,上分析圖:

佈局二分析圖

2.1.實現右邊圖片

//根Widget 佈局二 開始
    //右邊圖片佈局
    Widget LayoutTwoLeft = Container(
        //這次使用裁剪實現圓角矩形
        child:ClipRRect(
          //設定圓角
          borderRadius: BorderRadius.circular(4.0),
          child: Image.network(
            'https://img3.doubanio.com//view//photo//s_ratio_poster//public//p2545472803.webp',
            width: 100.0,
            height: 150.0,
            fit:BoxFit.fill,
          ),

        ),
    );
        //整體
    Widget RowWidget = Row(
      //主軸上設定居中
      mainAxisAlignment: MainAxisAlignment.start,
      //交叉軸(水平方向)設定從左開始
      crossAxisAlignment: CrossAxisAlignment.center,
      children: <Widget>[
        LayoutTwoLeft,
      ],
    );

複製程式碼

2.2.實現圓形頭像

就是用自帶的CircleAvatar這個Widget來實現:

    //右下角圓形
    CircleAvatar getCircleAvator(String image_url){
      //圓形頭像
      return CircleAvatar(
        backgroundColor: Colors.white,
        backgroundImage: NetworkImage(image_url),
      );
    }
複製程式碼

2.3.實現右邊佈局

右佈局就是用一個Column來實現,一列一列往下實現即可:

    //右佈局
    Widget LayoutTwoRightColumn = Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        //電影名稱
        Text(
          '流浪地球',
          style: getTextStyle(Colors.black, 20.0, true),
        ),

        //豆瓣評分
        Text(
          '豆瓣評分:7.9',
          style: getTextStyle(Colors.black54, 16.0, false),
        ),

        //型別
        Text(
          '型別:科幻、太空、災難',
          style:getTextStyle(Colors.black54, 16.0, false),
        ),

        //導演
        Text(
          '導演:郭帆',
          style: getTextStyle(Colors.black54, 16.0, false),
        ),

        //主演
        Container(
          margin: EdgeInsets.only(top:8.0),
          child:Row(
            children: <Widget>[
              Text('主演:'),
              //以Row從左到右排列頭像
              Row(
                children: <Widget>[
                  Container(
                    margin: EdgeInsets.only(left:2.0),
                    child: getCircleAvator('https://img3.doubanio.com//view//celebrity//s_ratio_celebrity//public//p1533348792.03.webp'),
                  ),
                  Container(
                    margin: EdgeInsets.only(left:12.0),
                    child: getCircleAvator('https://img3.doubanio.com//view//celebrity//s_ratio_celebrity//public//p1501738155.24.webp'),
                  ),
                  Container(
                    margin: EdgeInsets.only(left:12.0),
                    child: getCircleAvator('https://img3.doubanio.com//view//celebrity//s_ratio_celebrity//public//p1540619056.43.webp'),
                  ),

                ],
              ),
            ],
          ),
        ),
      ],
    );
    
    //佈局二 右佈局 用Expanded佔滿剩餘空間
    Widget LayoutTwoRightExpanded = Expanded(
      child:Container(
        //距離左佈局10
        margin:EdgeInsets.only(left:10.0),
        //高度
        height:150.0,
        child: LayoutTwoRightColumn,
      ),
    );

複製程式碼

右佈局用Expanded就是為了佔滿剩餘空間。

2.4.整合

    //整體
    Widget RowWidget = Row(
      //主軸上設定從開始方向對齊
      mainAxisAlignment: MainAxisAlignment.start,
      //交叉軸(水平方向)居中
      crossAxisAlignment: CrossAxisAlignment.center,
      children: <Widget>[
        LayoutTwoLeft,
        LayoutTwoRightExpanded,
      ],
    );
        return new Scaffold(
        appBar: new AppBar(
          title: new Text('Flutter Demo'),
        ),
        body: Card(
              child: Container(
                //alignment: Alignment(0.0, 0.0),
                height: 160.0,
                color: Colors.white,
                padding: EdgeInsets.all(10.0),
                child:  Center(
                // 佈局一
                // child: ColumnWidget,

                // 佈局二
                   child:RowWidget,
                )
              ),
          ),
      );
複製程式碼

執行效果圖如下:

佈局二實現效果圖

3.佈局三

同樣直接上需求:

需求三佈局
一看還是根佈局直接用Column,一行一行實現就可以了,這個佈局稍微簡單一點,上分析圖:

需求三佈局分析圖

3.1.實現第一行

    //佈局三開始第一行
    Widget LayoutThreeOne = Row(
       children: <Widget>[
         Expanded(
           child: Row(
             children: <Widget>[
               Text('作者:'),
               Text('HuYounger',
                  style: getTextStyle(Colors.redAccent[400], 14, false),
               ),
             ],
           )
         ),
         //收藏圖示
         getPaddingfromLTRB(Icon(Icons.favorite,color:Colors.red),r:10.0),
         //分享圖示
         Icon(Icons.share,color:Colors.black),
       ],
    );
複製程式碼

3.2.實現第三行

    //佈局三開始第三行
    Widget LayoutThreeThree = Row(
      children: <Widget>[
        Expanded(
          child: Row(
            children: <Widget>[
              Text('分類:'),
              getPaddingfromLTRB(Text('開發環境/Android',
                  style:getTextStyle(Colors.deepPurpleAccent, 14, false)),l:8.0),
            ],
          ),
        ),
        Text('釋出時間:2018-12-13'),
      ],
    );
複製程式碼

3.3.整合

 //佈局三整合
    Widget LayoutThreeColumn = Column(
      //主軸上設定居中
      mainAxisAlignment: MainAxisAlignment.center,
      //交叉軸(水平方向)設定從左開始
      crossAxisAlignment: CrossAxisAlignment.start,
      children: <Widget>[
        //第一行
        LayoutThreeOne,
        //第二行
        getPaddingfromLTRB(Text('Android Monitor使用介紹',
              style:getTextStyle(Colors.black, 18, false),
        ),t:10.0),
        //第三行
        getPaddingfromLTRB(LayoutThreeThree,t:10.0),
      ],

    );
 return new Scaffold(
        appBar: new AppBar(
          title: new Text('Flutter Demo'),
        ),
        body: Card(
              child: Container(
                //alignment: Alignment(0.0, 0.0),
                height: 160.0,
                color: Colors.white,
                padding: EdgeInsets.all(10.0),
                child:  Center(
                // 佈局一
                // child: ColumnWidget,

                // 佈局二
                // child:RowWidget,

                // 佈局三
                   child:LayoutThreeColumn,
                )
              ),
          ),
      );
    }
複製程式碼

執行效果:

佈局三效果圖

4.新增ListView

上面實現了基本的佈局,有了item後,那必須有ListView,這裡簡單模擬一下實現一下:

    return new Scaffold(
        appBar: new AppBar(
          title: new Text('Flutter Demo'),
        ),
            //ListView提供一個builder屬性
            body: ListView.builder(
                //數目
                itemCount: 20,
                //itemBuilder是一個匿名回撥函式,有兩個引數,BuildContext 和迭代器index
                //和ListView的Item項類似 迭代器從0開始 每呼叫一次這個函式,迭代器就會加1
                itemBuilder: (BuildContext context,int index){
                  return Column(
                    children: <Widget>[
                      cardWidget,
                    ],
                  );

                }),

      );
複製程式碼

發現螢幕上被20條Item項填充滿,這裡想想,把下拉重新整理和上滑載入加上,Flutter肯定會有方法的。

4.1.下拉重新整理

Flutter已經提供和原生Android一樣的重新整理元件,叫做RefreshIndicator,是MD風格的,Flutter裡面的ScrollView和子Widget都可以新增下拉重新整理,只要在子``Widget的上層包裹一層RefreshIndicator`,先看看構造方法:

  const RefreshIndicator({
    Key key,
    @required this.child,
    this.displacement = 40.0,//下拉重新整理的距離
    @required this.onRefresh,//下拉重新整理回撥方法
    this.color,              //進度指示器前景色 預設是系統主題色
    this.backgroundColor,    //背景色
    this.notificationPredicate = defaultScrollNotificationPredicate,
    this.semanticsLabel,     //小部件的標籤
    this.semanticsValue,     //載入進度
  })
複製程式碼

包裹住ListView,並且定義下拉重新整理方法:

    return new Scaffold(
        appBar: new AppBar(
          title: new Text('Flutter Demo'),
        ),
        body: RefreshIndicator(
            //ListView提供一個builder屬性
            child: ListView.builder(
                //數目
                itemCount: 20,
                //itemBuilder是一個匿名回撥函式,有兩個引數,BuildContext 和迭代器index
                //和ListView的Item項類似 迭代器從0開始 每呼叫一次這個函式,迭代器就會加1
                itemBuilder: (BuildContext context,int index){
                  return Column(
                    children: <Widget>[
                      cardWidget,
                    ],
                  );

                }),
            onRefresh: _onRefresh,),
      );
   //下拉重新整理方法
  Future<Null> _onRefresh() async {
      //寫邏輯
  }
複製程式碼

可以看到上面定義重新整理方法_onRefresh,這裡先不加任何邏輯。把根Widget繼承StatefulWidget,因為後面涉及到狀態更新:

class HomeStateful extends StatefulWidget{
  @override
  State<StatefulWidget> createState(){
    return new HomeWidget();
  }

}

class HomeWidget extends State<HomeStateful> {
  //列表要顯示的資料
  List list = new List();
  //是否正在載入 重新整理
  bool isfresh = false;
  //這個方法只會呼叫一次,在這個Widget被建立之後,必須呼叫super.initState()
  @override
  void initState(){
    super.initState();
    //初始化資料
    initData();
  }

  //延遲3秒後重新整理
  Future initData() async{
    await Future.delayed(Duration(seconds: 3),(){
      setState(() {
        //用生成器給所有元素賦初始值
        list = List.generate(20, (i){
          return i;
        });
      });
    });
  }
 }
複製程式碼

一開始先建立並初始化長度是20的List集合,ListView根據這個集合長度來構建對應數目的Item項,上面程式碼是初始化3秒後才重新整理資料,並加了標記isfresh是否載入重新整理,Scafford程式碼如下:

   //ListView Item
    Widget _itemColumn(BuildContext context,int index){
      if(index <list.length){
        return Column(
          children: <Widget>[
            cardWidget,
          ],
        );

      }

    }
    return new Scaffold(
        appBar: new AppBar(
          title: new Text('Flutter Demo'),
        ),
        body: RefreshIndicator(
            //ListView提供一個builder屬性
            child: ListView.builder(
                //集合數目
                itemCount: list.length,
                //itemBuilder是一個匿名回撥函式,有兩個引數,BuildContext 和迭代器index
                //和ListView的Item項類似 迭代器從0開始 每呼叫一次這個函式,迭代器就會加1
                itemBuilder: _itemColumn,
            ),
            onRefresh: _onRefresh,),
      );
    }
複製程式碼

下面把下拉重新整理方法邏輯簡單加一下,我這邊只是重新將集合清空,然後重新新增8條資料,只是為了看重新整理效果而兒:

      //下拉重新整理方法
  Future<Null> _onRefresh() async {
      //寫邏輯 延遲3秒後執行重新整理
      //重新整理把isfresh改為true
     isfresh = true;
     await Future.delayed(Duration(seconds: 3),(){
       setState(() {
         //資料清空再重新新增8條資料
         list.clear();
         list.addAll(List.generate(8, (i){
           return i;
         }));
       });
     });
  }
複製程式碼

為了看到重新整理效果,當重新整理的時候,因為isfresh為true,收藏圖示♥️改為紅色,否則是黑色:

 //佈局三開始第一行
    Widget LayoutThreeOne = Row(
       children: <Widget>[
         Expanded(
           child: Row(
             children: <Widget>[
               Text('作者:'),
               Text('HuYounger',
                  style: getTextStyle(Colors.redAccent[400], 14, false),
               ),
             ],
           )
         ),
         //收藏圖示 改為以下
         getPaddingfromLTRB(Icon(Icons.favorite,color:isfresh ? Colors.red : Colors.black),r:10.0),
         //分享圖示
         Icon(Icons.share,color:Colors.black),
       ],
    );
複製程式碼

效果如下:

ListView下拉重新整理

4.2.上拉載入

Flutter中載入更多的元件沒有是提供的,那就要自己來實現,我的思路是,當監聽滑到底部時,到底底部就要做載入處理。而ListViewScrollController這個屬性來控制ListView的滑動事件,在initState新增監聽是否到達底部,並且新增上拉載入更多方法:

class HomeWidget extends State<HomeStateful> {

  //ListView控制器
  ScrollController _controller = ScrollController();
  //這個方法只會呼叫一次,在這個Widget被建立之後,必須呼叫super.initState()
  @override
  void initState(){
    super.initState();
    //初始化資料
    initData();
    //新增監聽
    _controller.addListener((){
        //這裡判斷滑到底部第一個條件就可以了,加上不在重新整理和不是上滑載入
        if(_controller.position.pixels == _controller.position.maxScrollExtent){
           //滑到底部了
           _onGetMoreData();
        }
    });
  }
 }
 
 //上拉載入更多方法 每次加8條資料
  Future _onGetMoreData() async{
     print('進入上拉載入方法');
     isfresh = false;
     if(list.length <=30){
       await Future.delayed(Duration(seconds: 2),(){
         setState(() {
           //載入資料
           //這裡新增8項
             list.addAll(List.generate(8, (i){
               return i;
             }));

         });
       });

     }
  }
  
  //State刪除物件時呼叫Dispose,這是永久性 移除監聽 清理環境
  @override
  void dispose(){
    super.dispose();
    _controller.dispose();
  }
複製程式碼

最後在ListView.builde下增加controller屬性:

    return new Scaffold(
        appBar: new AppBar(
          title: new Text('Flutter Demo'),
        ),
        body: RefreshIndicator(
          onRefresh: _onRefresh,
            //ListView提供一個builder屬性
            child: ListView.builder(
                ...
                itemBuilder: _itemColumn,
                //控制器 上拉載入
                controller: _controller,
            ),
            ),
      );
複製程式碼

上面程式碼已經實現下拉載入更多,但是沒有任何互動,我們知道,軟體當上拉載入都會有提示,那下面增加一個載入更多的提示圓圈:

...
  //是否隱藏底部
  bool isBottomShow = false;
  //載入狀態
  String statusShow = '載入中...';
...  
//上拉載入更多方法
  Future _onGetMoreData() async{
     print('進入上拉載入方法');
     isBottomShow = false;
     isfresh = false;
     if(list.length <=30){
       await Future.delayed(Duration(seconds: 2),(){
         setState(() {
           //載入資料
           //這裡新增8項
             list.addAll(List.generate(8, (i){
               return i;
             }));
         });
       });
     }else{
       //假設已經沒有資料了
       await Future.delayed(Duration(seconds: 3),(){
         setState(() {
           isBottomShow = true;
         });
       });


     }

//顯示'載入更多',顯示在介面上
  Widget _GetMoreDataWidget(){
     return Center(
       child: Padding(
         padding:EdgeInsets.all(12.0),
         // Offstage就是實現載入後載入提示圓圈是否消失
         child:new Offstage(
         // widget 根據isBottomShow這個值來決定顯示還是隱藏
         offstage: isBottomShow,
           child:
           Row(
             mainAxisAlignment: MainAxisAlignment.center,
             crossAxisAlignment: CrossAxisAlignment.center,
             children: <Widget>[
               Text(
                   //根據狀態來顯示什麼
                   statusShow,
                   style:TextStyle(
                     color: Colors.grey[300],
                     fontSize: 16.0,
                   )
               ),
               //載入圓圈
               CircularProgressIndicator(
                 strokeWidth: 2.0,
               )
             ],
           ),
         )

       ),
     );
  }
複製程式碼

可以看到,上面用了OffstageWidget裡的offstage屬性來控制載入提示圓圈是否顯示,isBottomShow如果是true,載入圓圈就會消失,false就會顯示。並且statusShow來顯示載入中的狀態,然後要在集合長度加一,也就是給ListView新增尾部:

    return new Scaffold(
        appBar: new AppBar(
          title: new Text('Flutter Demo'),
        ),
        body: RefreshIndicator(
          onRefresh: _onRefresh,
            //ListView提供一個builder屬性
            child: ListView.builder(
                //數目 加上尾部載入更多list就要加1了
                itemCount: list.length + 1,
                //itemBuilder是一個匿名回撥函式,有兩個引數,BuildContext 和迭代器index
                //和ListView的Item項類似 迭代器從0開始 每呼叫一次這個函式,迭代器就會加1
                itemBuilder: _itemColumn,
                //控制器
                controller: _controller,
            ),
            ),
      );
複製程式碼

效果如下圖:

上滑載入

4.3.ListView.separated

基本還可以,把上滑載入的提示圈加上去了,做到這裡,我在想,有時候ListView並不是每一條Item養生都是一樣的,哪有沒有屬性是設定在不同位置插入不同的Item呢?答案是有的,那就是ListView.separatedListView.separated就是在Android中adapter不同型別的itemView。用法如下:

   body: new ListView.separated(
          //普通項
          itemBuilder: (BuildContext context, int index) {
            return new Text("text $index");
          },
          //插入項
          separatorBuilder: (BuildContext context, int index) {
            return new Container(height: 1.0, color: Colors.red);
          },
          //數目
          itemCount: 40),
複製程式碼

自己例子實現一下:

//ListView item 佈局二
    Widget cardWidget_two = Card(
      child: Container(
        //alignment: Alignment(0.0, 0.0),
          height: 160.0,
          color: Colors.white,
          padding: EdgeInsets.all(10.0),
          child: Center(
            // 佈局一
            child: ColumnWidget,
          )
      ),
    );

    return new Scaffold(
        appBar: new AppBar(
          title: new Text('Flutter Demo'),
        ),
        body: RefreshIndicator(
          onRefresh: _onRefresh,
            //ListView提供一個builder屬性
              child: ListView.separated(
                  itemBuilder: (BuildContext context,int index){
                    return  _itemColumn(context,index);

                  },
                  separatorBuilder: (BuildContext context,int index){
                    return Column(
                      children: <Widget>[
                        cardWidget_two
                      ],
                    );
                  },
                  itemCount: list.length + 1,
                  controller: _controller,
              ),
複製程式碼

把一開始實現的佈局一作為item插入ListView,效果如下:

ListView不同型別one
發現上面的程式碼是兩個不同型別item項互動插入在ListView中,下面試一下每隔3項才插一條試試看:

    return new Scaffold(
        appBar: new AppBar(
          title: new Text('Flutter Demo'),
        ),
        body: RefreshIndicator(
          onRefresh: _onRefresh,
            //ListView提供一個builder屬性
              child: ListView.separated(
                  itemBuilder: (BuildContext context,int index){
                    return  _itemColumn(context,index);

                  },
                  separatorBuilder: (BuildContext context,int index){
                    return Column(
                      children: <Widget>[
                        (index + 1) % 3 == 0 ? cardWidget_two : Container()
                        //cardWidget_two
                      ],
                    );
                  },
                  itemCount: list.length + 1,
                  controller: _controller,
              ),
      );
複製程式碼

效果如下:

ListView型別2

三、互動

1.自帶互動的控制元件

Flutter中,自帶如點選事件的控制元件有RaisedButtonIconButtonOutlineButtonCheckboxSnackBarSwitch等,如下面給OutlineButton新增點選事件:

         body:Center(
           child: OutlineButton(
               child: Text('點選我'),
               onPressed: (){
                 Fluttertoast.showToast(
                   msg: '你點選了FlatButton',
                   toastLength: Toast.LENGTH_SHORT,
                   gravity: ToastGravity.CENTER,
                   timeInSecForIos: 1,
                 );
               }),
         ),
複製程式碼

上面程式碼就可以捕捉OutlineButton的點選事件。

2.不自帶互動的控制元件

很多控制元件不像RaisedButtonOutlineButton等已經對presses(taps)或手勢做出了響應。那麼如果要監聽這些控制元件的手勢就需要用另一個控制元件GestureDetector,那看看原始碼GestureDetector支援哪些手勢:

  GestureDetector({
    Key key,
    this.child,
    this.onTapDown,//按下,每次和螢幕互動都會呼叫
    this.onTapUp,//抬起,停止觸控時呼叫
    this.onTap,//點選,短暫觸控螢幕時呼叫
    this.onTapCancel,//取消 觸發了onTapDown,但沒有完成onTap
    this.onDoubleTap,//雙擊,短時間內觸控螢幕兩次
    this.onLongPress,//長按,觸控時間超過500ms觸發
    this.onLongPressUp,//長按鬆開
    this.onVerticalDragDown,//觸控點開始和螢幕互動,同時豎直拖動按下
    this.onVerticalDragStart,//觸控點開始在豎直方向拖動開始
    this.onVerticalDragUpdate,//觸控點每次位置改變時,豎直拖動更新
    this.onVerticalDragEnd,//豎直拖動結束
    this.onVerticalDragCancel,//豎直拖動取消
    this.onHorizontalDragDown,//觸控點開始跟螢幕互動,並水平拖動
    this.onHorizontalDragStart,//水平拖動開始,觸控點開始在水平方向移動
    this.onHorizontalDragUpdate,//水平拖動更新,觸控點更新
    this.onHorizontalDragEnd,//水平拖動結束觸發
    this.onHorizontalDragCancel,//水平拖動取消 onHorizontalDragDown沒有成功觸發
    //onPan可以取代onVerticalDrag或者onHorizontalDrag,三者不能並存
    this.onPanDown,//觸控點開始跟螢幕互動時觸發
    this.onPanStart,//觸控點開始移動時觸發
    this.onPanUpdate,//螢幕上的觸控點位置每次改變時,都會觸發這個回撥
    this.onPanEnd,//pan操作完成時觸發
    this.onPanCancel,//pan操作取消
    //onScale可以取代onVerticalDrag或者onHorizontalDrag,三者不能並存,不能與onPan並存
    this.onScaleStart,//觸控點開始跟螢幕互動時觸發,同時會建立一個焦點為1.0
    this.onScaleUpdate,//跟螢幕互動時觸發,同時會標示一個新的焦點
    this.onScaleEnd,//觸控點不再跟螢幕互動,標示這個scale手勢完成
    this.behavior,
    this.excludeFromSemantics = false
  })
複製程式碼

這裡注意:onVerticalXXX/onHorizontalXXXonPanXXX不能同時設定,如果同時需要水平、豎直方向的移動,設定onPanXXX。直接上例子:

2.1.onTapXXX

           child: GestureDetector(
             child: Container(
               width: 300.0,
               height: 300.0,
               color:Colors.red,
             ),
             onTapDown: (d){
               print("onTapDown");
             },
             onTapUp: (d){
               print("onTapUp");
             },
             onTap:(){
               print("onTap");
             },
             onTapCancel: (){
               print("onTaoCancel");
             },
           )
複製程式碼

點了一下,並且抬起,結果是:

I/flutter (16304): onTapDown
I/flutter (16304): onTapUp
I/flutter (16304): onTap
先觸發onTapDown 然後onTapUp 繼續onTap
複製程式碼

2.2.onLongXXX

    //手勢測試
    Widget gestureTest = GestureDetector(
          child: Container(
            width: 300.0,
            height: 300.0,
            color:Colors.red,
          ),
           onDoubleTap: (){
              print("雙擊onDoubleTap");
           },
           onLongPress: (){
              print("長按onLongPress");
           },
           onLongPressUp: (){
              print("長按抬起onLongPressUP");
           },

    );
複製程式碼

實際結果:

I/flutter (16304): 長按onLongPress
I/flutter (16304): 長按抬起onLongPressUP
I/flutter (16304): 雙擊onDoubleTap
複製程式碼

2.3.onVerticalXXX

    //手勢測試
    Widget gestureTest = GestureDetector(
          child: Container(
            width: 300.0,
            height: 300.0,
            color:Colors.red,
          ),
            onVerticalDragDown: (_){
               print("豎直方向拖動按下onVerticalDragDown:"+_.globalPosition.toString());
            },
            onVerticalDragStart: (_){
               print("豎直方向拖動開始onVerticalDragStart"+_.globalPosition.toString());
            },
            onVerticalDragUpdate: (_){
               print("豎直方向拖動更新onVerticalDragUpdate"+_.globalPosition.toString());
            },
            onVerticalDragCancel: (){
               print("豎直方向拖動取消onVerticalDragCancel");
            },
            onVerticalDragEnd: (_){
               print("豎直方向拖動結束onVerticalDragEnd");
            },

    );
複製程式碼

輸出結果:

I/flutter (16304): 豎直方向拖動按下onVerticalDragDown:Offset(191.7, 289.3)
I/flutter (16304): 豎直方向拖動開始onVerticalDragStartOffset(191.7, 289.3)
I/flutter (16304): 豎直方向拖動更新onVerticalDragUpdateOffset(191.7, 289.3)
I/flutter (16304): 豎直方向拖動更新onVerticalDragUpdateOffset(191.7, 289.3)
I/flutter (16304): 豎直方向拖動更新onVerticalDragUpdateOffset(191.7, 289.3)
I/flutter (16304): 豎直方向拖動更新onVerticalDragUpdateOffset(191.7, 289.3)
I/flutter (16304): 豎直方向拖動更新onVerticalDragUpdateOffset(191.7, 289.3)
I/flutter (16304): 豎直方向拖動更新onVerticalDragUpdateOffset(191.3, 290.0)
I/flutter (16304): 豎直方向拖動更新onVerticalDragUpdateOffset(191.3, 291.3)
I/flutter (16304): 豎直方向拖動結束onVerticalDragEnd
複製程式碼

2.4.onPanXXX

    //手勢測試
    Widget gestureTest = GestureDetector(
          child: Container(
            width: 300.0,
            height: 300.0,
            color:Colors.red,
          ),
             onPanDown: (_){
                 print("onPanDown");
             },
             onPanStart: (_){
                 print("onPanStart");
             },
             onPanUpdate: (_){
                 print("onPanUpdate");
             },
             onPanCancel: (){
                 print("onPanCancel");
             },
             onPanEnd: (_){
                 print("onPanEnd");
             },

    );
複製程式碼

無論豎直拖動還是橫向拖動還是一起來,結果如下:

I/flutter (16304): onPanDown
I/flutter (16304): onPanStart
I/flutter (16304): onPanUpdate
I/flutter (16304): onPanUpdate
I/flutter (16304): onPanEnd
複製程式碼

2.5.onScaleXXX

    //手勢測試
    Widget gestureTest = GestureDetector(
          child: Container(
            width: 300.0,
            height: 300.0,
            color:Colors.red,
          ),
           onScaleStart: (_){
                 print("onScaleStart");
          },
          onScaleUpdate: (_){
                print("onScaleUpdate");
               },
          onScaleEnd: (_){
               print("onScaleEnd");

    );
複製程式碼

無論點選、豎直拖動、水平拖動,結果如下:

I/flutter (16304): onScaleStart
I/flutter (16304): onScaleUpdate
I/flutter (16304): onScaleUpdate
I/flutter (16304): onScaleUpdate
I/flutter (16304): onScaleUpdate
I/flutter (16304): onScaleUpdate
I/flutter (16304): onScaleUpdate
I/flutter (16304): onScaleUpdate
I/flutter (16304): onScaleEnd
複製程式碼

3.原始指標事件

除了GestureDetector能夠監聽觸控事件外,Pointer代表使用者與裝置螢幕互動的原始資料,也就是也能監聽手勢:

  1. PointerDownEvent:指標接觸到螢幕的特定位置
  2. PointerMoveEvent:指標從螢幕上的一個位置移動到另一個位置
  3. PointMoveEvent:指標停止接觸螢幕
  4. PointUpEvent:指標停止接觸螢幕
  5. PointerCancelEvent:指標的輸入事件不再針對此應用

上程式碼:

    //Pointer
    Widget TestContainer = Listener(
      child:Container(
        width: 300.0,
        height: 300.0,
        color:Colors.red,
    ),
      onPointerDown: (event){
        print("onPointerDown");
      },
      onPointerUp: (event){
        print("onPointerUp");
      },
      onPointerMove: (event){
        print("onPointerMove");
      },
      onPointerCancel: (event){
        print("onPointerCancel");
      },

    );
複製程式碼

在螢幕上點選,或者移動:

I/flutter (16304): onPointerDown
I/flutter (16304): onPointerMovee
I/flutter (16304): onPointerMove
I/flutter (16304): onPointerMoves
I/flutter (16304): onPointerMove
I/flutter (16304): onPointerUp
複製程式碼

發現也是可以監聽手勢的。

4.路由(頁面)跳轉

Android原生中,頁面跳轉是通過startActvity()來跳轉不同頁面,而在Flutter就不一樣。Flutter中,跳轉頁面有兩種方式:靜態路由方式和動態路由方式。在Flutter管理多個頁面有兩個核心概念和類:RouteNavigator。一個route是一個螢幕或者頁面的抽象,Navigator是管理routeWidgetNavigator可以通過route入棧和出棧來實現頁面之間的跳轉。

4.1.靜態路由

4.1.1.配置路由

在原頁面配置路由跳轉,就是在MaterialApp裡設定每個route對應的頁面,注意:一個app只能有一個材料設計(MaterialApp),不然返回上一個頁面會黑屏。程式碼如下:

//入口頁面
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      //靜態路由方式 配置初始路由
      initialRoute: '/',
      routes: {
        //預設走這個條件`/`
        '/':(context){
          return HomeStateful();
        },
        //新頁面路由
        '/mainnewroute':(context){
          return new newRoute();
        }
      },
      //主題色
      theme: ThemeData(
        //設定為紅色
          primarySwatch: Colors.red),
      //配置了初始路由,下面就不需要了
      //home: HomeStateful(),
    );
  }
}
複製程式碼

因為配置了初始路由,所以home:HomeStateful就不用配置了。

4.1.2.點選跳轉
//如果新頁面不在同一個類中,記得把它匯入
import 'mainnewroute.dart';
class HomeStateful extends StatefulWidget{
  @override
  State<StatefulWidget> createState(){
    return new HomeWidget();
  }

}

class HomeWidget extends State<HomeStateful> {
    @override
  Widget build(BuildContext context) {
   ...
       //Pointer
    Widget TestContainer = Listener(
      child:Container(
        width: 300.0,
        height: 300.0,
        color:Colors.red,
        child: RaisedButton(
            child: Text('點選我'),
            onPressed: (){
              //頁面跳轉方法
              Navigator.of(context).pushNamed('/mainnewroute');
            }),
    ),
   );
    return new Scaffold(
        appBar: new AppBar(
          title: new Text('Flutter Demo'),
        ),
         body:Center(
           child: TestContainer,
         ),
      );
 }
}
複製程式碼

RaisedButton配置了點選方法,上面用了Navigator.of(context).pushNamed('/mainnewroute'),執行到這句,路由會找routes有沒有配置/mainnewroute,有的話,就會根據配置跳到新的頁面。

4.1.3.配置新頁面

新頁面,我在lib下建立一個新的檔案(頁面)mainfourday.dart,很簡單:

import 'package:flutter/material.dart';
class newRoute extends StatelessWidget{

  @override
  Widget build(BuildContext context){
    return HomeWidget();
    //注意:不需要MaterialApp
//    return MaterialApp(
//      theme: ThemeData(
//        //設定為hongse
//          primarySwatch: Colors.red),
//      home: HomeWidget(),
//      );

  }
}

class HomeWidget extends StatelessWidget{

  @override
  Widget build(BuildContext context){
     return Scaffold(
       appBar: AppBar(
         title: Text('new Route'),
       ),
       body: Center(
         child:RaisedButton(
           child: Text('返回'),
             onPressed: (){
               //這是關閉頁面
               Navigator.pop(context);
             }),
        // child: Text('這是新的頁面'),
       ),
     );
  }
}
複製程式碼

最終效果如下:

新頁面跳轉

4.2.動態路由

下面說一下跳轉頁面的第二種方式,動態路由方式:

        child: RaisedButton(
            child: Text('點選我'),
            onPressed: (){
              //Navigator.of(context).pushNamed('/mainnewroute');
              //動態路由
              Navigator.push(
                context,
                MaterialPageRoute(builder: (newPage){
                  return new newRoute();
                }),
              );
            }),
複製程式碼

效果和上面是一樣的。

4.3.頁面傳遞資料

兩種方式都是傳遞引數的,直接上動態路由傳遞資料程式碼:

              Navigator.push(
                context,
                MaterialPageRoute(builder: (newPage){
                  return new newRoute("這是一份資料到新頁面");
                }),
              );
複製程式碼

在新頁面改為如下:


import 'package:flutter/material.dart';
class newRoute extends StatelessWidget{
  //接收上一個頁面傳遞的資料
  String str;
  //建構函式
  newRoute(this.str);

  @override
  Widget build(BuildContext context){
    return HomeWidget(str);
  }
}

class HomeWidget extends StatelessWidget{
  String newDate;
  HomeWidget(this.newDate);

  @override
  Widget build(BuildContext context){
     return Scaffold(
       appBar: AppBar(
         title: Text('new Route'),
       ),
       body: Center(
         child:RaisedButton(
           //顯示上一個頁面所傳遞的資料
           child: Text(newDate),
             onPressed: (){
               Navigator.pop(context);
             }),
        // child: Text('這是新的頁面'),
       ),
     );
  }
}
複製程式碼

靜態路由方式傳遞引數,也就是在newRoute()加上所要傳遞的引數就可以了

        //新頁面路由
        '/mainnewroute':(context){
          return new newRoute("sdsd");
        }
複製程式碼

4.4.頁面返回資料

傳遞資料給新頁面可以了,那麼怎樣將新頁面資料返回上一個頁面呢?也是很簡單,在返回方法pop加上所要返回的資料即可:

       body: Center(
         child:RaisedButton(
           //顯示上一個頁面所傳遞的資料
           child: Text(newDate),
             onPressed: (){
               Navigator.pop(context,"這是新頁面返回的資料");
             }),
        // child: Text('這是新的頁面'),
       ),
複製程式碼

因為開啟頁面是非同步的,所以頁面的結果需要通過一個Future來返回,靜態路由方式:

        child: RaisedButton(
            child: Text('點選我'),
            onPressed: () async {
              var data = await Navigator.of(context).pushNamed('/mainnewroute');
              //列印返回來的資料
              print(data);
            }),
複製程式碼

動態路由方式:

        child: RaisedButton(
            child: Text('點選我'),
            onPressed: () async {
              var data = await Navigator.push(
                context,
                MaterialPageRoute(builder: (newPage){
                  return new newRoute("這是一份資料到新頁面");
                }),
              );
              //列印返回的值
              print(data);
            }),
複製程式碼

兩者方式都是可以的。

四、動畫

Flutter動畫庫的核心類是Animation物件,它生成指導動畫的值,Animation物件指導動畫的當前狀態(例如,是開始、停止還是向前或者向後移動),但它不知道螢幕上顯示的內容。動畫型別分為兩類:

  1. 補簡動畫(Tween),定義了開始點和結束點、時間線以及定義轉換時間和速度的曲線。然後由框架計算如何從開始點過渡到結束點。Tween是一個無狀態(stateless)物件,需要begin和end值。Tween的唯一職責就是定義從輸入範圍到輸出範圍的對映。輸入範圍通常為0.0到1.0,但這不是必須的。
  2. 基於物理動畫,運動被模擬與真實世界行為相似,例如,當你擲球時,它何處落地,取決於拋球速度有多快、球有多重、距離地面有多遠。類似地,將連線在彈簧上的球落下(並彈起)與連線到繩子的球放下的方式也是不同。

Flutter中的動畫系統基於Animation物件的。widget可以在build函式中讀取Animation物件的當前值,並且可以監聽動畫的狀態改變。

1.動畫示例

import 'package:flutter/material.dart';
import 'package:flutter/animation.dart';


void main() {
  //執行程式
  runApp(LogoApp());
}

class LogoApp extends StatefulWidget{
  @override
  State<StatefulWidget> createState(){
    return new _LogoAppState();
  }

}

//logo
Widget ImageLogo = new Image(
    image: new AssetImage('images/logo.jpg'),
);

//with 是dart的關鍵字,混入的意思,將一個或者多個類的功能天驕到自己的類無需繼承這些類
//避免多重繼承問題
//SingleTickerProviderStateMixin 初始化 animation 和 Controller的時候需要一個TickerProvider型別的引數Vsync
//所依混入TickerProvider的子類
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin{
  //動畫的狀態,如動畫開啟,停止,前進,後退等
  Animation<double> animation;
  //管理者animation物件
  AnimationController controller;
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    //建立AnimationController
    //需要傳遞一個vsync引數,存在vsync時會防止螢幕外動畫(
    //譯者語:動畫的UI不在當前螢幕時)消耗不必要的資源。 通過將SingleTickerProviderStateMixin新增到類定義中,可以將stateful物件作為vsync的值。
    controller = new AnimationController(
        //時間是3000毫秒
        duration: const Duration(
            milliseconds: 3000
        ),
        //vsync 在此處忽略不必要的情況
        vsync: this,
    );
    //補間動畫
    animation = new Tween(
      //開始的值是0
      begin: 0.0,
      //結束的值是200
      end : 200.0,
    ).animate(controller)//新增監聽器
      ..addListener((){
        //動畫值在發生變化時就會呼叫
        setState(() {

        });
      });
    //只顯示動畫一次
    controller.forward();
  }
  @override
  Widget build(BuildContext context){
    return new MaterialApp(
      theme: ThemeData(
          primarySwatch: Colors.red

      ),
      home: new Scaffold(
        appBar: new AppBar(
          title: Text("動畫demo"),
        ),
        body:new Center(
          child: new Container(
            //寬和高都是根據animation的值來變化
            height: animation.value,
            width: animation.value,
            child: ImageLogo,
          ),
        ),
      ),
    );
  }


  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
    //資源釋放
    controller.dispose();
  }
  
}
複製程式碼

上面實現了影象在3000毫秒間從寬高是0變化到寬高是200,主要分為六部

  1. 混入SingleTickerProviderStateMixin,為了傳入vsync物件
  2. 初始化AnimationController物件
  3. 初始化Animation物件,並關聯AnimationController物件
  4. 呼叫AnimationControllerforward開啟動畫
  5. widget根據Animationvalue值來設定寬高
  6. widgetdispose()方法中呼叫釋放資源

最終效果如下:

動畫效果一
注意:上面建立Tween用了Dart語法的級聯符號

animation = tween.animate(controller)
          ..addListener(() {
            setState(() {
              // the animation object’s value is the changed state
            });
          });
複製程式碼

等價於下面程式碼:

animation = tween.animate(controller);
animation.addListener(() {
            setState(() {
              // the animation object’s value is the changed state
            });
          });
複製程式碼

所以還是有必要學一下Dart語法。

1.1.AnimatedWidget簡化

使用AnimatedWidget對動畫進行簡化,使用AnimatedWidget建立一個可重用動畫的widget,而不是用addListener()setState()來給widget新增動畫。AnimatedWidget類允許從setState()呼叫中的動畫程式碼中分離出widget程式碼。AnimatedWidget不需要維護一個State物件了來儲存動畫。

import 'package:flutter/material.dart';
import 'package:flutter/animation.dart';


void main() {
  //執行程式
  runApp(LogoApp());
}

class LogoApp extends StatefulWidget{
  @override
  State<StatefulWidget> createState(){
    return new _LogoAppState();
  }

}

//logo
Widget ImageLogo = new Image(
    image: new AssetImage('images/logo.jpg'),
);


//抽象出來
class AnimatedLogo extends AnimatedWidget{
  AnimatedLogo({Key key,Animation<double> animation})
     :super(key:key,listenable:animation);


  @override
  Widget build(BuildContext context){
    final Animation<double> animation = listenable;
    return new MaterialApp(
      theme: ThemeData(
          primarySwatch: Colors.red

      ),
      home: new Scaffold(
        appBar: new AppBar(
          title: Text("動畫demo"),
        ),
        body:new Center(
          child: new Container(
            //寬和高都是根據animation的值來變化
            height: animation.value,
            width: animation.value,
            child: ImageLogo,
          ),
        ),
      ),
    );

  }
}

//with 是dart的關鍵字,混入的意思,將一個或者多個類的功能新增到自己的類無需繼承這些類
//避免多重繼承問題
//SingleTickerProviderStateMixin 初始化 animation 和 Controller的時候需要一個TickerProvider型別的引數Vsync
//所依混入TickerProvider的子類
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin{
  //動畫的狀態,如動畫開啟,停止,前進,後退等
  Animation<double> animation;
  //管理者animation物件
  AnimationController controller;
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    //建立AnimationController
    //需要傳遞一個vsync引數,存在vsync時會防止螢幕外動畫(
    //譯者語:動畫的UI不在當前螢幕時)消耗不必要的資源。 通過將SingleTickerProviderStateMixin新增到類定義中,可以將stateful物件作為vsync的值。
    controller = new AnimationController(
        //時間是3000毫秒
        duration: const Duration(
            milliseconds: 3000
        ),
        //vsync 在此處忽略不必要的情況
        vsync: this,
    );
    //補間動畫
    animation = new Tween(
      //開始的值是0
      begin: 0.0,
      //結束的值是200
      end : 200.0,
    ).animate(controller);//新增監聽器
    //只顯示動畫一次
    controller.forward();
  }
  
  @override
  Widget build(BuildContext context){
      return AnimatedLogo(animation: animation);
  }


  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
    //資源釋放
    controller.dispose();
  }

}
複製程式碼

可以發現AnimatedWidget中會自動呼叫addListenersetState()_LogoAppStateAnimation物件傳遞給基類並用animation.value設定Image寬高。

1.2.監視動畫

在平時開發,我們知道,很多時候都需要監聽動畫的狀態,好像完成、前進、倒退等。在Flutter中可以通過addStatusListener()來得到這個通知,以下程式碼新增了動畫狀態

    //補間動畫
    animation = new Tween(
      //開始的值是0
      begin: 0.0,
      //結束的值是200
      end : 200.0,
    ).animate(controller)
    //新增動畫狀態
    ..addStatusListener((state){
      return print('$state');
    });//新增監聽器
複製程式碼

執行程式碼會輸出下面結果:

I/flutter (16745): AnimationStatus.forward //動畫開始
Syncing files to device KNT AL10...
I/zygote64(16745): Do partial code cache collection, code=30KB, data=25KB
I/zygote64(16745): After code cache collection, code=30KB, data=25KB
I/zygote64(16745): Increasing code cache capacity to 128KB
I/flutter (16745): AnimationStatus.completed//動畫完成
複製程式碼

下面那就運用addStatusListener()在開始或結束反轉動畫。那就產生迴圈效果:

    //補間動畫
    animation = new Tween(
      //開始的值是0
      begin: 0.0,
      //結束的值是200
      end : 200.0,
    ).animate(controller)
    //新增動畫狀態
    ..addStatusListener((state){
      //如果動畫完成了
      if(state == AnimationStatus.completed){
        //開始反向這動畫
        controller.reverse();
      } else if(state == AnimationStatus.dismissed){
        //開始向前執行著動畫
        controller.forward();
      }

    });//新增監聽器
複製程式碼

效果如下:

動畫效果圖二

1.3.用AnimatedBuilder重構

上面的程式碼存在一個問題:更改動畫需要更改顯示Imagewidget,更好的解決方案是將職責分離:

  1. 顯示影象
  2. 定義Animation物件
  3. 渲染過渡效果 這時候可以藉助AnimatedBuilder類完成此分離。AnimatedBuilder是渲染樹中的一個獨立的類,與AnimatedWidget類似,AnimatedBuilder自動監聽來自Animation物件的通知,並根據需要將該控制元件樹標記為髒(dirty),因此不需要手動呼叫addListener()
//AnimatedBuilder
class GrowTransition extends StatelessWidget{
  final Widget child;
  final Animation<double> animation;
  GrowTransition({this.child,this.animation});

  @override
  Widget build(BuildContext context){
    return new MaterialApp(
      theme: ThemeData(
          primarySwatch: Colors.red

      ),
      home: new Scaffold(
        appBar: new AppBar(
          title: Text("動畫demo"),
        ),
        body:new Center(
            child: new AnimatedBuilder(
                animation: animation,
                builder: (BuildContext context,Widget child){
                  return new Container(
                    //寬和高都是根據animation的值來變化
                    height: animation.value,
                    width: animation.value,
                    child: child,
                  );
                },
              child: child,
            ),

        ),
      ),
    );

  }
  class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin{
  //動畫的狀態,如動畫開啟,停止,前進,後退等
  Animation animation;
  //管理者animation物件
  AnimationController controller;
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    //建立AnimationController
    //需要傳遞一個vsync引數,存在vsync時會防止螢幕外動畫(
    //譯者語:動畫的UI不在當前螢幕時)消耗不必要的資源。 通過將SingleTickerProviderStateMixin新增到類定義中,可以將stateful物件作為vsync的值。
    controller = new AnimationController(
        //時間是3000毫秒
        duration: const Duration(
            milliseconds: 3000
        ),
        //vsync 在此處忽略不必要的情況
        vsync: this,
    );
    final CurvedAnimation curve  = new CurvedAnimation(parent: controller, curve: Curves.easeIn);
    //補間動畫
    animation = new Tween(
      //開始的值是0
      begin: 0.0,
      //結束的值是200
      end : 200.0,
    ).animate(curve)
//    //新增動畫狀態
    ..addStatusListener((state){
      //如果動畫完成了
      if(state == AnimationStatus.completed){
        //開始反向這動畫
        controller.reverse();
      } else if(state == AnimationStatus.dismissed){
        //開始向前執行著動畫
        controller.forward();
      }

    });//新增監聽器
    //只顯示動畫一次
    controller.forward();
  }

  @override
  Widget build(BuildContext context){
      //return AnimatedLogo(animation: animation);
        return new GrowTransition(child:ImageLogo,animation: animation);
  }


  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
    //資源釋放
    controller.dispose();
  }

}
複製程式碼

上面程式碼有一個迷惑的問題是,child看起來好像是指定了兩次,但實際發生的事情是,將外部引用的child傳遞給AnimatedBuilderAnimatedBuilder將其傳遞給匿名構造器,然後將該物件用作其子物件。最終的結果是AnimatedBuilder插入到渲染樹中的兩個Widget之間。最後,在initState()方法建立一個AnimationController和一個Tween,然後通過animate()繫結,在build方法中,返回帶有一個Image為子物件的GrowTransition物件和一個用於驅動過渡的動畫物件。如果只是想把可複用的動畫定義成一個widget,那就用AnimatedWidget

1.5.並行動畫

很多時候,一個動畫需要兩種或者兩種以上的動畫,在Flutter也是可以實現的,每一個Tween管理動畫的一種效果,如:

    final AnimationController controller =
    new AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
    final Animation<double> sizeAnimation =
    new Tween(begin: 0.0, end: 300.0).animate(controller);
    final Animation<double> opacityAnimation =
    new Tween(begin: 0.1, end: 1.0).animate(controller);
複製程式碼

可以通過sizeAnimation.Value來獲取大小,通過opacityAnimation.value來獲取不透明度,但AnimatedWidget的建構函式只能接受一個動畫物件,解決這個問題,需要動畫的widget建立了自己的Tween物件,上程式碼:

//AnimatedBuilder
class GrowTransition extends StatelessWidget {
  final Widget child;
  final Animation<double> animation;

  GrowTransition({this.child, this.animation});
  static final _opacityTween = new Tween<double>(begin: 0.1, end: 1.0);
  static final _sizeTween = new Tween<double>(begin: 0.0, end: 200.0);

  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      theme: ThemeData(primarySwatch: Colors.red),
      home: new Scaffold(
        appBar: new AppBar(
          title: Text("動畫demo"),
        ),
        body: new Center(
          child: new AnimatedBuilder(
            animation: animation,
            builder: (BuildContext context, Widget child) {
              return new Opacity(
                  opacity: _opacityTween.evaluate(animation),
                child: new Container(
                //寬和高都是根據animation的值來變化
                height: _sizeTween.evaluate(animation),
                width: _sizeTween.evaluate(animation),
                child: child,
              ),
              );

            },
            child: child,
          ),
        ),
      ),
    );
  }
}

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  //動畫的狀態,如動畫開啟,停止,前進,後退等
  Animation<double> animation;

  //管理者animation物件
  AnimationController controller;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    //建立AnimationController
    //需要傳遞一個vsync引數,存在vsync時會防止螢幕外動畫(
    //譯者語:動畫的UI不在當前螢幕時)消耗不必要的資源。 通過將SingleTickerProviderStateMixin新增到類定義中,可以將stateful物件作為vsync的值。
    controller = new AnimationController(
      //時間是3000毫秒
      duration: const Duration(milliseconds: 3000),
      //vsync 在此處忽略不必要的情況
      vsync: this,
    );
    //新增
    animation = new CurvedAnimation(parent: controller, curve: Curves.easeIn)
      ..addStatusListener((state) {
        //如果動畫完成了
        if (state == AnimationStatus.completed) {
          //開始反向這動畫
          controller.reverse();
        } else if (state == AnimationStatus.dismissed) {
          //開始向前執行著動畫
          controller.forward();
        }
      }); //新增監聽器
    //只顯示動畫一次
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
     return new GrowTransition(child:ImageLogo,animation: animation);
  }

  @override
  void dispose() {
    // TODO: implement dispose
    super.dispose();
    //資源釋放
    controller.dispose();
  }
}

複製程式碼

可以看到在GrowTransition定義兩個Tween動畫,並且加了不透明Opacitywidget,最後在initState方法中修改增加一句animation = new CurvedAnimation(parent: controller, curve: Curves.easeIn),最後的動畫效果:

並行動畫
注意:可以通過改變Curves.easeIn值來實現非線性運動效果。

2.自定義動畫

先上效果圖:

小球進度條

2.1.自定義小球

class _bollView extends CustomPainter{
  //顏色
  Color color;
  //數量
  int count;
  //集合放動畫
  List<Animation<double>> ListAnimators;
  _bollView({this.color,this.count,this.ListAnimators});
  @override
  void paint(Canvas canvas,Size size){
     //繪製流程
     double boll_radius = (size.width - 15) / 8;
     Paint paint = new Paint();
     paint.color = color;
     paint.style = PaintingStyle.fill;
     //因為這個wiaget是80 球和球之間相隔5
     for(int i = 0; i < count;i++){
       double value = ListAnimators[i].value;
       //確定圓心 半徑 畫筆
       //第一個球 r
       //第二個球 5 + 3r
       //第三個球 15 + 5r
       //第四個球 30 + 7r
       //半徑也是隨著動畫值改變
       canvas.drawCircle(new Offset((i+1) * boll_radius + i * boll_radius  + i * 5,size.height / 2), boll_radius * (value > 1 ? (2 - value) : value), paint);
     }
  }

  //重新整理是否重繪
  @override
  bool shouldRepaint(CustomPainter oldDelegate){
    return oldDelegate != this;

  }
}
複製程式碼

2.2.配置小球屬性

class MyBalls extends StatefulWidget{
  Size size;
  Color color;
  int count;
  int seconds;

  //預設四個小球 紅色
  MyBalls({this.size,this.seconds : 400,this.color :Colors.redAccent,this.count : 4});

  @override
  State<StatefulWidget> createState(){
    return MyBallsState();
  }

}
複製程式碼

2.3.建立動畫

//繼承TickerProviderStateMixin,提供Ticker物件
class MyBallsState extends State<MyBalls> with TickerProviderStateMixin {
  //動畫集合
  List<Animation<double>>animatios = [];
  //控制器集合
  List<AnimationController> animationControllers = [];
  //顏色
  Animation<Color> colors;

  @override
  void initState(){
    super.initState();
    for(int i = 0;i < widget.count;i++){
         //建立動畫控制器
         AnimationController animationController = new AnimationController(
             vsync: this,
             duration: Duration(
               milliseconds: widget.count * widget.seconds
             ));
         //新增到控制器集合
         animationControllers.add(animationController);
         //顏色隨機
         colors = ColorTween(begin: Colors.red,end:Colors.green).animate(animationController);
         //建立動畫 每個動畫都要繫結控制器
         Animation<double> animation = new Tween(begin: 0.1,end:1.9).animate(animationController);
         animatios.add(animation);
    }
    animatios[0].addListener((){
      //重新整理
      setState(() {

      });
    });

    //延遲執行
    var delay = (widget.seconds ~/ (2 * animatios.length - 2));
    for(int i = 0;i < animatios.length;i++){
     Future.delayed(Duration(milliseconds: delay * i),(){
        animationControllers[i]
            ..repeat().orCancel;
      });
    }
  }
  @override
  Widget build(BuildContext context){
    return new CustomPaint(
      //自定義畫筆
      painter: _bollView(color: colors.value,count: widget.count,ListAnimators : animatios),
      size: widget.size,
    );
  }
  //釋放資源
  @override
  void dispose(){
    super.dispose();
    animatios[0].removeListener((){
      setState(() {

      });
    });
    animationControllers[0].dispose();
  }
}
複製程式碼

2.4.呼叫

class Ball extends StatelessWidget{
  @override
  Widget build(BuildContext context){
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Animation demo'),
        ),
        body: Center(
            child: MyBalls(size: new Size(80.0,20.0)),
        ),
      ),
    );
  }
}
複製程式碼

五、總結

  1. 寫佈局時,Flutter佈局都是物件,可以用變數值取記錄,相比Android來說,這複用性很高,但是寫複雜佈局時,會一行一行堆疊,括號滿腦子飛。
  2. 不像Android,佈局和實現邏輯分開,所有一切都寫在Dart中,需要做好封裝和職責分明。
  3. 頁面跳轉和Android一樣,是棧的思想。
  4. Android中,通過Xml方式或者animate()在View上呼叫,在Flutter需要到動畫的Widget可以使用動畫庫將動畫封裝在Widget上。

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

相關文章