一、前言
前一天学习了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: [
//第一行
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: [
Text("分类:",
style: getTextStyle(Colors.blue,14,true),
),
getText("多线程", l1),
getText("Java深入", l2),
],
);
//根Widget
Widget ColumnWidget = Column(
//主轴上设置居中
mainAxisAlignment: MainAxisAlignment.center,
//交叉轴(水平方向)设置从左开始
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//第一行
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: [
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: [
//第一行
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: [
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: [
//电影名称
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: [
Text('主演:'),
//以Row从左到右排列头像
Row(
children: [
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: [
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: [
Expanded(
child: Row(
children: [
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: [
Expanded(
child: Row(
children: [
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: [
//第一行
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: [
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: [
cardWidget,
],
);
}),
onRefresh: _onRefresh,),
);
//下拉刷新方法
Future _onRefresh() async {
//写逻辑
}
复制代码
可以看到上面定义刷新方法_onRefresh
,这里先不加任何逻辑。把根Widget
继承StatefulWidget
,因为后面涉及到状态更新:
class HomeStateful extends StatefulWidget{
@override
State 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 return Column(
children: [
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 _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: [
Expanded(
child: Row(
children: [
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: [
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: [
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: [
(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 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 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 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;
//集合放动画
Listdouble>> 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 createState() {
return MyBallsState();
}
}
复制代码
2.3.创建动画
//继承TickerProviderStateMixin,提供Ticker对象
class MyBallsState extends State<MyBalls> with TickerProviderStateMixin {
//动画集合
Listdouble>>animatios = [];
//控制器集合
List animationControllers = [];
//颜色
Animation 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
上。
如有不正之处欢迎大家批评指正~