该文章是王叔不秃flutter系列视频的学习笔记
网络测试接口
github: https://api.github.com/events getsnh48: h5.48.cn/resource/js… get
快捷键
- 1、快速创建widget:在dart文件中输入stf或stl出现提示后按回车即可
- 2、快速修复:option +回车
- 3、自动生成构造函数:选中 final参数,快捷键:option +回车
- 4、
添加父组件、变为子组件、删除子组件:option+回车 - 5、万能的搜索:双击shift
- 6、查看最近打开的文件:command + E
- 7、查看当前类结构:command + fn + f12
- 8、查看源码:将光标放到要查看源码的类名或方法名上,长按command然后的点击
- 9、查看类的子类:选中要查看的类,然后: command +B 或 option + command + B
- 10、将代码更新到模拟器上:选中模拟器然后 command +R
- 11、导入类的快捷键:将光标放在要导入类的上面,然后按 option + enter
- 12、前进后退:当跟踪代码的时候,经常跳转到其他类,后退快捷键:option+command+方向左键,前进快捷键:option+command+方向右键
- 13、全局搜索:command + shift + F
- 14、全局替换:command + shift + R
- 15、查找引用:option + shift + F7
- 16、重命名:fn+shift+f6
注:以上快捷键是在Android Studio 的macOS的keymap下,如果是Windows系统,将command 换成Ctrl,option换成Alt即可。
注意:该部分 copy于# Android Studio开发flutter快捷键
空安全 Null-safety
为什么要做空安全
把运行时的错误尽量提前到编译时的错误。
优点: 速度快、效率高、更安全(不是绝对的安全)
空安全的类型
- 空安全类型汇总
- Null !== 0
如何处理空安全
int addOne(int x) {
return x + 1;
}
- 只有值不为空时才调用
_nullSafety0() {
int? i;
//只有值不为空时才调用
if (i != null) {
addOne(i);
}
}
- 调用时传入一个预备值
_nullSafety1() {
int? i;
//调用时传入一个预备值
addOne(i ?? 0);
}
- 用感叹号强制解包,保证值不为空。这种方法尽量避免
_nullSafety2() {
int? i;
// 用感叹号强制解包,保证值不为空
addOne(i!);
}
as 关键字
- as 可以将某个值
向下解包成某种类型,但是如果这个值如果不能被转换成目标类型时,就会崩溃
main {
int? i;
addOne(i as int);
}
late 关键字
- 声明一个非空的变量,但是没有马上给它赋值。
需要在之后某个时机才会赋值,再赋值前使用就会发生错误
late int id;
late String name;
late Color color;
- 延后执行:
虽然在声明的时候已经赋值,但是其实并没有,只有真正使用到时才会赋值.类似于swift中lazy关键字
late int id = 100;
- dart语言中,所有的Class以外的全局变量都会默认加上 late关键字
//这个name会默认被dart编译器加上late关键字
String name = 'lee'; // ==> late String name = 'lee';
class Demo {
}
健全的空安全
健全的空安全:每一个模块都是空安全 --> 那就是健全的空安全
布局
布局流
- 向下传递约束
- 向上传递尺寸
获取和设置布局约束
获取约束 -LayoutBuilder
body: Container(
width: 300,
height: 300,
//使用 LayoutBuilder,就可以获得上级对下级的 constraints
child: Center(
child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
print('constraints: $constraints');
return FlutterLogo(size: 200,);
},),
)
)
打印结果
flutter: constraints: BoxConstraints(0.0<=w<=300.0, 0.0<=h<=300.0) 说明FlutterLogo 大小只要在满足0.0<=w<=300.0, 0.0<=h<=300.0,都是可以的正常显示的
松约束 - 紧约束
- loosen-constraints:松约束,最小约束 == 0,eg:(0.0<=w<=120.0, 0.0<=h<=120.0), BoxConstraints源码=>
BoxConstraints.loose(Size size)
: minWidth = 0.0,
maxWidth = size.width,
minHeight = 0.0,
maxHeight = size.height;
- tightly-constraints:紧约束,最小约束和最大约束相等,eg:(60.0<=w<=60.0, 120.0<=h<=120.0),BoxConstraints源码=>
BoxConstraints.tight(Size size)
: minWidth = size.width,
maxWidth = size.width,
minHeight = size.height,
maxHeight = size.height;
- 将尺寸约束变为一个松约束 --> loosen()
child: ConstrainedBox(
constraints: const BoxConstraints(
minHeight: 60,
maxHeight: 120,
minWidth: 60,
maxWidth: 120
).loosen(),//loosen(),
child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
print('constraints: $constraints');
return FlutterLogo(size: 200,);
},),
),
- 不加loosen() 约束:constraints: BoxConstraints(60.0<=w<=120.0, 60.0<=h<=120.0)
- 加loosen() 约束:BoxConstraints(0.0<=w<=120.0, 0.0<=h<=120.0)
可以看到变为了minHeight和minWidth是0。--> 在BoxConstraints术语中如果minHeight == minWidth == 0时,就是松约束,其实就是
约束变为了类似 Width == Height == 120的 Center() - 松约束 和 紧约束
不是对立的。eg: (0.0<=w<=0.0, 0.0<=h<=0.0),
Padding 会 欺上瞒下
padding会修改子部件的约束
body: Container(
width: 300,
height: 300,
//使用 LayoutBuilder,就可以获得上级对下级的 constraints
child: Padding(
padding: const EdgeInsets.all(10.0),
child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
print('constraints: $constraints');
return FlutterLogo(size: 200,);
},),
)
)
==> flutter: constraints: BoxConstraints(w=280.0, h=280.0),而并不是(300,300)
Flex
- 内部子组件分为弹性的和非弹性的
- 计算高度时
先计算非弹性的,再计算弹性的
Stack
- 内部子组件分为有位置的和没有位置的
- stack布局
先计算有没有位置的,再计算有位置的 - stack的大小 == 所有没有位置组件中最大组件的大小
- 超出stack大小的控件,
超出的部分不能响应点击事件 - Positioned组件只能在Stack中用,不属于
StatefulWidget、StatelessWidget、RenderObjectWidget任何一种,
属于ParentDataWidget这类的组件只能给子组件提供数据
Container
- 在Container 中有子部件的时候,会紧紧包裹子部件
- 在Container中没有子部件的时候,Container 大小 == 所给约束的最大
- Container是个什么 ===>
集合了很多个组件,以属性对外提供组件能力。避免组件嵌套太多层。eg:图中 padding ==> Padding、alignment ==> Align...Container源码。可以看到其实当设置padding 就是 会自动加上Padding组件
Widget build(BuildContext context) {
Widget? current = child;
if (child == null && (constraints == null || !constraints!.isTight)) {
current = LimitedBox(
maxWidth: 0.0,
maxHeight: 0.0,
child: ConstrainedBox(constraints: const BoxConstraints.expand()),
);
} else if (alignment != null) {
current = Align(alignment: alignment!, child: current);
}
final EdgeInsetsGeometry? effectivePadding = _paddingIncludingDecoration;
if (effectivePadding != null)//可以看到其实当设置padding 就是 会自动加上Padding组件
current = Padding(padding: effectivePadding, child: current);
if (color != null)
current = ColoredBox(color: color!, child: current);
if (clipBehavior != Clip.none) {
assert(decoration != null);
current = ClipPath(
clipper: _DecorationClipper(
textDirection: Directionality.maybeOf(context),
decoration: decoration!,
),
clipBehavior: clipBehavior,
child: current,
);
}
Container大小
- 没有child就越大越好,除非约束无边界
- 有child就匹配尺寸,除非Container要对齐
CustomMultiChildLayout
- 可以自定义内部子部件的尺寸
- 需要遵循代理
SingleChildRenderObjectWidget
自定义绘制组件 手动实现一个 RenderObject --> Flutter 教程 Layout-7 自己动手写个RenderObject
滚动
ListView
重用、缓冲数量
- 使用ListView.build()去构建,其缓冲区的大小大概是上下各多
屏幕大小的1/3;eg: 当屏幕能显示9个cell时,上下未显示的cell各有3个; 而当屏幕能显示90个cell时,上下未显示的cell各有30个; - cacheExtent: 控制缓冲数量
- itemExtent: 强制每个cell的高度,用于大幅加载
- padding: 主轴方向一直生效,而交叉轴方向只有滑到最顶部和最底部才生效,和Padding控件略有不同
- physics: 滚动状态,iOSor安卓
ListView.builder(
itemCount: 100,
cacheExtent: 0, //缓冲数量
itemExtent: 60, //紧约束,控制每个item的高度,这里设置了,itemBuilder设置的高度就不会生效
padding: const EdgeInsets.all(20.0),
itemBuilder: (BuildContext context, int index) {
print('$index'); //加个打印就能判断出来
return Text('dddd');
}),
分割线
使用 ListView.separated -> separatorBuilder
body: ListView.separated(
itemCount: 10,
cacheExtent: 0, //缓冲数量
separatorBuilder: (BuildContext context, int index) {
print('Divider $index');
return Divider(
thickness: 1,
color: Colors.red,
);
},
itemBuilder: (BuildContext context, int index) {
print('Text $index');
return Text('dddd');
}),
);
滚动条 - ScrollBar
- 外面加上ScrollBar就可以了,如果要使用iOS风格的就需要用到;
- 如果要使用controller,那就要和ListView使用同一个controller
Scrollbar(
controller: _scrollController,
child: ListView.builder(
controller: _scrollController,
itemBuilder: (BuildContext context, int index) {
print('Text $index');
return Container(
height: 40,
color: Colors.red,
child: Text('dddd'),
);
}),
),
- 原理:(滑动)事件向上传递,通过NotificationListener监听
下拉刷新 - RefreshIndicator
RefreshIndicator(
onRefresh: () {
return Future.delayed(Duration(seconds: 2));
},
child: ListView.builder(
itemCount: 50,
itemBuilder: (BuildContext context, int index) {
print('Text $index');
return Container(
height: 40,
color: Colors.red,
child: Text('dddd'),
);
}),
),
滑动删除 - Dismissible
ListView.builder(
itemCount: 50,
itemBuilder: (BuildContext context, int index) {
print('Text $index');
return Dismissible(
key: UniqueKey(),
//左滑
background: Container(
alignment: Alignment.centerLeft,
color: Colors.green,
child: Icon(Icons.phone),
),
//右滑
secondaryBackground: Container(
alignment: Alignment.centerRight,
color: Colors.blue,
child: Icon(Icons.message),
),
//滑动 是否删除
confirmDismiss: (direction) async {
if (direction == DismissDirection.endToStart) {
await Future.delayed(Duration(seconds: 1));
return true;
} else {
return false;
}
},
onDismissed: (direction) {
print('$direction');
if (direction == DismissDirection.endToStart) {
// 删除源数据操作...
listDatas.removeWhere((e) => e.id == '9527');
}
},
child: Container(
height: 40,
color: Colors.red[index % 9 * 100]
),
);
}),
GridView - 二维网格
- gridDelegate: 如何显示网格
body: GridView.builder(
itemCount: 50,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,//主轴方向有几个
childAspectRatio: 16/9,//每个item宽高比例,默认1:1
),
itemBuilder: (BuildContext context, int index) {
print('Text $index');
return Container(
height: 40,
color: Colors.red[index % 8 * 100]
);
}),
);
- GridView.count
body: GridView.count(
crossAxisCount: 5,
children: const [
Icon(Icons.add),
Icon(Icons.add),
Icon(Icons.add),
Icon(Icons.add),
],
),
大小如何计算:
- 1.width:
先计算主轴的宽度/个数 - 2.height: 根据
childAspectRatio(默认1:1) 由width计算height - 横竖屏也会按比列来
ListWheelScrollView - 转轮类似于iOS中的UIPickView
ListWheelScrollView(
//高度
itemExtent: 50,
//偏离中心显示透明度
overAndUnderCenterOpacity: 0.5,
// offAxisFraction: 1.5,
useMagnifier: true,
//滚动到某个item固定效果
physics: FixedExtentScrollPhysics(),
onSelectedItemChanged: (index) {
print('$index');
},
children: List.generate(10, (index) => Container(
alignment: Alignment.center,
color: Colors.red[200],
child: Text('${index}'),
),),
)
- 横向滚动 很遗憾
ListWheelScrollView不支持横向滚动,但是我们可以使用RotatedBox处理横向问题
PageView
PageView(
//滚动事件
onPageChanged: (index) => print('$index'),
//滑动方向
scrollDirection: Axis.vertical,
children: [
Container(color: Colors.red,),
Container(color: Colors.orangeAccent,),
],
)
ReorderableListView 可拖动的换位置的listView
ReorderableListView(
header: Container(
alignment: Alignment.center,
height: 40,
color: Colors.green,
key: UniqueKey(),//注意需要唯一的Key
child: const Text('这个是Header 不支持拖动'),
),
children: List.generate(10, (index) => Container(
alignment: Alignment.center,
height: 60,
color: Colors.red[index % 9 *100],
key: UniqueKey(),//注意需要唯一的Key
child: Text('$index'),)
),
onReorder: (int newIndex, int oldIndex) {
print('newIndex:$newIndex ,oldIndex:$oldIndex');
},
)
SingleChildScrollView
- 处理单个视图的滚动,eg:
Column本身不支持滚动,但是内容太过多就会溢出,可以考虑使用SingleChildScrollView - 当内容不溢出的时候,是不滚动的
- 但是不推荐这么使用,因为SingleChildScrollView 会导致layout两遍,效率很低
SingleChildScrollView(
child: Column(
children: [
FlutterLogo(size:500),
FlutterLogo(size:400),
],
),
)
异步
简单使用
以下两种都是异步操作
//.then
void _incrementCounter() {
Future.delayed(Duration(seconds: 2)).then((value) {
setState(() {
_counter++;
});
});
}
//async await
void _incrementCounter2() async {
await Future.delayed(Duration(seconds: 2));
setState(() {
_counter++;
});
}
事件循环Event Loop
dart的异步操作不是多线程
事件队列
- EventQueue:事件队列
- MicrotaskQueue: 微任务事件,优先级大于EventQueue
事件执行类型
- 直接运行
void futureTest() {
print('1');
Future.sync(() => print('Future.sync'));
Future.value(_getName());
print('2');
}
打印
flutter: 1
flutter: Future.sync
flutter: Closure: <Y0>([FutureOr<Y0>?]) => Future<Y0> from Function 'Future.value': static.
flutter: 2
- Microtask
void futureTest() {
print('1');
scheduleMicrotask(() => print('Microtask 1'));
Future.microtask(() => print('Microtask 2'));
Future.value(123).then((value) => print('Microtask 3'));
print('2');
}
打印
flutter: 1
flutter: 2
flutter: Microtask 1
flutter: Microtask 2
flutter: Microtask 3
- event
void futureTest() {
print('1');
Future.delayed(Duration(seconds: 1)).then((value) => print('event 1'));
print('2');
}
打印
flutter: 1
flutter: 2
flutter: event 1
Future
Future的三种状态
http.get('www.baidu.com')//未完成态
.then((value) => null)//完成态
.catchError(onError)//错误态
.whenComplete(() => null);//完成态
.then 可以多个使用
Future<int> futureTest1() {
return Future.value(100);
}
void futureTest() {
futureTest1()
.then((value) {
print(value);
return value * 2;
}).then((value) => print(value));
打印
flutter: 100
flutter: 200
FutureBuilder
child: FutureBuilder(
// future: Future.delayed(Duration(seconds: 2)).then((value) => 555),
future: Future.delayed(Duration(seconds: 2), () => 555),
// future: Future.delayed(Duration(seconds: 2)).then((value) => throw('错误')),
initialData: 72,//没有完成前
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
//方式一
// if (snapshot.connectionState == ConnectionState.waiting) {
// return CircularProgressIndicator();
// }
// if (snapshot.connectionState == ConnectionState.done) {
// print(snapshot.error);
// print(snapshot.data);
// if (snapshot.hasError) {
// return Icon(Icons.error);
// }
// if (snapshot.hasData) {
// return Text('${snapshot.data}',style: TextStyle(fontSize: 72),);
// }
// }
// throw "should not happen";
//方式二
if (snapshot.hasData) {
return Text('${snapshot.data}',style: TextStyle(fontSize: 72),);
}
if (snapshot.hasError) {
return Icon(Icons.error);
}
return CircularProgressIndicator();
},
),
Stream & StreamBuilder
如果说Future是一次等待处理,那么Stream 就是多次等待处理
创建
final _testStream = Stream.periodic(Duration(seconds: 1), (_) => 44);
final _streamController = StreamController();//一个stream监听, 事件会被缓存
final _streamController_broadcast = StreamController.broadcast();//多个监听,事件不会被缓存
事件 sink.add、listen
- 添加事件:_streamController.sink.add(77);
- 监听事件:_streamController.stream.listen((event) { });
- 组合事件:
_streamController.stream
.map((event) => event * 2)//map可以处理event
.where((event) => event is int)//where可以过滤
.distinct();//可以去重
需要关闭,一旦关闭在去add就会奔溃
@override
void dispose() {
//需要关闭
_streamController.close();
super.dispose();
}
- Steam流方法
Stream<DateTime> _getTime() async* {
while (true) {
await Future.delayed(Duration(seconds: 1));
yield DateTime.now();
}
}
使用列子
final _testStream = Stream.periodic(Duration(seconds: 1), (_) => 44);
final _streamController = StreamController();//一个stream监听, 事件会被缓存
final _streamController_broadcast = StreamController.broadcast();//多个监听,事件不会被缓存
_dd() {
_streamController.sink.add(77);
_streamController.stream.listen((event) { });
_streamController.stream
.map((event) => event * 2)//map可以处理event
.where((event) => event is int)//where可以过滤
.distinct();//可以去重
}
Stream<DateTime> _getTime() async* {
while (true) {
await Future.delayed(Duration(seconds: 1));
yield DateTime.now();
}
}
@override
void dispose() {
// TODO: implement dispose
//需要关闭
_streamController.close();
super.dispose();
}
Column(
children: [
RaisedButton(onPressed: () => _streamController.sink.add(1),child: Text('add1'),),
RaisedButton(onPressed: () => _streamController.sink.add(99),child: Text('add99'),),
//close() 之后就不能再接受数据了,会崩溃
RaisedButton(onPressed: () => _streamController.sink.close(),child: Text('Close'),),
StreamBuilder(
//多种情况的stream
// stream: null,
// stream: _testStream,
// stream: _streamController.stream,
stream: _getTime(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return Text("none: 没有数据");
break;
case ConnectionState.waiting:
return Text("waiting: 等待");
break;
case ConnectionState.active:
if (snapshot.hasError) {
return Text("active: 错误:${snapshot.error}");
} else {
return Text("active: 正常:${snapshot.data}");
}
break;
case ConnectionState.done:
return Text("Done");
break;
}
return Container();
}),
],
),
Sliver - 片段
Sliver 是滚动视图中的片段,需要通过视窗(一般是滚动的容器:CustomScrollView)才能使用Sliver片段
- 使用Sliver后,内部的子组件都应该是Sliver开头的子组件。eg:SliverGrid,SliverList、、、
使用
- SliverToBoxAdapter: 用于转换具体的组件,一对一
- SliverList: 类似ListView,内部可以放好多
- SliverGrid: 类似GridView,内部可以放好多
body: CustomScrollView(
slivers: [
SliverToBoxAdapter(child: FlutterLogo(size: 100,)),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
height: 50,
color: Colors.primaries[Random().nextInt(18)],
);
},
childCount: 4,//定义个数
),
),
SliverGrid(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
height: 50,
color: Colors.primaries[Random().nextInt(18)],
);
},
childCount: 20,//定义个数
),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
),
),
],
)
各种各样的Sliver
- SliverPrototypeExtentList: 通过prototypeItem 可以做到不同设备间的适配
SliverPrototypeExtentList(
prototypeItem: FlutterLogo(size: 30,),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
height: 50,
color: Colors.primaries[Random().nextInt(18)],
);
},
childCount: 4,//定义个数
),
),
- SliverFillViewport:每个子部件都是占满屏幕的,类似pageView
SliverFillViewport(delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
color: Colors.primaries[Random().nextInt(18)],
);
},
childCount: 4,//定义个数
),)
SliverAppBar 可以做到动态的AppBar
- 类似上滑隐藏AppBar
SliverAppBar(title: Text('SliverAppBar'),
floating: true,
// pinned: true,
snap: true,
expandedHeight: 300,
flexibleSpace: FlexibleSpaceBar(
background: FlutterLogo(),
title: Text('FlexibleSpaceBar'),
collapseMode: CollapseMode.parallax,//上滑效果
stretchModes: [ //下拉效果
StretchMode.blurBackground,
StretchMode.zoomBackground,
StretchMode.fadeTitle,
],
),
),
SliverOpacity
注意内部不是child 是 sliver
SliverOpacity(
opacity: 0.5,
sliver: SliverToBoxAdapter(
child: FlutterLogo(
size: 100,
))),
SliverAnimatedOpacity
SliverAnimatedOpacity(
opacity: 0.5,
duration: Duration(seconds: 3),
sliver: SliverToBoxAdapter(
child: FlutterLogo(
size: 100,
)),
),
SliverFillRemaining
- 填充剩余空间
- 一般用于Sliver 最底部
SliverFillRemaining(child: Placeholder(),),
SliverFillRemaining(child: Center(child: CircularProgressIndicator(),),),
SliverLayoutBuilder 用于查看约束,便于理解sliver原理
SliverLayoutBuilder(builder: (BuildContext context, SliverConstraints constraints) {
print('$constraints');
return SliverToBoxAdapter();
}),
打印
flutter: SliverConstraints(AxisDirection.down, GrowthDirection.forward, ScrollDirection.forward, scrollOffset: 781.6, remainingPaintExtent: 667.0, crossAxisExtent: 375.0, crossAxisDirection: AxisDirection.right, viewportMainAxisExtent: 667.0, remainingCacheExtent: 1167.0, cacheOrigin: -250.0)
SliverPersistentHeader
固定头效果
SliverPersistentHeader(delegate: delegate)
BuildContext
BuildContext到底是什么:看源码
/// [BuildContext] objects are actually [Element] objects. The [BuildContext]
/// interface is used to discourage direct manipulation of [Element] objects.
abstract class BuildContext {
...
}
==> 第一句: BuildContext是element
==> 第二句: 不鼓励直接操作[Element] 对象
setState
- 使用setState 会触发重绘
int _counter = 0;
void _incrementCounter() {
//单独_counter++ 是不生效的
_counter++;
//setState 触发重绘 才会生效
setState(() {
_counter++;
});
}
- 原理: 看源码 => 可以看到setState将当前element 标记需要重新build,所以后面会调用build方法
@protected
void setState(VoidCallback fn) {
//...
//...
_element!.markNeedsBuild();
}
- 所以我们也可以手动强制使用markNeedsBuild(),也可以触发页面刷新; eg:
int _counter = 0;
void _incrementCounter() {
_counter++;
(context as Element).markNeedsBuild();
}
但是直接操作Element是不被推荐的,因为有性能上的浪费
XXX.of(context)
Scaffold.of(context).showSnackBar(snackbar);
Theme.of(context)
平常我们接触很多类似上面这种context的用法。那这个context是什么呢
==> XXX.of(context)方法通过传入的context找到当前Widget树上面存在的第一个对应的state 然后做操作。eg:
Scaffold.of(context).showSnackBar(snackbar);
static ScaffoldState of(BuildContext context) {
assert(context != null);
final ScaffoldState? result = context.findAncestorStateOfType<ScaffoldState>();
...
...
}
Scaffold.of内部是,通过context.findAncestorStateOfType方法找到ScaffoldState类型的state,再返回。这个查找的顺序是从当前context往上找;
组件拆分会增加层次,能避免找不到对应的组件
下面的Drawer在点击FloatingActionButton是会崩溃的,显示不了Drawer,因为当前FloatingActionButton和Scaffold 在同一层级找不到Scaffold
class _TestPageState extends State<TestPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Sliver'),
),
body: Container(
height: 50,
color: Colors.red,
),
drawer: Drawer(),
floatingActionButton:FloatingActionButton(onPressed: () {
Scaffold.of(context).openDrawer();
}),
);
}
}
应该拆分出来,这样Foo就是TestPage的下级了,而TestPage内部有Scaffold,就能找到,所以能打开Drawer()
class _TestPageState extends State<TestPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Sliver'),
),
body: Container(
height: 50,
color: Colors.red,
),
drawer: Drawer(),
floatingActionButton: Foo(),
);
}
}
class Foo extends StatelessWidget {
const Foo({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return FloatingActionButton(onPressed: () {
Scaffold.of(context).openDrawer();
});
}
}
build是什么
上面的情况还可以使用LayoutBuilder,因为LayoutBuilder的不属于当前的context 不属于当前的context。
Widget build(BuildContext context) {//1 context
return Scaffold(
appBar: AppBar(
title: Text('Sliver'),
),
body: Container(
height: 50,
color: Colors.red,
),
drawer: Drawer(),
floatingActionButton: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {// 2 context
return FloatingActionButton(onPressed: () {
Scaffold.of(context).openDrawer();//3 context
});
}),
);
}
}
可以看到当光标选择context的时候,2和3 是同一个context,而1不是
到这里我们可以知道builder内部其实是创建一个匿名的组件,会有一个新的context
key
key: 保证widget的唯一性。
key的原理
widget和state是分开的,下图中color是在widget中的,_count是在state中的
- 先判断类型,再判断key
- 同级寻找对应关系
StatefulWidget 才需要key,StatelessWidget不需要key
key的类型
-
local key: 局部key,
只校验同级,性能好.下面中还有两个valueKey(1),但是它们不是在同一级,所以是ok的 -
global key: 全局key,
校验全局,性能差
LocalKey
LocalKey有三种: valueKey, ObjectKey, UniqueKey
- valueKey: 对比的是value
TextField(key:ValueKey('name));
TextField(key:ValueKey('password));
- ObjectKey: 对比的是Object 下面两个得到的key是不相等的。
TextField(key:ObjectKey(new Student()));
TextField(key:ObjectKey(new Student()));
- UniqueKey: 唯一key,每次都会变,但是不相等 一般用于强行需要widget更新
TextField(key:UniqueKey());
TextField(key:UniqueKey());
GlobalKey
- 全局唯一
- 快速获取某个widget的(
CurrentState,CurrentWidget,CurrentContext), 注意需要通过as转换成对应类型
- CurrentState
- CurrentWidget
- CurrentContext
动画
如何选择动画
隐式动画(自动控制)
两行代码就能动起来
- 使用AnimatedContainer
class _AnimationPageState extends State<AnimationPage> {
double pHeight = 150;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Animation'),
),
body: Center(
child: AnimatedContainer(
duration: const Duration(milliseconds: 1000),
width: 300,
height: pHeight,
color: Colors.red,
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
pHeight += 100.0;
});
},
),
);
}
}
ps: 不影响child内部的效果
- 蓄水效果
在不同控件之间切换的过渡动画
- 使用
AnimatedSwitcher可以做到在不同控件间有过度动画
AnimatedSwitcher(
duration: const Duration(milliseconds: 1000),
child: isOk ? Text('1111111') : Image.network(imageUrl) ,
),
child 需要是不同类型的控件,如果是相同是没有动画效果的;但是如果相同控件拥有不同的key,也是会有动画效果的,因为AnimatedSwitcher会先判断类型,再判断key,如果可以不同也会有动画效果
AnimatedSwitcher(
duration: const Duration(milliseconds: 1000),
child: isOk ? Text('1111111') : Text('2222222',key: GlobalKey(),) ,
),
- AnimatedSwitcher的transitionBuilder属性:控制动画的效果
AnimatedSwitcher(
transitionBuilder: (child,animation) {
//还可以组合
return RotationTransition(
turns: animation,
child: ScaleTransition(
scale: animation,
child: child,)
);
// 旋转
return RotationTransition(
turns: animation,
child: child,
);
//AnimatedSwitcher 默认的Transition
return FadeTransition(
opacity: animation,
child: child,
);
},
duration: const Duration(milliseconds: 1000),
child: isOk ? Text('1111111',style: TextStyle(fontSize: 72),) : Text('2222222',key: GlobalKey(),) ,
),
更多动画控件及曲线(Curves)
- 任何Animated控件存在
curve属性,用于指定动画变化线性关系
AnimatedPadding(
duration: Duration(seconds: 1),
curve: Curves.linear,//线性变化
//curve: Curves.bounceInOut,//弹跳变化
padding: EdgeInsets.only(top: 0),
child: Container(
width: 300,
height: 300,
color: Colors.blue,
child: null,
),
内置的还不够用? ==》万能补间动画
- TweenAnimationBuilder
- tween:代表
between,设置初始和结尾的动画效果值 - builder:每次tween中的值变化时,这个builder就会被调用
TweenAnimationBuilder(
tween: Tween(begin: 0.0,end: 1.0) ,//between 0-1
builder: (BuildContext context, value, Widget? child) {
//这里的value就是tween 中的0.0 - 1.0在1秒中的变化值
return Opacity(
opacity: value as double,
child: Container(
height: 300,
width: 300,
color: Colors.blue,
),
);
}, duration: Duration(seconds: 1),
),
- value是个泛型,可以是任何值,下面就是代表字体大小
TweenAnimationBuilder(
duration: Duration(seconds: 1),
tween: Tween<double>(begin: 20, end: 100), //between 0-1
builder: (BuildContext context, value, Widget? child) {
return Container(
height: 300,
width: 300,
color: Colors.blue,
child: Text(
'Hi',
style: TextStyle(fontSize: value as double),
),
);
},
),
实例:翻滚吧!计数器 ==》 封装成一枚动画控件
显示动画(手动控制)
可以无尽旋转的显示动画
- SingleTickerProviderStateMixin:获取硬件设备屏幕刷新参数,每次屏幕刷新就会产生一次Ticker
SingleTickerProviderStateMixin只能提供一个Ticker,如果想要多个可以使用TickerProviderStateMixin
- 要在initState中初始化AnimationController
- dispose()中要移除
- _animationController.forward();//转一次
- _animationController.reset();//重置
- _animationController.repeat();//循环
class _AnimationPageState extends State<AnimationPage> with SingleTickerProviderStateMixin{
late AnimationController _animationController;
bool _loading = true;
@override
void initState() {
_animationController = AnimationController(
duration: Duration(seconds: 1),
vsync: this// 垂直同步
);
super.initState();
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Animation'),
),
body: Center(
child: RotationTransition(
turns: _animationController,
child: Container(
height: 300,
width: 300,
color: Colors.blue,
child: Text(
'Hi',
style: TextStyle(fontSize: 100),
),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// _animationController.stop();//转一次
// _animationController.forward();//转一次
if (_loading) {
_animationController.reset();//重置
} else {
_animationController.repeat();//循环
//_animationController.repeat(reverse: true);//循环变大变小
}
_loading = !_loading;
},
),
);
}
}
动画控制器到底是个什么东西
class AnimationController extends Animation<double> {}
由源码可知AnimationController 是Animation一系列的double,动画开始的时候它会从lowerBound设置的值到upperBound设置的值中不断在duration这个时间段中借助vsync垂直同步(屏幕硬件FPS)变化而变化
_animationController = AnimationController(
duration: Duration(seconds: 1),
lowerBound: 3.0,
upperBound: 4.0,
vsync: this// 垂直同步
);
_animationController.addListener(() {
print('${_animationController.value}');
});
RotationTransition(
turns: _animationController,//[3.0..4.0]之间的一个值
child: Container(
height: 300,
width: 300,
color: Colors.blue,
child: Text(
'Hi',
style: TextStyle(fontSize: 100),
),
),
),
//打印结果
flutter: 3.0
flutter: 3.016666
flutter: 3.033333
...
flutter: 3.983334
flutter: 4.0
RotationTransition中turns就会从变化中_animationController中取值
如何使用控制器串联补间(Tween)和曲线
-小技巧: 自动创建一个带有AnimationController的快捷 ==> sta
class extends StatefulWidget {
const ({Key? key}) : super(key: key);
@override
State<> createState() => _State();
}
class _State extends State<> with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container();
}
}
- 使用
_animationController.drive(Tween(begin: 0.0,end:1.0)), 可以替换_animationController的lowerBound、upperBound
ScaleTransition(
scale: _animationController.drive(Tween(begin: 0.0,end:1.0)),
child: Container(
height: 300,
width: 300,
color: Colors.blue,
child: Text(
'Hi',
style: TextStyle(fontSize: 100),
),
),
),
- 其他写法
SlideTransition(
position: _animationController.drive(Tween(begin: Offset(0, 0),end:Offset(0.5, 0))),
position:
(Tween(begin: Offset(0, 0), end: Offset(0.5, 0)))
.chain(CurveTween(curve: Curves.easeInOut))
.chain(CurveTween(curve: Interval(0.8,1.0)))
.animate(_animationController),
child: Container(
height: 300,
width: 300,
color: Colors.blue,
),
),
交错动画
内置不够用?==》 万能自定义动画
- AnimatedBuilder:自定义各种动画,需要配合
AnimationController - 使用child,减少不必要的渲染
- 使用
Tween.evaluate更加灵活 :Tween(begin: 200.0,end:300.0).evaluate(_animationController)
_animationController =
AnimationController(duration: Duration(seconds: 1), vsync: this // 垂直同步
)
..repeat();
AnimatedBuilder(
animation: _animationController,
builder: (BuildContext context, Widget? child) {
return Opacity(
opacity: _animationController.value,
child: Container(
height: Tween<double>(begin: 200.0,end:300.0).evaluate(_animationController),
width: 300,
color: Colors.blue,
child: child,
));
},
child: const Center(
child: Text(
'Hi',
style: TextStyle(fontSize: 100),
),
),
),
),
- 还可以直接使用Animation
其他
flutter动画背后的机制和原理
- 隐式动画: 以
AnimatedContainer为例 看源码
class AnimatedContainer extends ImplicitlyAnimatedWidget {}
abstract class ImplicitlyAnimatedWidgetState<T extends ImplicitlyAnimatedWidget> extends State<T> with SingleTickerProviderStateMixin<T> {
/// The animation controller driving this widget's implicit animations.
@protected
AnimationController get controller => _controller;
late final AnimationController _controller = AnimationController(
duration: widget.duration,
debugLabel: kDebugMode ? widget.toStringShort() : null,
vsync: this,
);
可以看到AnimatedContainer 继承ImplicitlyAnimatedWidget,而ImplicitlyAnimatedWidget内部也是使用AnimationController 做动画处理的
- 显示动画: 以
AnimatedBuilder为例
//继承于AnimatedWidget
class AnimatedBuilder extends AnimatedWidget {
const AnimatedBuilder({
Key? key,
required Listenable animation,//内部监听一个animation
required this.builder,
this.child,
}) : assert(animation != null),
assert(builder != null),
super(key: key, listenable: animation);
final TransitionBuilder builder;practice.
final Widget? child;
@override
Widget build(BuildContext context) {
return builder(context, child);
}
}
abstract class AnimatedWidget extends StatefulWidget {
const AnimatedWidget({
Key? key,
required this.listenable,
}) : assert(listenable != null),
super(key: key);
@protected
Widget build(BuildContext context);
@override
State<AnimatedWidget> createState() => _AnimatedState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Listenable>('animation', listenable));
}
}
class _AnimatedState extends State<AnimatedWidget> {
@override
void initState() {
super.initState();
//添加对这个listenable的监听回调事件
widget.listenable.addListener(_handleChange);
}
@override
void didUpdateWidget(AnimatedWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.listenable != oldWidget.listenable) {
oldWidget.listenable.removeListener(_handleChange);
widget.listenable.addListener(_handleChange);
}
}
@override
void dispose() {
widget.listenable.removeListener(_handleChange);
super.dispose();
}
//回调事件会每次调用setState
void _handleChange() {
setState(() {
// The listenable's state is our build state, and it changed already.
});
}
//也就会每次重新widget.build(context)
@override
Widget build(BuildContext context) => widget.build(context);
}
总结: 监听动画控制器,当动画的数值发生变化时,就直接调用setState(),重新渲染整个控件
- 什么是Ticker: 硬件设备屏幕刷新就会有个回调
Ticker _ticker = Ticker((duration) => setState(() {}));