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

1、前言

前一天學習了Flutter基本控件和基本佈局,我是以爲蠻有意思的。做爲前端開發者,如何開發出好看,用戶體驗好的界面尤爲重要。今天學習的方向主要有三:前端

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

2、佈局

由於我是從事Android開發,學習了Flutter以後,發現其佈局和在Android下佈局是不同的,Android佈局是在XML文件下,直觀性強一點,基本是總體到局部,首先是肯定根佈局是用LinearLayout仍是RelativeLayout或者是constraintLayout等。而在Flutter下,都是由Widget來拼接起來,不少時候都是Row+Column合成,我本身是在草稿上畫出用什麼Widget來拼出需求佈局,而後纔去實現。java

1.佈局一

直接上需求:ios

需求圖
很容易看出三塊豎直排列,跟 WidgetColumn來實現,局部第一行是 Text,第二行是 Row行,可是 Row並非都是統同樣式,多線程和Java深刻是帶圓角背景的,下面再仔細講解,第三行是兩個文本(做者文本和時間文本),一個圖標,第一個文本很容易想到 Expanded,當s時間文本和圖標擺放後,其會佔滿剩餘主軸空間。

分析佈局一

1.1.封裝TextStyle和Padding

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

/** * 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是有設置圓角漸變屬性的:canvas

//抽取第二行漸變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,
                )
              ),
          ),
    );
複製代碼

最終效果以下:app

佈局一實現效果

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來實現:less

//右下角圓形
    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

3、交互

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);
            }),
複製代碼

二者方式都是能夠的。

4、動畫

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)),
        ),
      ),
    );
  }
}
複製代碼

5、總結

  1. 寫佈局時,Flutter佈局都是對象,能夠用變量值取記錄,相比Android來講,這複用性很高,可是寫複雜佈局時,會一行一行堆疊,括號滿腦子飛。
  2. 不像Android,佈局和實現邏輯分開,全部一切都寫在Dart中,須要作好封裝和職責分明。
  3. 頁面跳轉和Android同樣,是棧的思想。
  4. Android中,經過Xml方式或者animate()在View上調用,在Flutter須要到動畫的Widget可使用動畫庫將動畫封裝在Widget上。

若有不正之處歡迎你們批評指正~

相關文章
相關標籤/搜索