一、前言
前一天學習了Flutter
基本控制元件和基本佈局,我是覺得蠻有意思的。作為前端開發者,如何開發出好看,使用者體驗好的介面尤其重要。今天學習的方向主要有三:
- 加深佈局的熟練度。
- 學習手勢,頁面跳轉互動。
- 學習動畫。
二、佈局
因為我是從事Android
開發,學習了Flutter
之後,發現其佈局和在Android
下佈局是不一樣的,Android
佈局是在XML
檔案下,直觀性強一點,基本是整體到區域性,首先是確定根佈局是用LinearLayout
還是RelativeLayout
或者是constraintLayout
等。而在Flutter
下,都是由Widget
來拼接起來,很多時候都是Row
+Column
合成,我自己是在草稿上畫出用什麼Widget
來拼出需求佈局,然後才去實現。
1.佈局一
直接上需求:
很容易看出三塊豎直排列,跟Widget
用Column
來實現,區域性第一行是Text
,第二行是Row
行,但是Row
並不是都是統一樣式,多執行緒和Java深入是帶圓角背景的,下面再仔細講解,第三行是兩個文字(作者文字和時間文字),一個圖示,第一個文字很容易想到Expanded
,當s時間文字和圖示擺放後,其會佔滿剩餘主軸空間。
1.1.封裝TextStyle和Padding
首先我看到整個佈局下字型的顏色至少四種,有加粗和不加粗的,並且有部分加了padding
,還是封裝TextStyle
和padding
把:
/**
* TextStyle:封裝
* colors:顏色
* fontsizes:字型大小
* isFontWeight:是否加粗
*/
TextStyle getTextStyle(Color colors,double fontsizes,bool isFontWeight){
return TextStyle(
color:colors,
fontSize: fontsizes,
fontWeight: isFontWeight == true ? FontWeight.bold : FontWeight.normal ,
);
}
/**
* 元件加上下左右padding
* w:所要加padding的元件
* all:加多少padding
*/
Widget getPadding(Widget w,double all){
return Padding(
child:w,
padding:EdgeInsets.all(all),
);
}
/**
* 元件選擇性加padding
* 這裡用了位置可選命名引數{param1,param2,...}來命名引數,也呼叫的時候可以不傳
*
*/
Widget getPaddingfromLTRB(Widget w,{double l,double t,double,r,double b}){
return Padding(
child:w,
padding:EdgeInsets.fromLTRB(l ?? 0,t ?? 0,r ?? 0,b ?? 0),
);
}
複製程式碼
1.2.實現第一行
因為上面分析,整體是用Column
來實現,下面實現第一行Java synchronized原理總結
Widget ColumnWidget = Column(
//主軸上設定居中
mainAxisAlignment: MainAxisAlignment.center,
//交叉軸(水平方向)設定從左開始
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
//第一行
getPaddingfromLTRB(Text('Java synchronized原理總結',
style: getTextStyle(Colors.black, 16,true),
),t:0.0),
],
);
複製程式碼
1.3.實現第二行
1.3.1實現漸變圓角Text
第二行可以看到多執行緒
和Java深入
是帶漸變效果的圓角,一看到這,我是沒有頭緒的,查了網上的資料發現Container
是有設定圓角
和漸變
屬性的:
//抽取第二行漸變text效果
Container getText(String text,LinearGradient linearGradient){
return Container(
//距離左邊距離10dp
margin: const EdgeInsets.only(left: 10),
//約束 相當於直接制定了該Container的寬和高,且它的優先順序要高於width和height
constraints: new BoxConstraints.expand(
width: 70.0, height: 30.0,),
//文字居中
alignment: Alignment.center,
child: new Text(
text,
style:getTextStyle(Colors.white,14,false),
),
decoration: new BoxDecoration(
color: Colors.blue,
//圓角
borderRadius: new BorderRadius.all(new Radius.circular(6.0)),
//新增漸變
gradient:linearGradient,
),
);
}
複製程式碼
1.3.2.整合第二行
//第二行
Widget rowWidget = Row(
//主軸左邊對齊
mainAxisAlignment: MainAxisAlignment.start,
//交叉軸(豎直方向)居中
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text("分類:",
style: getTextStyle(Colors.blue,14,true),
),
getText("多執行緒", l1),
getText("Java深入", l2),
],
);
//根Widget
Widget ColumnWidget = Column(
//主軸上設定居中
mainAxisAlignment: MainAxisAlignment.center,
//交叉軸(水平方向)設定從左開始
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
//第一行
getPaddingfromLTRB(Text('Java synchronized原理總結',
style: getTextStyle(Colors.black, 16,true),
),t:0.0),
//第二行
getPaddingfromLTRB(rowWidget,t:10.0),
],
);
複製程式碼
1.4.實現第三行
第三行就簡單了,直接一個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)
],
);
複製程式碼
1.5.整體
//根Widget
Widget ColumnWidget = Column(
//主軸上設定居中
mainAxisAlignment: MainAxisAlignment.center,
//交叉軸(水平方向)設定從左開始
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
//第一行
getPaddingfromLTRB(Text('Java synchronized原理總結',
style: getTextStyle(Colors.black, 16,true),
),t:0.0),
//第二行
getPaddingfromLTRB(rowWidget,t:10.0),
//第三行
getPaddingfromLTRB(rowthreeWidget,t:10.0),
],
);
return new Scaffold(
appBar: new AppBar(
title: new Text('Flutter Demo'),
),
//用card裹住
body: Card(
child: Container(
//高度
height: 160.0,
//顏色
color: Colors.white,
padding: EdgeInsets.all(10.0),
child: Center(
child: ColumnWidget,
)
),
),
);
複製程式碼
最終效果如下:
2.佈局二
直接上電影卡片佈局,如下:
大致把圖看了一遍,大致框架是最外層是用Row
,左孩子是圖片,右孩子是Column
,其孩子分為五行,最後一行主演還是用Row
來實現,上分析圖:
2.1.實現右邊圖片
//根Widget 佈局二 開始
//右邊圖片佈局
Widget LayoutTwoLeft = Container(
//這次使用裁剪實現圓角矩形
child:ClipRRect(
//設定圓角
borderRadius: BorderRadius.circular(4.0),
child: Image.network(
'https://img3.doubanio.com//view//photo//s_ratio_poster//public//p2545472803.webp',
width: 100.0,
height: 150.0,
fit:BoxFit.fill,
),
),
);
//整體
Widget RowWidget = Row(
//主軸上設定居中
mainAxisAlignment: MainAxisAlignment.start,
//交叉軸(水平方向)設定從左開始
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
LayoutTwoLeft,
],
);
複製程式碼
2.2.實現圓形頭像
就是用自帶的CircleAvatar
這個Widget
來實現:
//右下角圓形
CircleAvatar getCircleAvator(String image_url){
//圓形頭像
return CircleAvatar(
backgroundColor: Colors.white,
backgroundImage: NetworkImage(image_url),
);
}
複製程式碼
2.3.實現右邊佈局
右佈局就是用一個Column
來實現,一列一列往下實現即可:
//右佈局
Widget LayoutTwoRightColumn = Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
//電影名稱
Text(
'流浪地球',
style: getTextStyle(Colors.black, 20.0, true),
),
//豆瓣評分
Text(
'豆瓣評分:7.9',
style: getTextStyle(Colors.black54, 16.0, false),
),
//型別
Text(
'型別:科幻、太空、災難',
style:getTextStyle(Colors.black54, 16.0, false),
),
//導演
Text(
'導演:郭帆',
style: getTextStyle(Colors.black54, 16.0, false),
),
//主演
Container(
margin: EdgeInsets.only(top:8.0),
child:Row(
children: <Widget>[
Text('主演:'),
//以Row從左到右排列頭像
Row(
children: <Widget>[
Container(
margin: EdgeInsets.only(left:2.0),
child: getCircleAvator('https://img3.doubanio.com//view//celebrity//s_ratio_celebrity//public//p1533348792.03.webp'),
),
Container(
margin: EdgeInsets.only(left:12.0),
child: getCircleAvator('https://img3.doubanio.com//view//celebrity//s_ratio_celebrity//public//p1501738155.24.webp'),
),
Container(
margin: EdgeInsets.only(left:12.0),
child: getCircleAvator('https://img3.doubanio.com//view//celebrity//s_ratio_celebrity//public//p1540619056.43.webp'),
),
],
),
],
),
),
],
);
//佈局二 右佈局 用Expanded佔滿剩餘空間
Widget LayoutTwoRightExpanded = Expanded(
child:Container(
//距離左佈局10
margin:EdgeInsets.only(left:10.0),
//高度
height:150.0,
child: LayoutTwoRightColumn,
),
);
複製程式碼
右佈局用Expanded
就是為了佔滿剩餘空間。
2.4.整合
//整體
Widget RowWidget = Row(
//主軸上設定從開始方向對齊
mainAxisAlignment: MainAxisAlignment.start,
//交叉軸(水平方向)居中
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
LayoutTwoLeft,
LayoutTwoRightExpanded,
],
);
return new Scaffold(
appBar: new AppBar(
title: new Text('Flutter Demo'),
),
body: Card(
child: Container(
//alignment: Alignment(0.0, 0.0),
height: 160.0,
color: Colors.white,
padding: EdgeInsets.all(10.0),
child: Center(
// 佈局一
// child: ColumnWidget,
// 佈局二
child:RowWidget,
)
),
),
);
複製程式碼
執行效果圖如下:
3.佈局三
同樣直接上需求:
一看還是根佈局直接用Column
,一行一行實現就可以了,這個佈局稍微簡單一點,上分析圖:
3.1.實現第一行
//佈局三開始第一行
Widget LayoutThreeOne = Row(
children: <Widget>[
Expanded(
child: Row(
children: <Widget>[
Text('作者:'),
Text('HuYounger',
style: getTextStyle(Colors.redAccent[400], 14, false),
),
],
)
),
//收藏圖示
getPaddingfromLTRB(Icon(Icons.favorite,color:Colors.red),r:10.0),
//分享圖示
Icon(Icons.share,color:Colors.black),
],
);
複製程式碼
3.2.實現第三行
//佈局三開始第三行
Widget LayoutThreeThree = Row(
children: <Widget>[
Expanded(
child: Row(
children: <Widget>[
Text('分類:'),
getPaddingfromLTRB(Text('開發環境/Android',
style:getTextStyle(Colors.deepPurpleAccent, 14, false)),l:8.0),
],
),
),
Text('釋出時間:2018-12-13'),
],
);
複製程式碼
3.3.整合
//佈局三整合
Widget LayoutThreeColumn = Column(
//主軸上設定居中
mainAxisAlignment: MainAxisAlignment.center,
//交叉軸(水平方向)設定從左開始
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
//第一行
LayoutThreeOne,
//第二行
getPaddingfromLTRB(Text('Android Monitor使用介紹',
style:getTextStyle(Colors.black, 18, false),
),t:10.0),
//第三行
getPaddingfromLTRB(LayoutThreeThree,t:10.0),
],
);
return new Scaffold(
appBar: new AppBar(
title: new Text('Flutter Demo'),
),
body: Card(
child: Container(
//alignment: Alignment(0.0, 0.0),
height: 160.0,
color: Colors.white,
padding: EdgeInsets.all(10.0),
child: Center(
// 佈局一
// child: ColumnWidget,
// 佈局二
// child:RowWidget,
// 佈局三
child:LayoutThreeColumn,
)
),
),
);
}
複製程式碼
執行效果:
4.新增ListView
上面實現了基本的佈局,有了item
後,那必須有ListView
,這裡簡單模擬一下實現一下:
return new Scaffold(
appBar: new AppBar(
title: new Text('Flutter Demo'),
),
//ListView提供一個builder屬性
body: ListView.builder(
//數目
itemCount: 20,
//itemBuilder是一個匿名回撥函式,有兩個引數,BuildContext 和迭代器index
//和ListView的Item項類似 迭代器從0開始 每呼叫一次這個函式,迭代器就會加1
itemBuilder: (BuildContext context,int index){
return Column(
children: <Widget>[
cardWidget,
],
);
}),
);
複製程式碼
發現螢幕上被20條Item
項填充滿,這裡想想,把下拉重新整理和上滑載入加上,Flutter
肯定會有方法的。
4.1.下拉重新整理
在Flutter
已經提供和原生Android一樣的重新整理元件,叫做RefreshIndicator
,是MD
風格的,Flutter
裡面的ScrollView
和子Widget
都可以新增下拉重新整理,只要在子``Widget的上層包裹一層
RefreshIndicator`,先看看構造方法:
const RefreshIndicator({
Key key,
@required this.child,
this.displacement = 40.0,//下拉重新整理的距離
@required this.onRefresh,//下拉重新整理回撥方法
this.color, //進度指示器前景色 預設是系統主題色
this.backgroundColor, //背景色
this.notificationPredicate = defaultScrollNotificationPredicate,
this.semanticsLabel, //小部件的標籤
this.semanticsValue, //載入進度
})
複製程式碼
包裹住ListView
,並且定義下拉重新整理方法:
return new Scaffold(
appBar: new AppBar(
title: new Text('Flutter Demo'),
),
body: RefreshIndicator(
//ListView提供一個builder屬性
child: ListView.builder(
//數目
itemCount: 20,
//itemBuilder是一個匿名回撥函式,有兩個引數,BuildContext 和迭代器index
//和ListView的Item項類似 迭代器從0開始 每呼叫一次這個函式,迭代器就會加1
itemBuilder: (BuildContext context,int index){
return Column(
children: <Widget>[
cardWidget,
],
);
}),
onRefresh: _onRefresh,),
);
//下拉重新整理方法
Future<Null> _onRefresh() async {
//寫邏輯
}
複製程式碼
可以看到上面定義重新整理方法_onRefresh
,這裡先不加任何邏輯。把根Widget
繼承StatefulWidget
,因為後面涉及到狀態更新:
class HomeStateful extends StatefulWidget{
@override
State<StatefulWidget> createState(){
return new HomeWidget();
}
}
class HomeWidget extends State<HomeStateful> {
//列表要顯示的資料
List list = new List();
//是否正在載入 重新整理
bool isfresh = false;
//這個方法只會呼叫一次,在這個Widget被建立之後,必須呼叫super.initState()
@override
void initState(){
super.initState();
//初始化資料
initData();
}
//延遲3秒後重新整理
Future initData() async{
await Future.delayed(Duration(seconds: 3),(){
setState(() {
//用生成器給所有元素賦初始值
list = List.generate(20, (i){
return i;
});
});
});
}
}
複製程式碼
一開始先建立並初始化長度是20的List
集合,ListView
根據這個集合長度來構建對應數目的Item
項,上面程式碼是初始化3秒後才重新整理資料,並加了標記isfresh
是否載入重新整理,Scafford
程式碼如下:
//ListView Item
Widget _itemColumn(BuildContext context,int index){
if(index <list.length){
return Column(
children: <Widget>[
cardWidget,
],
);
}
}
return new Scaffold(
appBar: new AppBar(
title: new Text('Flutter Demo'),
),
body: RefreshIndicator(
//ListView提供一個builder屬性
child: ListView.builder(
//集合數目
itemCount: list.length,
//itemBuilder是一個匿名回撥函式,有兩個引數,BuildContext 和迭代器index
//和ListView的Item項類似 迭代器從0開始 每呼叫一次這個函式,迭代器就會加1
itemBuilder: _itemColumn,
),
onRefresh: _onRefresh,),
);
}
複製程式碼
下面把下拉重新整理方法邏輯簡單加一下,我這邊只是重新將集合清空,然後重新新增8條資料,只是為了看重新整理效果而兒:
//下拉重新整理方法
Future<Null> _onRefresh() async {
//寫邏輯 延遲3秒後執行重新整理
//重新整理把isfresh改為true
isfresh = true;
await Future.delayed(Duration(seconds: 3),(){
setState(() {
//資料清空再重新新增8條資料
list.clear();
list.addAll(List.generate(8, (i){
return i;
}));
});
});
}
複製程式碼
為了看到重新整理效果,當重新整理的時候,因為isfresh
為true,收藏圖示♥️改為紅色,否則是黑色:
//佈局三開始第一行
Widget LayoutThreeOne = Row(
children: <Widget>[
Expanded(
child: Row(
children: <Widget>[
Text('作者:'),
Text('HuYounger',
style: getTextStyle(Colors.redAccent[400], 14, false),
),
],
)
),
//收藏圖示 改為以下
getPaddingfromLTRB(Icon(Icons.favorite,color:isfresh ? Colors.red : Colors.black),r:10.0),
//分享圖示
Icon(Icons.share,color:Colors.black),
],
);
複製程式碼
效果如下:
4.2.上拉載入
在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,
),
),
);
複製程式碼
效果如下圖:
4.3.ListView.separated
基本還可以,把上滑載入的提示圈加上去了,做到這裡,我在想,有時候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,
),
);
複製程式碼
效果如下:
三、互動
1.自帶互動的控制元件
在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
的點選事件。
2.不自帶互動的控制元件
很多控制元件不像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
。直接上例子:
2.1.onTapXXX
child: GestureDetector(
child: Container(
width: 300.0,
height: 300.0,
color:Colors.red,
),
onTapDown: (d){
print("onTapDown");
},
onTapUp: (d){
print("onTapUp");
},
onTap:(){
print("onTap");
},
onTapCancel: (){
print("onTaoCancel");
},
)
複製程式碼
點了一下,並且抬起,結果是:
I/flutter (16304): onTapDown
I/flutter (16304): onTapUp
I/flutter (16304): onTap
先觸發onTapDown 然後onTapUp 繼續onTap
複製程式碼
2.2.onLongXXX
//手勢測試
Widget gestureTest = GestureDetector(
child: Container(
width: 300.0,
height: 300.0,
color:Colors.red,
),
onDoubleTap: (){
print("雙擊onDoubleTap");
},
onLongPress: (){
print("長按onLongPress");
},
onLongPressUp: (){
print("長按抬起onLongPressUP");
},
);
複製程式碼
實際結果:
I/flutter (16304): 長按onLongPress
I/flutter (16304): 長按抬起onLongPressUP
I/flutter (16304): 雙擊onDoubleTap
複製程式碼
2.3.onVerticalXXX
//手勢測試
Widget gestureTest = GestureDetector(
child: Container(
width: 300.0,
height: 300.0,
color:Colors.red,
),
onVerticalDragDown: (_){
print("豎直方向拖動按下onVerticalDragDown:"+_.globalPosition.toString());
},
onVerticalDragStart: (_){
print("豎直方向拖動開始onVerticalDragStart"+_.globalPosition.toString());
},
onVerticalDragUpdate: (_){
print("豎直方向拖動更新onVerticalDragUpdate"+_.globalPosition.toString());
},
onVerticalDragCancel: (){
print("豎直方向拖動取消onVerticalDragCancel");
},
onVerticalDragEnd: (_){
print("豎直方向拖動結束onVerticalDragEnd");
},
);
複製程式碼
輸出結果:
I/flutter (16304): 豎直方向拖動按下onVerticalDragDown:Offset(191.7, 289.3)
I/flutter (16304): 豎直方向拖動開始onVerticalDragStartOffset(191.7, 289.3)
I/flutter (16304): 豎直方向拖動更新onVerticalDragUpdateOffset(191.7, 289.3)
I/flutter (16304): 豎直方向拖動更新onVerticalDragUpdateOffset(191.7, 289.3)
I/flutter (16304): 豎直方向拖動更新onVerticalDragUpdateOffset(191.7, 289.3)
I/flutter (16304): 豎直方向拖動更新onVerticalDragUpdateOffset(191.7, 289.3)
I/flutter (16304): 豎直方向拖動更新onVerticalDragUpdateOffset(191.7, 289.3)
I/flutter (16304): 豎直方向拖動更新onVerticalDragUpdateOffset(191.3, 290.0)
I/flutter (16304): 豎直方向拖動更新onVerticalDragUpdateOffset(191.3, 291.3)
I/flutter (16304): 豎直方向拖動結束onVerticalDragEnd
複製程式碼
2.4.onPanXXX
//手勢測試
Widget gestureTest = GestureDetector(
child: Container(
width: 300.0,
height: 300.0,
color:Colors.red,
),
onPanDown: (_){
print("onPanDown");
},
onPanStart: (_){
print("onPanStart");
},
onPanUpdate: (_){
print("onPanUpdate");
},
onPanCancel: (){
print("onPanCancel");
},
onPanEnd: (_){
print("onPanEnd");
},
);
複製程式碼
無論豎直拖動還是橫向拖動還是一起來,結果如下:
I/flutter (16304): onPanDown
I/flutter (16304): onPanStart
I/flutter (16304): onPanUpdate
I/flutter (16304): onPanUpdate
I/flutter (16304): onPanEnd
複製程式碼
2.5.onScaleXXX
//手勢測試
Widget gestureTest = GestureDetector(
child: Container(
width: 300.0,
height: 300.0,
color:Colors.red,
),
onScaleStart: (_){
print("onScaleStart");
},
onScaleUpdate: (_){
print("onScaleUpdate");
},
onScaleEnd: (_){
print("onScaleEnd");
);
複製程式碼
無論點選、豎直拖動、水平拖動,結果如下:
I/flutter (16304): onScaleStart
I/flutter (16304): onScaleUpdate
I/flutter (16304): onScaleUpdate
I/flutter (16304): onScaleUpdate
I/flutter (16304): onScaleUpdate
I/flutter (16304): onScaleUpdate
I/flutter (16304): onScaleUpdate
I/flutter (16304): onScaleUpdate
I/flutter (16304): onScaleEnd
複製程式碼
3.原始指標事件
除了GestureDetector
能夠監聽觸控事件外,Pointer
代表使用者與裝置螢幕互動的原始資料,也就是也能監聽手勢:
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
複製程式碼
發現也是可以監聽手勢的。
4.路由(頁面)跳轉
在Android
原生中,頁面跳轉是通過startActvity()
來跳轉不同頁面,而在Flutter
就不一樣。Flutter
中,跳轉頁面有兩種方式:靜態路由方式和動態路由方式。在Flutter
管理多個頁面有兩個核心概念和類:Route
和Navigator
。一個route
是一個螢幕或者頁面的抽象,Navigator
是管理route
的Widget
。Navigator
可以通過route
入棧和出棧來實現頁面之間的跳轉。
4.1.靜態路由
4.1.1.配置路由
在原頁面配置路由跳轉,就是在MaterialApp
裡設定每個route
對應的頁面,注意:一個app只能有一個材料設計(MaterialApp),不然返回上一個頁面會黑屏。程式碼如下:
//入口頁面
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
//靜態路由方式 配置初始路由
initialRoute: '/',
routes: {
//預設走這個條件`/`
'/':(context){
return HomeStateful();
},
//新頁面路由
'/mainnewroute':(context){
return new newRoute();
}
},
//主題色
theme: ThemeData(
//設定為紅色
primarySwatch: Colors.red),
//配置了初始路由,下面就不需要了
//home: HomeStateful(),
);
}
}
複製程式碼
因為配置了初始路由,所以home:HomeStateful
就不用配置了。
4.1.2.點選跳轉
//如果新頁面不在同一個類中,記得把它匯入
import 'mainnewroute.dart';
class HomeStateful extends StatefulWidget{
@override
State<StatefulWidget> createState(){
return new HomeWidget();
}
}
class HomeWidget extends State<HomeStateful> {
@override
Widget build(BuildContext context) {
...
//Pointer
Widget TestContainer = Listener(
child:Container(
width: 300.0,
height: 300.0,
color:Colors.red,
child: RaisedButton(
child: Text('點選我'),
onPressed: (){
//頁面跳轉方法
Navigator.of(context).pushNamed('/mainnewroute');
}),
),
);
return new Scaffold(
appBar: new AppBar(
title: new Text('Flutter Demo'),
),
body:Center(
child: TestContainer,
),
);
}
}
複製程式碼
RaisedButton
配置了點選方法,上面用了Navigator.of(context).pushNamed('/mainnewroute')
,執行到這句,路由會找routes
有沒有配置/mainnewroute
,有的話,就會根據配置跳到新的頁面。
4.1.3.配置新頁面
新頁面,我在lib
下建立一個新的檔案(頁面)mainfourday.dart
,很簡單:
import 'package:flutter/material.dart';
class newRoute extends StatelessWidget{
@override
Widget build(BuildContext context){
return HomeWidget();
//注意:不需要MaterialApp
// return MaterialApp(
// theme: ThemeData(
// //設定為hongse
// primarySwatch: Colors.red),
// home: HomeWidget(),
// );
}
}
class HomeWidget extends StatelessWidget{
@override
Widget build(BuildContext context){
return Scaffold(
appBar: AppBar(
title: Text('new Route'),
),
body: Center(
child:RaisedButton(
child: Text('返回'),
onPressed: (){
//這是關閉頁面
Navigator.pop(context);
}),
// child: Text('這是新的頁面'),
),
);
}
}
複製程式碼
最終效果如下:
4.2.動態路由
下面說一下跳轉頁面的第二種方式,動態路由方式:
child: RaisedButton(
child: Text('點選我'),
onPressed: (){
//Navigator.of(context).pushNamed('/mainnewroute');
//動態路由
Navigator.push(
context,
MaterialPageRoute(builder: (newPage){
return new newRoute();
}),
);
}),
複製程式碼
效果和上面是一樣的。
4.3.頁面傳遞資料
兩種方式都是傳遞引數的,直接上動態路由傳遞資料程式碼:
Navigator.push(
context,
MaterialPageRoute(builder: (newPage){
return new newRoute("這是一份資料到新頁面");
}),
);
複製程式碼
在新頁面改為如下:
import 'package:flutter/material.dart';
class newRoute extends StatelessWidget{
//接收上一個頁面傳遞的資料
String str;
//建構函式
newRoute(this.str);
@override
Widget build(BuildContext context){
return HomeWidget(str);
}
}
class HomeWidget extends StatelessWidget{
String newDate;
HomeWidget(this.newDate);
@override
Widget build(BuildContext context){
return Scaffold(
appBar: AppBar(
title: Text('new Route'),
),
body: Center(
child:RaisedButton(
//顯示上一個頁面所傳遞的資料
child: Text(newDate),
onPressed: (){
Navigator.pop(context);
}),
// child: Text('這是新的頁面'),
),
);
}
}
複製程式碼
靜態路由方式傳遞引數,也就是在newRoute()
加上所要傳遞的引數就可以了
//新頁面路由
'/mainnewroute':(context){
return new newRoute("sdsd");
}
複製程式碼
4.4.頁面返回資料
傳遞資料給新頁面可以了,那麼怎樣將新頁面資料返回上一個頁面呢?也是很簡單,在返回方法pop
加上所要返回的資料即可:
body: Center(
child:RaisedButton(
//顯示上一個頁面所傳遞的資料
child: Text(newDate),
onPressed: (){
Navigator.pop(context,"這是新頁面返回的資料");
}),
// child: Text('這是新的頁面'),
),
複製程式碼
因為開啟頁面是非同步的,所以頁面的結果需要通過一個Future
來返回,靜態路由方式:
child: RaisedButton(
child: Text('點選我'),
onPressed: () async {
var data = await Navigator.of(context).pushNamed('/mainnewroute');
//列印返回來的資料
print(data);
}),
複製程式碼
動態路由方式:
child: RaisedButton(
child: Text('點選我'),
onPressed: () async {
var data = await Navigator.push(
context,
MaterialPageRoute(builder: (newPage){
return new newRoute("這是一份資料到新頁面");
}),
);
//列印返回的值
print(data);
}),
複製程式碼
兩者方式都是可以的。
四、動畫
Flutter
動畫庫的核心類是Animation
物件,它生成指導動畫的值,Animation
物件指導動畫的當前狀態(例如,是開始、停止還是向前或者向後移動),但它不知道螢幕上顯示的內容。動畫型別分為兩類:
- 補簡動畫(Tween),定義了開始點和結束點、時間線以及定義轉換時間和速度的曲線。然後由框架計算如何從開始點過渡到結束點。Tween是一個無狀態(stateless)物件,需要begin和end值。Tween的唯一職責就是定義從輸入範圍到輸出範圍的對映。輸入範圍通常為0.0到1.0,但這不是必須的。
- 基於物理動畫,運動被模擬與真實世界行為相似,例如,當你擲球時,它何處落地,取決於拋球速度有多快、球有多重、距離地面有多遠。類似地,將連線在彈簧上的球落下(並彈起)與連線到繩子的球放下的方式也是不同。
在Flutter
中的動畫系統基於Animation
物件的。widget
可以在build
函式中讀取Animation
物件的當前值,並且可以監聽動畫的狀態改變。
1.動畫示例
import 'package:flutter/material.dart';
import 'package:flutter/animation.dart';
void main() {
//執行程式
runApp(LogoApp());
}
class LogoApp extends StatefulWidget{
@override
State<StatefulWidget> createState(){
return new _LogoAppState();
}
}
//logo
Widget ImageLogo = new Image(
image: new AssetImage('images/logo.jpg'),
);
//with 是dart的關鍵字,混入的意思,將一個或者多個類的功能天驕到自己的類無需繼承這些類
//避免多重繼承問題
//SingleTickerProviderStateMixin 初始化 animation 和 Controller的時候需要一個TickerProvider型別的引數Vsync
//所依混入TickerProvider的子類
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin{
//動畫的狀態,如動畫開啟,停止,前進,後退等
Animation<double> animation;
//管理者animation物件
AnimationController controller;
@override
void initState() {
// TODO: implement initState
super.initState();
//建立AnimationController
//需要傳遞一個vsync引數,存在vsync時會防止螢幕外動畫(
//譯者語:動畫的UI不在當前螢幕時)消耗不必要的資源。 通過將SingleTickerProviderStateMixin新增到類定義中,可以將stateful物件作為vsync的值。
controller = new AnimationController(
//時間是3000毫秒
duration: const Duration(
milliseconds: 3000
),
//vsync 在此處忽略不必要的情況
vsync: this,
);
//補間動畫
animation = new Tween(
//開始的值是0
begin: 0.0,
//結束的值是200
end : 200.0,
).animate(controller)//新增監聽器
..addListener((){
//動畫值在發生變化時就會呼叫
setState(() {
});
});
//只顯示動畫一次
controller.forward();
}
@override
Widget build(BuildContext context){
return new MaterialApp(
theme: ThemeData(
primarySwatch: Colors.red
),
home: new Scaffold(
appBar: new AppBar(
title: Text("動畫demo"),
),
body:new Center(
child: new Container(
//寬和高都是根據animation的值來變化
height: animation.value,
width: animation.value,
child: ImageLogo,
),
),
),
);
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
//資源釋放
controller.dispose();
}
}
複製程式碼
上面實現了影像在3000毫秒間從寬高是0變化到寬高是200,主要分為六部
- 混入
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
語法。
1.1.AnimatedWidget簡化
使用AnimatedWidget
對動畫進行簡化,使用AnimatedWidget
建立一個可重用動畫的widget
,而不是用addListener()
和setState()
來給widget
新增動畫。AnimatedWidget
類允許從setState()
呼叫中的動畫程式碼中分離出widget
程式碼。AnimatedWidget
不需要維護一個State
物件了來儲存動畫。
import 'package:flutter/material.dart';
import 'package:flutter/animation.dart';
void main() {
//執行程式
runApp(LogoApp());
}
class LogoApp extends StatefulWidget{
@override
State<StatefulWidget> createState(){
return new _LogoAppState();
}
}
//logo
Widget ImageLogo = new Image(
image: new AssetImage('images/logo.jpg'),
);
//抽象出來
class AnimatedLogo extends AnimatedWidget{
AnimatedLogo({Key key,Animation<double> animation})
:super(key:key,listenable:animation);
@override
Widget build(BuildContext context){
final Animation<double> animation = listenable;
return new MaterialApp(
theme: ThemeData(
primarySwatch: Colors.red
),
home: new Scaffold(
appBar: new AppBar(
title: Text("動畫demo"),
),
body:new Center(
child: new Container(
//寬和高都是根據animation的值來變化
height: animation.value,
width: animation.value,
child: ImageLogo,
),
),
),
);
}
}
//with 是dart的關鍵字,混入的意思,將一個或者多個類的功能新增到自己的類無需繼承這些類
//避免多重繼承問題
//SingleTickerProviderStateMixin 初始化 animation 和 Controller的時候需要一個TickerProvider型別的引數Vsync
//所依混入TickerProvider的子類
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin{
//動畫的狀態,如動畫開啟,停止,前進,後退等
Animation<double> animation;
//管理者animation物件
AnimationController controller;
@override
void initState() {
// TODO: implement initState
super.initState();
//建立AnimationController
//需要傳遞一個vsync引數,存在vsync時會防止螢幕外動畫(
//譯者語:動畫的UI不在當前螢幕時)消耗不必要的資源。 通過將SingleTickerProviderStateMixin新增到類定義中,可以將stateful物件作為vsync的值。
controller = new AnimationController(
//時間是3000毫秒
duration: const Duration(
milliseconds: 3000
),
//vsync 在此處忽略不必要的情況
vsync: this,
);
//補間動畫
animation = new Tween(
//開始的值是0
begin: 0.0,
//結束的值是200
end : 200.0,
).animate(controller);//新增監聽器
//只顯示動畫一次
controller.forward();
}
@override
Widget build(BuildContext context){
return AnimatedLogo(animation: animation);
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
//資源釋放
controller.dispose();
}
}
複製程式碼
可以發現AnimatedWidget
中會自動呼叫addListener
和setState()
,_LogoAppState
將Animation
物件傳遞給基類並用animation.value
設定Image寬高。
1.2.監視動畫
在平時開發,我們知道,很多時候都需要監聽動畫的狀態,好像完成、前進、倒退等。在Flutter
中可以通過addStatusListener()
來得到這個通知,以下程式碼新增了動畫狀態
//補間動畫
animation = new Tween(
//開始的值是0
begin: 0.0,
//結束的值是200
end : 200.0,
).animate(controller)
//新增動畫狀態
..addStatusListener((state){
return print('$state');
});//新增監聽器
複製程式碼
執行程式碼會輸出下面結果:
I/flutter (16745): AnimationStatus.forward //動畫開始
Syncing files to device KNT AL10...
I/zygote64(16745): Do partial code cache collection, code=30KB, data=25KB
I/zygote64(16745): After code cache collection, code=30KB, data=25KB
I/zygote64(16745): Increasing code cache capacity to 128KB
I/flutter (16745): AnimationStatus.completed//動畫完成
複製程式碼
下面那就運用addStatusListener()
在開始或結束反轉動畫。那就產生迴圈效果:
//補間動畫
animation = new Tween(
//開始的值是0
begin: 0.0,
//結束的值是200
end : 200.0,
).animate(controller)
//新增動畫狀態
..addStatusListener((state){
//如果動畫完成了
if(state == AnimationStatus.completed){
//開始反向這動畫
controller.reverse();
} else if(state == AnimationStatus.dismissed){
//開始向前執行著動畫
controller.forward();
}
});//新增監聽器
複製程式碼
效果如下:
1.3.用AnimatedBuilder重構
上面的程式碼存在一個問題:更改動畫需要更改顯示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
。
1.5.並行動畫
很多時候,一個動畫需要兩種或者兩種以上的動畫,在Flutter
也是可以實現的,每一個Tween
管理動畫的一種效果,如:
final AnimationController controller =
new AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
final Animation<double> sizeAnimation =
new Tween(begin: 0.0, end: 300.0).animate(controller);
final Animation<double> opacityAnimation =
new Tween(begin: 0.1, end: 1.0).animate(controller);
複製程式碼
可以通過sizeAnimation.Value
來獲取大小,通過opacityAnimation.value
來獲取不透明度,但AnimatedWidget
的建構函式只能接受一個動畫物件,解決這個問題,需要動畫的widget
建立了自己的Tween
物件,上程式碼:
//AnimatedBuilder
class GrowTransition extends StatelessWidget {
final Widget child;
final Animation<double> animation;
GrowTransition({this.child, this.animation});
static final _opacityTween = new Tween<double>(begin: 0.1, end: 1.0);
static final _sizeTween = new Tween<double>(begin: 0.0, end: 200.0);
@override
Widget build(BuildContext context) {
return new MaterialApp(
theme: ThemeData(primarySwatch: Colors.red),
home: new Scaffold(
appBar: new AppBar(
title: Text("動畫demo"),
),
body: new Center(
child: new AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget child) {
return new Opacity(
opacity: _opacityTween.evaluate(animation),
child: new Container(
//寬和高都是根據animation的值來變化
height: _sizeTween.evaluate(animation),
width: _sizeTween.evaluate(animation),
child: child,
),
);
},
child: child,
),
),
),
);
}
}
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
//動畫的狀態,如動畫開啟,停止,前進,後退等
Animation<double> animation;
//管理者animation物件
AnimationController controller;
@override
void initState() {
// TODO: implement initState
super.initState();
//建立AnimationController
//需要傳遞一個vsync引數,存在vsync時會防止螢幕外動畫(
//譯者語:動畫的UI不在當前螢幕時)消耗不必要的資源。 通過將SingleTickerProviderStateMixin新增到類定義中,可以將stateful物件作為vsync的值。
controller = new AnimationController(
//時間是3000毫秒
duration: const Duration(milliseconds: 3000),
//vsync 在此處忽略不必要的情況
vsync: this,
);
//新增
animation = new CurvedAnimation(parent: controller, curve: Curves.easeIn)
..addStatusListener((state) {
//如果動畫完成了
if (state == AnimationStatus.completed) {
//開始反向這動畫
controller.reverse();
} else if (state == AnimationStatus.dismissed) {
//開始向前執行著動畫
controller.forward();
}
}); //新增監聽器
//只顯示動畫一次
controller.forward();
}
@override
Widget build(BuildContext context) {
return new GrowTransition(child:ImageLogo,animation: animation);
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
//資源釋放
controller.dispose();
}
}
複製程式碼
可以看到在GrowTransition
定義兩個Tween
動畫,並且加了不透明Opacity
widget,最後在initState
方法中修改增加一句animation = new CurvedAnimation(parent: controller, curve: Curves.easeIn)
,最後的動畫效果:
Curves.easeIn
值來實現非線性運動效果。
2.自定義動畫
先上效果圖:
2.1.自定義小球
class _bollView extends CustomPainter{
//顏色
Color color;
//數量
int count;
//集合放動畫
List<Animation<double>> ListAnimators;
_bollView({this.color,this.count,this.ListAnimators});
@override
void paint(Canvas canvas,Size size){
//繪製流程
double boll_radius = (size.width - 15) / 8;
Paint paint = new Paint();
paint.color = color;
paint.style = PaintingStyle.fill;
//因為這個wiaget是80 球和球之間相隔5
for(int i = 0; i < count;i++){
double value = ListAnimators[i].value;
//確定圓心 半徑 畫筆
//第一個球 r
//第二個球 5 + 3r
//第三個球 15 + 5r
//第四個球 30 + 7r
//半徑也是隨著動畫值改變
canvas.drawCircle(new Offset((i+1) * boll_radius + i * boll_radius + i * 5,size.height / 2), boll_radius * (value > 1 ? (2 - value) : value), paint);
}
}
//重新整理是否重繪
@override
bool shouldRepaint(CustomPainter oldDelegate){
return oldDelegate != this;
}
}
複製程式碼
2.2.配置小球屬性
class MyBalls extends StatefulWidget{
Size size;
Color color;
int count;
int seconds;
//預設四個小球 紅色
MyBalls({this.size,this.seconds : 400,this.color :Colors.redAccent,this.count : 4});
@override
State<StatefulWidget> createState(){
return MyBallsState();
}
}
複製程式碼
2.3.建立動畫
//繼承TickerProviderStateMixin,提供Ticker物件
class MyBallsState extends State<MyBalls> with TickerProviderStateMixin {
//動畫集合
List<Animation<double>>animatios = [];
//控制器集合
List<AnimationController> animationControllers = [];
//顏色
Animation<Color> colors;
@override
void initState(){
super.initState();
for(int i = 0;i < widget.count;i++){
//建立動畫控制器
AnimationController animationController = new AnimationController(
vsync: this,
duration: Duration(
milliseconds: widget.count * widget.seconds
));
//新增到控制器集合
animationControllers.add(animationController);
//顏色隨機
colors = ColorTween(begin: Colors.red,end:Colors.green).animate(animationController);
//建立動畫 每個動畫都要繫結控制器
Animation<double> animation = new Tween(begin: 0.1,end:1.9).animate(animationController);
animatios.add(animation);
}
animatios[0].addListener((){
//重新整理
setState(() {
});
});
//延遲執行
var delay = (widget.seconds ~/ (2 * animatios.length - 2));
for(int i = 0;i < animatios.length;i++){
Future.delayed(Duration(milliseconds: delay * i),(){
animationControllers[i]
..repeat().orCancel;
});
}
}
@override
Widget build(BuildContext context){
return new CustomPaint(
//自定義畫筆
painter: _bollView(color: colors.value,count: widget.count,ListAnimators : animatios),
size: widget.size,
);
}
//釋放資源
@override
void dispose(){
super.dispose();
animatios[0].removeListener((){
setState(() {
});
});
animationControllers[0].dispose();
}
}
複製程式碼
2.4.呼叫
class Ball extends StatelessWidget{
@override
Widget build(BuildContext context){
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Animation demo'),
),
body: Center(
child: MyBalls(size: new Size(80.0,20.0)),
),
),
);
}
}
複製程式碼
五、總結
- 寫佈局時,
Flutter
佈局都是物件,可以用變數值取記錄,相比Android
來說,這複用性很高,但是寫複雜佈局時,會一行一行堆疊,括號滿腦子飛。 - 不像
Android
,佈局和實現邏輯分開,所有一切都寫在Dart
中,需要做好封裝和職責分明。 - 頁面跳轉和
Android
一樣,是棧的思想。 Android
中,通過Xml
方式或者animate()
在View上呼叫,在Flutter
需要到動畫的Widget
可以使用動畫庫將動畫封裝在Widget
上。
如有不正之處歡迎大家批評指正~