Flutter 学习之布局、交互、动画,字节跳动学习笔记
}
//延迟 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 ItemWidget _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 开始 每调用一次这个函数,迭代器就会加 1itemBuilder: _itemColumn,),onRefresh: _onRefresh,),);}
下面把下拉刷新方法逻辑简单加一下,我这边只是重新将集合清空,然后重新添加 8 条数据,只是为了看刷新效果而儿:
//下拉刷新方法 Future<Null> _onRefresh() async {//写逻辑 延迟 3 秒后执行刷新//刷新把 isfresh 改为 trueisfresh = 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()@overridevoid 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,这是永久性 移除监听 清理环境 @overridevoid 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 开始 每调用一次这个函数,迭代器就会加 1itemBuilder: _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,但没有完成 onTapthis.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.0this.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): onTapDownI/flutter (16304): onTapUpI/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): 长按 onLongPressI/flutter (16304): 长按抬起 onLongPressUPI/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): onPanDownI/flutter (16304): onPanStartI/flutter (16304): onPanUpdateI/flutter (16304): onPanUpdateI/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): onScaleStartI/flutter (16304): onScaleUpdateI/flutter (16304): onScaleUpdateI/flutter (16304): onScaleUpdateI/flutter (16304): onScaleUpdateI/flutter (16304): onScaleUpdateI/flutter (16304): onScaleUpdateI/flutter (16304): onScaleUpdateI/flutter (16304): onScaleEnd
3.原始指针事件
除了GestureDetector
能够监听触摸事件外,Pointer
代表用户与设备屏幕交互的原始数据,也就是也能监听手势:
PointerDownEvent
:指针接触到屏幕的特定位置PointerMoveEvent
:指针从屏幕上的一个位置移动到另一个位置PointMoveEvent
:指针停止接触屏幕PointUpEvent
:指针停止接触屏幕PointerCancelEvent
:指针的输入事件不再针对此应用
上代码:
//PointerWidget 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): onPointerDownI/flutter (16304): onPointerMoveeI/flutter (16304): onPointerMoveI/flutter (16304): onPointerMovesI/flutter (16304): onPointerMoveI/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 {@overrideWidget 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{@overrideState<StatefulWidget> createState(){return new HomeWidget();}
}
class HomeWidget extends State<HomeStateful> {@overrideWidget build(BuildContext context) {...//PointerWidget 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{
@overrideWidget build(BuildContext context){return HomeWidget();//注意:不需要 MaterialApp// return MaterialApp(// theme: ThemeData(// //设置为 hongse// primarySwatch: Colors.red),// home: HomeWidget(),// );
}}
class HomeWidget extends StatelessWidget{
@overrideWidget 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);
@overrideWidget build(BuildContext context){return HomeWidget(str);}}
class HomeWidget extends StatelessWidget{String newDate;HomeWidget(this.newDate);
@overrideWidget 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{@overrideState<StatefulWidget> createState(){return new _LogoAppState();}
}
//logoWidget 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;@overridevoid initState() {// TODO: implement initStatesuper.initState();//创建 AnimationController//需要传递一个 vsync 参数,存在 vsync 时会防止屏幕外动画(//译者语:动画的 UI 不在当前屏幕时)消耗不必要的资源。 通过将 SingleTickerProviderStateMixin 添加到类定义中,可以将 stateful 对象作为 vsync 的值。controller = new AnimationController(//时间是 3000 毫秒 duration: const Duration(milliseconds: 3000),//vsync 在此处忽略不必要的情况 vsync: this,);//补间动画 animation = new Tween(//开始的值是 0begin: 0.0,//结束的值是 200end : 200.0,).animate(controller)//添加监听器..addListener((){//动画值在发生变化时就会调用 setState(() {
});});//只显示动画一次 controller.forward();}@overrideWidget 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,),),),);}
@overridevoid dispose() {// TODO: implement disposesuper.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{@overrideState<StatefulWidget> createState(){return new _LogoAppState();}
}
//logoWidget ImageLogo = new Image(image: new AssetImage('images/logo.jpg'),);
//抽象出来 class AnimatedLogo extends AnimatedWidget{AnimatedLogo({Key key,Animation<double> animation}):super(key:key,listenable:animation);
@overrideWidget 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;@overridevoid initState() {// TODO: implement initStatesuper.initState();//创建 AnimationController//需要传递一个 vsync 参数,存在 vsync 时会防止屏幕外动画(//译者语:动画的 UI 不在当前屏幕时)消耗不必要的资源。 通过将 SingleTickerProviderStateMixin 添加到类定义中,可以将 stateful 对象作为 vsync 的值。controller = new AnimationController(//时间是 3000 毫秒 duration: const Duration(milliseconds: 3000),//vsync 在此处忽略不必要的情况 vsync: this,);//补间动画 animation = new Tween(//开始的值是 0begin: 0.0,//结束的值是 200end : 200.0,).animate(controller);//添加监听器//只显示动画一次 controller.forward();}
@overrideWidget build(BuildContext context){return AnimatedLogo(animation: animation);}
@overridevoid dispose() {// TODO: implement disposesuper.dispose();//资源释放 controller.dispose();}
}
可以发现AnimatedWidget
中会自动调用addListener
和setState()
,_LogoAppState
将Animation
对象传递给基类并用animation.value
设置 Image 宽高。
1.2.监视动画
在平时开发,我们知道,很多时候都需要监听动画的状态,好像完成、前进、倒退等。在Flutter
中可以通过addStatusListener()
来得到这个通知,以下代码添加了动画状态
//补间动画 animation = new Tween(//开始的值是 0begin: 0.0,//结束的值是 200end : 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=25KBI/zygote64(16745): After code cache collection, code=30KB, data=25KBI/zygote64(16745): Increasing code cache capacity to 128KBI/flutter (16745): AnimationStatus.completed//动画完成
下面那就运用addStatusListener()
在开始或结束反转动画。那就产生循环效果:
//补间动画 animation = new Tween(//开始的值是 0begin: 0.0,//结束的值是 200end : 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()
//AnimatedBuilderclass GrowTransition extends StatelessWidget{final Widget child;final Animation<double> animation;GrowTransition({this.child,this.animation});
@overrideWidget 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;@overridevoid initSta
te() {// TODO: implement initStatesuper.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(//开始的值是 0begin: 0.0,//结束的值是 200end : 200.0,).animate(curve)// //添加动画状态..addStatusListener((state){//如果动画完成了 if(state == AnimationStatus.completed){//开始反向这动画 controller.reverse();} else if(state == AnimationStatus.dismissed){//开始向前运行着动画 controller.forward();}
});//添加监听器//只显示动画一次 controller.forward();}
@overrideWidget build(BuildContext context){//return AnimatedLogo(animation: animation);return new GrowTransition(child:ImageLogo,animation: animation);}
@override
评论