前一天學習了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
上。若有不正之處歡迎你們批評指正~