前一天学习了Flutter
基本控件和基本布局,我是以为蛮有意思的。做为前端开发者,如何开发出好看,用户体验好的界面尤为重要。今天学习的方向主要有三:前端
由于我是从事Android
开发,学习了Flutter
以后,发现其布局和在Android
下布局是不同的,Android
布局是在XML
文件下,直观性强一点,基本是总体到局部,首先是肯定根布局是用LinearLayout
仍是RelativeLayout
或者是constraintLayout
等。而在Flutter
下,都是由Widget
来拼接起来,不少时候都是Row
+Column
合成,我本身是在草稿上画出用什么Widget
来拼出需求布局,而后才去实现。java
直接上需求:ios
Widget
用
Column
来实现,局部第一行是
Text
,第二行是
Row
行,可是
Row
并非都是统同样式,多线程和Java深刻是带圆角背景的,下面再仔细讲解,第三行是两个文本(做者文本和时间文本),一个图标,第一个文本很容易想到
Expanded
,当s时间文本和图标摆放后,其会占满剩余主轴空间。
首先我看到整个布局下字体的颜色至少四种,有加粗和不加粗的,而且有部分加了padding
,仍是封装TextStyle
和padding
把:web
/** * TextStyle:封装 * colors:颜色 * fontsizes:字体大小 * isFontWeight:是否加粗 */ TextStyle getTextStyle(Color colors,double fontsizes,bool isFontWeight){ return TextStyle( color:colors, fontSize: fontsizes, fontWeight: isFontWeight == true ? FontWeight.bold : FontWeight.normal , ); } /** * 组件加上下左右padding * w:所要加padding的组件 * all:加多少padding */ Widget getPadding(Widget w,double all){ return Padding( child:w, padding:EdgeInsets.all(all), ); } /** * 组件选择性加padding * 这里用了位置可选命名参数{param1,param2,...}来命名参数,也调用的时候能够不传 * */ Widget getPaddingfromLTRB(Widget w,{double l,double t,double,r,double b}){ return Padding( child:w, padding:EdgeInsets.fromLTRB(l ?? 0,t ?? 0,r ?? 0,b ?? 0), ); } 复制代码
由于上面分析,总体是用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), ], ); 复制代码
第二行能够看到多线程
和Java深刻
是带渐变效果的圆角,一看到这,我是没有头绪的,查了网上的资料发现Container
是有设置圆角
和渐变
属性的:canvas
//抽取第二行渐变text效果 Container getText(String text,LinearGradient linearGradient){ return Container( //距离左边距离10dp margin: const EdgeInsets.only(left: 10), //约束 至关于直接制定了该Container的宽和高,且它的优先级要高于width和height constraints: new BoxConstraints.expand( width: 70.0, height: 30.0,), //文字居中 alignment: Alignment.center, child: new Text( text, style:getTextStyle(Colors.white,14,false), ), decoration: new BoxDecoration( color: Colors.blue, //圆角 borderRadius: new BorderRadius.all(new Radius.circular(6.0)), //添加渐变 gradient:linearGradient, ), ); } 复制代码
//第二行 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), ], ); 复制代码
第三行就简单了,直接一个Row
Widget,内部嵌套Expanded
、Text
、Icon
就Ok了,代码以下:markdown
//第三行 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) ], ); 复制代码
//根Widget Widget ColumnWidget = Column( //主轴上设置居中 mainAxisAlignment: MainAxisAlignment.center, //交叉轴(水平方向)设置从左开始 crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ //第一行 getPaddingfromLTRB(Text('Java synchronized原理总结', style: getTextStyle(Colors.black, 16,true), ),t:0.0), //第二行 getPaddingfromLTRB(rowWidget,t:10.0), //第三行 getPaddingfromLTRB(rowthreeWidget,t:10.0), ], ); return new Scaffold( appBar: new AppBar( title: new Text('Flutter Demo'), ), //用card裹住 body: Card( child: Container( //高度 height: 160.0, //颜色 color: Colors.white, padding: EdgeInsets.all(10.0), child: Center( child: ColumnWidget, ) ), ), ); 复制代码
最终效果以下:多线程
直接上电影卡片布局,以下:app
Row
,左孩子是图片,右孩子是
Column
,其孩子分为五行,最后一行主演仍是用
Row
来实现,上分析图:
//根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, ], ); 复制代码
就是用自带的CircleAvatar
这个Widget
来实现:框架
//右下角圆形 CircleAvatar getCircleAvator(String image_url){ //圆形头像 return CircleAvatar( backgroundColor: Colors.white, backgroundImage: NetworkImage(image_url), ); } 复制代码
右布局就是用一个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
就是为了占满剩余空间。
//总体 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, ) ), ), ); 复制代码
运行效果图以下:
一样直接上需求:
Column
,一行一行实现就能够了,这个布局稍微简单一点,上分析图:
//布局三开始第一行 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), ], ); 复制代码
//布局三开始第三行 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'), ], ); 复制代码
//布局三整合 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, ) ), ), ); } 复制代码
运行效果:
上面实现了基本的布局,有了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
确定会有方法的。
在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), ], ); 复制代码
效果以下:
在Flutter
中加载更多的组件没有是提供的,那就要本身来实现,个人思路是,当监听滑到底部时,到底底部就要作加载处理。而ListView
有ScrollController
这个属性来控制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, ) ], ), ) ), ); } 复制代码
能够看到,上面用了Offstage
Widget里的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, ), ), ); 复制代码
效果以下图:
基本还能够,把上滑加载的提示圈加上去了,作到这里,我在想,有时候ListView
并非每一条Item
养生都是同样的,哪有没有属性是设置在不一样位置插入不一样的Item
呢?答案是有的,那就是ListView.separated
,ListView.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
,效果以下:
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, ), ); 复制代码
效果以下:
在Flutter
中,自带如点击事件的控件有RaisedButton
、IconButton
、OutlineButton
、Checkbox
、SnackBar
、Switch
等,以下面给OutlineButton
添加点击事件:
body:Center( child: OutlineButton( child: Text('点击我'), onPressed: (){ Fluttertoast.showToast( msg: '你点击了FlatButton', toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.CENTER, timeInSecForIos: 1, ); }), ), 复制代码
上面代码就能够捕捉OutlineButton
的点击事件。
不少控件不像RaisedButton
、OutlineButton
等已经对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/onHorizontalXXX
和onPanXXX
不能同时设置,若是同时须要水平、竖直方向的移动,设置onPanXXX
。直接上例子:
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 复制代码
//手势测试 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 复制代码
//手势测试 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 复制代码
//手势测试 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 复制代码
//手势测试 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 复制代码
除了GestureDetector
可以监听触摸事件外,Pointer
表明用户与设备屏幕交互的原始数据,也就是也能监听手势:
PointerDownEvent
:指针接触到屏幕的特定位置PointerMoveEvent
:指针从屏幕上的一个位置移动到另外一个位置PointMoveEvent
:指针中止接触屏幕PointUpEvent
:指针中止接触屏幕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 复制代码
发现也是能够监听手势的。
在Android
原生中,页面跳转是经过startActvity()
来跳转不一样页面,而在Flutter
就不同。Flutter
中,跳转页面有两种方式:静态路由方式和动态路由方式。在Flutter
管理多个页面有两个核心概念和类:Route
和Navigator
。一个route
是一个屏幕或者页面的抽象,Navigator
是管理route
的Widget
。Navigator
能够经过route
入栈和出栈来实现页面之间的跳转。
在原页面配置路由跳转,就是在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
就不用配置了。
//若是新页面不在同一个类中,记得把它导入 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
,有的话,就会根据配置跳到新的页面。
新页面,我在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('这是新的页面'), ), ); } } 复制代码
最终效果以下:
下面说一下跳转页面的第二种方式,动态路由方式:
child: RaisedButton( child: Text('点击我'), onPressed: (){ //Navigator.of(context).pushNamed('/mainnewroute'); //动态路由 Navigator.push( context, MaterialPageRoute(builder: (newPage){ return new newRoute(); }), ); }), 复制代码
效果和上面是同样的。
两种方式都是传递参数的,直接上动态路由传递数据代码:
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"); } 复制代码
传递数据给新页面能够了,那么怎样将新页面数据返回上一个页面呢?也是很简单,在返回方法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
对象指导动画的当前状态(例如,是开始、中止仍是向前或者向后移动),但它不知道屏幕上显示的内容。动画类型分为两类:
在Flutter
中的动画系统基于Animation
对象的。widget
能够在build
函数中读取Animation
对象的当前值,而且能够监听动画的状态改变。
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,主要分为六部
SingleTickerProviderStateMixin
,为了传入vsync
对象AnimationController
对象Animation
对象,并关联AnimationController
对象AnimationController
的forward
开启动画widget
根据Animation
的value
值来设置宽高widget
的dispose()
方法中调用释放资源最终效果以下:
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
语法。
使用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
中会自动调用addListener
和setState()
,_LogoAppState
将Animation
对象传递给基类并用animation.value
设置Image宽高。
在平时开发,咱们知道,不少时候都须要监听动画的状态,好像完成、前进、倒退等。在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(); } });//添加监听器 复制代码
效果以下:
上面的代码存在一个问题:更改动画须要更改显示Image
的widget
,更好的解决方案是将职责分离:
Animation
对象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
传递给AnimatedBuilder
,AnimatedBuilder
将其传递给匿名构造器,而后将该对象用做其子对象。最终的结果是AnimatedBuilder
插入到渲染树中的两个Widget
之间。最后,在initState()
方法建立一个AnimationController
和一个Tween
,而后经过animate()
绑定,在build
方法中,返回带有一个Image
为子对象的GrowTransition
对象和一个用于驱动过渡的动画对象。若是只是想把可复用的动画定义成一个widget
,那就用AnimatedWidget
。
不少时候,一个动画须要两种或者两种以上的动画,在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
动画,而且加了不透明Opacity
widget,最后在initState
方法中修改增长一句animation = new CurvedAnimation(parent: controller, curve: Curves.easeIn)
,最后的动画效果:
Curves.easeIn
值来实现非线性运动效果。
先上效果图:
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; } } 复制代码
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(); } } 复制代码
//继承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(); } } 复制代码
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)), ), ), ); } } 复制代码
Flutter
布局都是对象,能够用变量值取记录,相比Android
来讲,这复用性很高,可是写复杂布局时,会一行一行堆叠,括号满脑子飞。Android
,布局和实现逻辑分开,全部一切都写在Dart
中,须要作好封装和职责分明。Android
同样,是栈的思想。Android
中,经过Xml
方式或者animate()
在View上调用,在Flutter
须要到动画的Widget
可使用动画库将动画封装在Widget
上。若有不正之处欢迎你们批评指正~