Flutter学习之布局、交互、动画

10,674 阅读18分钟

一、前言

前一天学习了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上。

如有不正之处欢迎大家批评指正~