前一天學習了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了,代碼以下:多線程
//第三行
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
來實現:less
//右下角圓形
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
上。若有不正之處歡迎你們批評指正~