Flutter学习之布局、交互、动画[通俗易懂]

Flutter学习之布局、交互、动画[通俗易懂]前一天学习了Flutter基本控件和基本布局,我是觉得蛮有意思的。作为前端开发者,如何开发出好看,用户体验好的界面尤其重要。今天学习的方向主要有三: 加深布局的熟练度。 学习手势,页面跳转交互。 学习动画。 因为我是从事Android开发,学习了Flutter之后,发现其布局和…

一、前言

前一天学习了Flutter基本控件和基本布局,我是觉得蛮有意思的。作为前端开发者,如何开发出好看,用户体验好的界面尤其重要。今天学习的方向主要有三:

  1. 加深布局的熟练度。
  2. 学习手势,页面跳转交互。
  3. 学习动画。

二、布局

因为我是从事Android开发,学习了Flutter之后,发现其布局和在Android下布局是不一样的,Android布局是在XML文件下,直观性强一点,基本是整体到局部,首先是确定根布局是用LinearLayout还是RelativeLayout或者是constraintLayout等。而在Flutter下,都是由Widget来拼接起来,很多时候都是Row+Column合成,我自己是在草稿上画出用什么Widget来拼出需求布局,然后才去实现。

1.布局一

直接上需求:

需求图

很容易看出三块竖直排列,跟
Widget
Column来实现,局部第一行是
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上。

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

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
转载请注明出处: https://daima100.com/12894.html

(0)

相关推荐

  • Python空字典创造指南

    Python空字典创造指南 Python是一种高级编程语言,它具有强大而灵活的数据类型和数据结构。其中,字典是一个非常有用的数据结构,它可以存储键-值对。Python中可以通过空字典来存储键-值对,这为数据的存储提供了很多灵活性。本篇文章将为您提供Python空字典创造的指南,包括如何创建空字典、如何向空字典添加键值对、如何查找字典中的键和值、如何删除键值对和如何使用空字典来处理数据。希望这篇文章能帮助您更好地使用Python的字典和空字典。

    2024-08-08
    28
  • mysql5.5的几个新参数「终于解决」

    mysql5.5的几个新参数「终于解决」
    mysql5.5的几个新参数 分类: Mysql/postgreSQL 2013-01-04 12:04:14 新参数项 旧参数 参数说明 innodb_r…

    2023-04-07
    146
  • Python seek()函数:快速定位并移动文件指针

    Python seek()函数:快速定位并移动文件指针a href=”https://beian.miit.gov.cn/”苏ICP备2023018380号-1/a Copyright www.python100.com .Some Rights Reserved.

    2024-03-10
    94
  • ORA-01658: 无法为表空间 XXX 中的段创建 INITIAL 区[通俗易懂]

    ORA-01658: 无法为表空间 XXX 中的段创建 INITIAL 区[通俗易懂]ORA-01658: 无法为表空间 XXX中的段创建 INITIAL 区 表示表空间撑满了,需要扩充表空间,如果最大值也超过了就要新增数据文件 扩充表空间的几个方式 alter database d…

    2022-12-16
    165
  • json与string转换:com.alibaba.fastjson.JSONObject

    json与string转换:com.alibaba.fastjson.JSONObject在实际的使用中经常会需要将string转成json类型 String转为json对象 输出结果: 从结果看好像str和json区别不大,但是json支持的方法已经改变,可以直接通过json提取相关的内

    2023-08-18
    154
  • Python实现矩阵乘法

    Python实现矩阵乘法矩阵乘法是线性代数中的重要概念,对于Python工程师来说,熟练掌握矩阵乘法的方法是非常有必要的。Python在实现矩阵乘法时,可以通过NumPy库中的dot函数来进行计算。该函数可以接受2个ndarray型的参数,返回它们的矩阵乘积。

    2024-06-24
    46
  • 深入学习Python API

    深入学习Python APIPython是一种高级编程语言,它被广泛应用于Web开发、数据分析、人工智能、自然语言处理等领域。Python语言特别适用于函数式编程,代码简洁易读,非常适合快速开发和原型设计。Python的API库也非常丰富,开发者可以使用这些API快速构建自己的应用程序。Python API是Python语言的精髓之一,是许多Python程序员和开发人员使用Python编写应用程序时必须了解的重要知识。

    2024-07-01
    39
  • Python os.environ模块:环境变量的管理

    Python os.environ模块:环境变量的管理os.environ模块是Python提供的用于对系统环境变量进行管理的工具,简单而言,它是一个存储环境变量的字典。环境变量是指在操作系统中定义的以键值对的形式存在的一系列变量,这些变量用于存储系统相关的信息,例如当前用户的登录名、操作系统的安装目录、Python安装路径等。在本文中,我们将详细介绍os.environ模块的用法,并演示如何使用os.environ来设置、获取和删除环境变量。

    2023-12-16
    123

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注