//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 {
//列表要显示的数据
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: [
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 {
//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 {
@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 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,
);
//补间动画
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 animation})
:super(key:key,listenable:animation);
@override
Widget build(BuildContext context){
final Animation 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 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,
);
//补间动画
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 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 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 sizeAnimation =
new Tween(begin: 0.0, end: 300.0).animate(controller);
final Animation 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 animation;
GrowTransition({this.child, this.animation});
static final _opacityTween = new Tween(begin: 0.1, end: 1.0);
static final _sizeTween = new Tween(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 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,
);
//新增
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> ListAnimators;
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
er.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)
,最后的动画效果:
[外链图片转存中…(img-egNIlIwM-1715530385080)]
注意:可以通过改变Curves.easeIn
值来实现非线性运动效果。
2.自定义动画
先上效果图:
[外链图片转存中…(img-yh2SxIHr-1715530385080)]
2.1.自定义小球
class _bollView extends CustomPainter{
//颜色
Color color;
//数量
int count;
//集合放动画
List<Animation> ListAnimators;
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-qlq8eaLp-1715530385082)]
[外链图片转存中…(img-KvaZYh14-1715530385083)]
[外链图片转存中…(img-k0ElfmXA-1715530385084)]
[外链图片转存中…(img-r5WyUwXO-1715530385085)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!