参照:https://book.flutterchina.club/
参照:https://flutter.cn/docs/development/platform-integration/platform-channels?tab=type-mappings-java-tab
目前进度:https://book.flutterchina.club/chapter9/animated_widgets.html(https://book.flutterchina.club/chapter9/hero.html#_9-4-1-%E8%87%AA%E5%AE%9E%E7%8E%B0hero%E5%8A%A8%E7%94%BB)
Material Design所有图标可以在其官网查看:https://material.io/tools/icons/
extension BuildContextExt on BuildContext {
/// 获取当前组件的 RenderBox
RenderBox? renderBox() {
return findRenderObject() is RenderBox ? (findRenderObject() as RenderBox) : null;
}
/// 获取当前组件的 position
Offset? position({Offset offset = Offset.zero}) {
return renderBox()?.localToGlobal(offset);
}
}
///value: 文本内容;
///fontSize : 文字的大小;
///fontWeight:文字权重;
///maxWidth:文本框的最大宽度;
///maxLines:文本支持最大多少行
static double getTextWidth(BuildContext context, String value, FontWeight? fontWeight, double? fontSize, double? maxWidth, int? maxLines) {
if (value.isEmpty) {
return 0.0;
}
TextPainter painter = TextPainter(
locale: Localizations.localeOf(context),
maxLines: maxLines,
textDirection: TextDirection.ltr,
text: TextSpan(
text: value,
style: TextStyle(
fontSize: fontSize,
fontWeight: fontWeight,
),
),
);
painter.layout(maxWidth: maxWidth ?? double.infinity);
return painter.width;
}
该组件可让您创建一个带格式的文本。
是一个可点击的Icon,不包括文字,默认没有背景,点击后会出现背景
默认带有阴影和灰色背景。按下后,阴影会变大
默认背景透明并不带阴影。按下后,会有背景色
默认有一个边框,不带阴影且背景透明。按下后,边框颜色会变亮、同时出现背景和阴影(较弱)
数据源可以是asset、文件、内存以及网络。
ImageProvider:是一个抽象类,主要定义了图片数据获取的接口load(),从不同的数据源获取图片需要实现不同的ImageProvider ,如AssetImage是实现了从Asset中加载图片的 ImageProvider,而NetworkImage 实现了从网络加载图片的 ImageProvider。
一个必选的image参数,它对应一个 ImageProvider。
属性
所有图标
开启逻辑:
flutter:
uses-material-design: true
只能定义宽度,高度也是固定的,不保存状态;
大小是固定的,无法自定义,不保存状态;
实际业务中,在正式向服务器提交数据前,都会对各个输入框数据进行合法性校验,但是对每一个TextField都分别进行校验将会是一件很麻烦的事。还有,如果用户想清除一组TextField的内容,除了一个一个清除有没有什么更好的办法呢?为此,Flutter提供了一个Form 组件,它可以对输入框进行分组,然后进行一些统一操作,如输入内容校验、输入框重置以及输入内容保存。
FormState:FormState为Form的State类,可以通过Form.of()或GlobalKey获得。我们可以通过它来对Form的子孙FormField进行统一操作。我们看看其常用的三个方法:
登录按钮的onPressed方法中不能通过Form.of(context)来获取FormState,原因是,此处的context为FormTestRoute的context,而Form.of(context)是根据所指定context向根去查找,而FormState是在FormTestRoute的子树中,所以不行。正确的做法是通过Builder来构建登录按钮,Builder会将widget节点的context作为回调参数:
属性:
Form的子孙元素必须是FormField类型,FormField是一个抽象类,定义几个属性,FormState内部通过它们来完成操作
继承自FormField类,也是TextField的一个包装类,所以除了FormField定义的属性之外,它还包括TextField的属性
Flex组件可以沿着水平或垂直方向排列子组件,如果你知道主轴方向,使用Row或Column会方便一些,因为Row和Column都继承自Flex,参数基本相同,所以能使用Flex的地方基本上都可以使用Row或Column。Flex本身功能是很强大的,它也可以和Expanded组件配合实现弹性布局。接下来我们只讨论Flex和弹性布局相关的属性(其他属性已经在介绍Row和Column时介绍过了)。
Expanded 只能作为 Flex 的孩子(否则会报错),它可以按比例“扩伸”Flex子组件所占用的空间。因为 Row和Column 都继承自 Flex,所以 Expanded 也可以作为它们的孩子。
flex参数为弹性系数,如果为 0 或null,则child是没有弹性的,即不会被扩伸占用的空间。如果大于0,所有的Expanded按照其 flex 的比例来分割主轴的全部空闲空间。
Spacer的功能是占用指定比例的空间,实际上它只是Expanded的一个包装类,相当于空白占位
溢出部分则会自动折行
层叠布局和 Web 中的绝对定位、Android 中的 Frame 布局是相似的,子组件可以根据距父容器四个角的位置来确定自身的位置。层叠布局允许子组件按照代码中声明的顺序堆叠起来。Flutter中使用Stack和Positioned这两个组件来配合实现绝对定位。Stack允许子组件堆叠,而Positioned用于根据Stack的四个角来确定子组件的位置。
简单的调整一个子元素在父元素中的位置的话,使用Align组件会更简单一些。
通过 LayoutBuilder,我们可以在布局过程中拿到父组件传递的约束信息,然后我们可以根据约束信息动态的构建不同的布局。
可以在子组件布局完成后执行一个回调,并同时将 RenderObject 对象作为参数传递。
这些具有弹性空间的布局类Widget可让您在水平(Row)和垂直(Column)方向上创建灵活的布局。其设计是基于web开发中的Flexbox布局模型。
取代线性布局 (译者语:和Android中的LinearLayout相似),Stack允许子 widget 堆叠, 你可以使用 Positioned 来定位他们相对于Stack的上下左右四条边的位置。Stacks是基于Web开发中的绝度定位(absolute positioning )布局模型设计的。
可以给其子节点添加填充(留白),和边距效果类似。
可以在其子组件绘制前(或后)绘制一些装饰(Decoration),如背景、边框、渐变等。
可以在其子组件绘制时对其应用一些矩阵变换来实现一些特效。
Transform.translate接收一个offset参数,可以在绘制时沿x、y轴对子组件平移指定的距离。
Transform.rotate可以对子组件进行旋转变换
Transform.scale可以对子组件进行缩小或放大
变换是在layout阶段,会影响在子组件的位置和大小。
Container 可让您创建矩形视觉元素。container 可以装饰为一个BoxDecoration, 如 background、一个边框、或者一个阴影。 Container 也可以具有边距(margins)、填充(padding)和应用于其大小的约束(constraints)。另外, Container可以使用矩阵在三维空间中对其进行变换。
是一个组合类容器,它本身不对应具体的RenderObject,它是DecoratedBox、ConstrainedBox、Transform、Padding、Align等组件组合的一个多功能容器,所以我们只需通过一个Container组件可以实现同时需要装饰、变换、限制的场景。
沿宽高进行圆形或椭圆裁剪
圆角矩形剪裁
默认剪裁掉子组件布局空间之外的绘制内容(溢出部分剪裁)
按照自定义的路径剪裁
getClip() 是用于获取剪裁区域的接口,由于图片大小是60×60,我们返回剪裁区域为Rect.fromLTWH(10.0, 15.0, 40.0, 30.0),即图片中部40×30像素的范围。
shouldReclip() 接口决定是否重新剪裁。如果在应用中,剪裁区域始终不会发生变化时应该返回false,这样就不会触发重新剪裁,避免不必要的性能开销。如果剪裁区域会发生变化(比如在对剪裁区域执行一个动画),那么变化后应该返回true来重新执行剪裁。
单独的一个控件超出父容器限制处理
包含:一个导航栏、导航栏右边有一个分享按钮、有一个抽屉菜单、有一个底部导航、右下角有一个悬浮的动作按钮
主要由三个角色组成:Scrollable、Viewport 和 Sliver:
具体布局过程:
用于处理滑动手势,确定滑动偏移,滑动偏移变化时构建 Viewport
用于渲染当前视口中需要显示 Sliver
Sliver 主要作用是对子组件进行构建和布局,比如 ListView 的 Sliver 需要实现子组件(列表项)按需加载功能,只有当列表项进入预渲染区域时才会去对它进行构建和布局、渲染。
Sliver 对应的渲染对象类型是 RenderSliver,RenderSliver 和 RenderBox 的相同点是都继承自 RenderObject 类,不同点是在布局的时候约束信息不同。
常用的 Sliver
Sliver名称 | 功能 | 对应的可滚动组件 |
---|---|---|
SliverList | 列表 | ListView |
SliverFixedExtentList | 高度固定的列表 | ListView,指定itemExtent时 |
SliverAnimatedList | 添加/删除列表项可以执行动画 | AnimatedList |
SliverGrid | 网格 | GridView |
SliverPrototypeExtentList | 根据原型生成高度固定的列表 | ListView,指定prototypeItem 时 |
SliverFillViewport | 包含多个子组件,每个都可以填满屏幕 | PageView |
SliverAppBar | 对应 AppBar,主要是为了在 CustomScrollView 中使用。 | |
SliverToBoxAdapter | 一个适配器,可以将 RenderBox 适配为 Sliver,后面介绍。 | |
SliverPersistentHeader | 滑动到顶部时可以固定住,后面介绍。 |
对 Sliver 进行布局、装饰的组件,它们的子组件必须是 Sliver
SliverPadding | Padding |
---|---|
SliverVisibility、SliverOpacity | Visibility、Opacity |
SliverFadeTransition | FadeTransition |
SliverLayoutBuilder | LayoutBuilder |
它最常见的使用场景是在作为 NestedScrollView 的 header
是一个Material风格的滚动指示器(滚动条),如果要给可滚动组件添加滚动条,只需将Scrollbar作为可滚动组件的任意一个父级组件即可
是 iOS 风格的滚动条,如果你使用的是Scrollbar,那么在iOS平台它会自动切换为CupertinoScrollbar。
类似于Android中的ScrollView,它只能接收一个子组件
ListView是最常用的可滚动组件之一,它可以沿一个方向线性排布所有子组件,并且它也支持列表项懒加载(在需要时才会创建)。
ListView.builder适合列表项比较多或者列表项不确定的情况
ListView.separated可以在生成的列表项之间添加一个分割组件,它比ListView.builder多了一个separatorBuilder参数,该参数是一个分割组件生成器。
ps:ListView 中的列表项组件都是 RenderBox,并不是 Sliver, 这个一定要注意。
一个 ListView 中只有一个Sliver,对列表项进行按需加载的逻辑是 Sliver 中实现的。
ListView 的 Sliver 默认是 SliverList,如果指定了 itemExtent ,则会使用 SliverFixedExtentList;如果 prototypeItem 属性不为空,则会使用 SliverPrototypeExtentList,无论是是哪个,都实现了子组件的按需加载模型。
原理:
原理: 当ScrollController和可滚动组件关联时,可滚动组件首先会调用ScrollController的createScrollPosition()方法来创建一个ScrollPosition来存储滚动位置信息,接着,可滚动组件会调用attach()方法,将创建的ScrollPosition添加到ScrollController的positions属性中,这一步称为“注册位置”,只有注册后animateTo() 和 jumpTo()才可以被调用。
当可滚动组件销毁时,会调用ScrollController的detach()方法,将其ScrollPosition对象从ScrollController的positions属性中移除,这一步称为“注销位置”,注销后animateTo() 和 jumpTo() 将不能再被调用。
需要注意的是,ScrollController的animateTo() 和 jumpTo()内部会调用所有ScrollPosition的animateTo() 和 jumpTo(),以实现所有和该ScrollController关联的可滚动组件都滚动到指定的位置。
滚动监听的类
接收到滚动事件时,参数类型为ScrollNotification,它包括一个metrics属性,它的类型是ScrollMetrics,该属性包含当前ViewPort及滚动位置等信息:
AnimatedList 和 ListView 的功能大体相似,不同的是, AnimatedList 可以在列表中插入或删除节点时执行一个动画,在需要添加或删除列表项的场景中会提高用户体验。
AnimatedList 是一个 StatefulWidget,它对应的 State 类型为 AnimatedListState,添加和删除元素的方法位于 AnimatedListState 中:
例如:
// 添加一个列表项
data.add('${++counter}');
// 告诉列表项有新添加的列表项
globalKey.currentState!.insertItem(data.length - 1);
// 移除一个列表项
globalKey.currentState!.removeItem(
index,
(context, animation) {
// 删除过程执行的是反向动画,animation.value 会从1变为0
var item = buildItem(context, index);
print('删除 ${data[index]}');
data.removeAt(index);
// 删除动画是一个合成动画:渐隐 + 缩小列表项告诉
return FadeTransition(
opacity: CurvedAnimation(
parent: animation,
//让透明度变化的更快一些
curve: const Interval(0.5, 1.0),
),
// 不断缩小列表项的高度
child: SizeTransition(
sizeFactor: animation,
axisAlignment: 0.0,
child: item,
),
);
},
duration: Duration(milliseconds: 200), // 动画时间为 200 ms
);
SliverGridDelegateWithFixedCrossAxisCount
该子类实现了一个横轴为固定数量子元素的layout算法,子元素的大小是通过crossAxisCount和childAspectRatio两个参数共同决定的。注意,这里的子元素指的是子组件的最大显示空间,注意确保子组件的实际大小不要超出子元素的空间。
SliverGridDelegateWithMaxCrossAxisExtent
该子类实现了一个横轴子元素为固定最大长度的layout算法
如果要实现页面切换和 Tab 布局,我们可以使用 PageView 组件。
虽然 PageView 的默认构造函数和 PageView.builder 构造函数中没有该参数,但它们最终都会生成一个 SliverChildDelegate 来负责列表项的按需加载,而在 SliverChildDelegate 中每当列表项构建完成后,SliverChildDelegate 都会为其添加一个 AutomaticKeepAlive 父组件。
AutomaticKeepAlive 的组件的主要作用是将列表项的根 RenderObject 的 keepAlive 按需自动标记 为 true 或 false。为了方便叙述,我们可以认为根 RenderObject 对应的组件就是列表项的根 Widget,代表整个列表项组件,同时我们将列表组件的 Viewport区域 + cacheExtent(预渲染区域)称为加载区域 :
想要缓存在页面page中处理
让Page 页变成一个 AutomaticKeepAlive Client 即可。为了便于开发者实现,Flutter 提供了一个 AutomaticKeepAliveClientMixin ,我们只需要让 PageState 混入这个 mixin,且同时添加一些必要操作即可:
class _PageState extends State with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context); // 必须调用
return Center(child: Text("${widget.text}", textScaleFactor: 5));
}
@override
bool get wantKeepAlive => true; // 是否需要缓存
}
@override
Widget build(BuildContext context) {
var children = [];
for (int i = 0; i < 6; ++i) {
//只需要用 KeepAliveWrapper 包装一下即可
children.add(KeepAliveWrapper(child:Page( text: '$i'));
}
return PageView(children: children);
}
TabBar 通常位于 AppBar 的底部,它也可以接收一个 TabController ,如果需要和 TabBarView 联动, TabBar 和 TabBarView 使用同一个 TabController 即可,注意,联动时 TabBar 和 TabBarView 的孩子数量需要一致。如果没有指定 controller,则会在组件树中向上查找并使用最近的一个 DefaultTabController 。另外我们需要创建需要的 tab 并通过 tabs 传给 TabBar, tab 可以是任何 Widget,不过Material 组件库中已经实现了一个 Tab 组件,我们一般都会直接使用它。
实战中,如果需要 TabBar 和 TabBarView 联动,通常会创建一个 DefaultTabController 作为它们共同的父级组件,这样它们在执行时就会从组件树向上查找,都会使用我们指定的这个 DefaultTabController。
TabController 用于监听和控制 TabBarView 的页面切换,通常和 TabBar 联动。如果没有指定,则会在组件树中向上查找并使用最近的一个 DefaultTabController 。
ps: ext 和 child 是互斥的,不能同时制定
Sliver 的布局协议如下:
Sliver布局模型和盒布局模型
两者布局流程基本相同:父组件告诉子组件约束信息 > 子组件根据父组件的约束确定自生大小 > 父组件获得子组件大小调整其位置。不同是:
它的功能是监听一个数据源,如果数据源发生变化,则会重新执行其 builder,
ValueListenableBuilder 和数据流向是无关的,只要数据源发生变化它就会重新构建子组件树,因此可以实现任意流向的数据共享。
ps: 关于 ValueListenableBuilder 有两点需要牢记:
1.和数据流向无关,可以实现任意流向的数据共享。
2.实践中,ValueListenableBuilder 的拆分粒度应该尽可能细,可以提高性能。
ps: 当同时监听onTap和onDoubleTap事件时,当用户触发tap事件时,会有200毫秒左右的延时,这是因为当用户点击完之后很可能会再次点击以触发双击事件,所以GestureDetector会等一段时间来确定是否为双击事件。如果用户只监听了onTap(没有监听onDoubleTap)事件时,则没有延时。
手势函数属性说明:
GestureDetector内部是使用一个或多个GestureRecognizer来识别各种手势的,而GestureRecognizer的作用就是通过Listener来将原始指针事件转换为语义手势,GestureDetector直接可以接收一个子widget。GestureRecognizer是一个抽象类,一种手势的识别器对应一个GestureRecognizer的子类,Flutter实现了丰富的手势识别器,我们可以直接使用。
PS: 使用GestureRecognizer后一定要调用其dispose()方法来释放资源(主要是取消内部的计时器)。
//高度动画
height = Tween(
begin: .0,
end: 300.0,
).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(
0.0, 0.6, //间隔,前60%的动画时间
curve: Curves.ease,
),
),
);
当AnimatedSwitcher的 child 发生变化时(类型或 Key 不同),旧 child 会执行隐藏动画,新 child 会执行执行显示动画。究竟执行何种动画效果则由transitionBuilder参数决定,该参数接受一个AnimatedSwitcherTransitionBuilder类型的 builder,定义如下:
AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
transitionBuilder: (Widget child, Animation animation) {
//执行缩放动画
return ScaleTransition(child: child, scale: animation);
},
child: Text(
'$_count',
//显示指定key,不同的key会被认为是不同的Text,这样才能执行动画
key: ValueKey(_count),
style: Theme.of(context).textTheme.headline4,
),
)
DefaultTextStyle:当前widget节点内部所有文本控件的默认样式
padding:内边距
package:指定报名下的资源
onPressed:点击事件;
*.color:渲染颜色
*.icon:图标,按钮的话是默认在左侧添加一个图标;
可滚动组件.scrollDirection:滑动的主轴
可滚动组件.reverse:滑动方向是否反向
可滚动组件.controller:这些属性最终会透传给对应的 Scrollable 和 Viewport
可滚动组件.physics:这些属性最终会透传给对应的 Scrollable 和 Viewport
可滚动组件.cacheExtent:这些属性最终会透传给对应的 Scrollable 和 Viewport
Int.isOdd:此整数是否是基数
Widget.xxx:获取指定Widget下的参数数据
当指针按下时,Flutter会对应用程序执行命中测试(Hit Test),以确定指针与屏幕接触的位置存在哪些组件(widget), 指针按下事件(以及该指针的后续事件)然后被分发到由命中测试发现的最内部的组件,然后从那里开始,事件会在组件树中向上冒泡,这些事件会从最内部的组件被分发到组件树根的路径上的所有组件,这和Web开发中浏览器的事件冒泡机制相似, 但是Flutter中没有机制取消或停止“冒泡”过程,而浏览器的冒泡是可以停止的。注意,只有通过命中测试的组件才能触发事件。
命中测试是在 PointerDownEvent 事件触发时进行的,一个完成的事件流是 down > move > up (cancle)。
如果父子组件都监听了同一个事件,则子组件会比父组件先响应事件。这是因为命中测试过程是按照深度优先规则遍历的,所以子渲染对象会比父渲染对象先加入 HitTestResult 列表,又因为在事件分发时是从前到后遍历 HitTestResult 列表的,所以子组件比父组件会更先被调用 handleEvent 。
如果想要跳过某个组件的命中测试可以使用IgnorePointer 包裹组件
ps:
参考控件中的触摸监听
参考控件中的手势识别
冲突处理:是因为在拖动时,刚开始按下手指且没有移动时,拖动手势还没有完整的语义,此时TapDown手势胜出(win),此时打印"down",而拖动时,拖动手势会胜出,当手指抬起时,onHorizontalDragEnd 和 onTapUp发生了冲突,但是因为是在拖动的语义中,所以onHorizontalDragEnd胜出,所以就会打印 “onHorizontalDragEnd”。
Listener监听原始指针事件
通过 Listener 解决手势冲突的原因是竞争只是针对手势的,而 Listener 是监听原始指针事件,原始指针事件并非语义话的手势,所以根本不会走手势竞争的逻辑,所以也就不会相互影响。
Positioned(
top:80.0,
left: _leftB,
child: Listener(
onPointerDown: (details) {
print("down");
},
onPointerUp: (details) {
//会触发
print("up");
},
child: GestureDetector(
child: CircleAvatar(child: Text("B")),
onHorizontalDragUpdate: (DragUpdateDetails details) {
setState(() {
_leftB += details.delta.dx;
});
},
onHorizontalDragEnd: (details) {
print("onHorizontalDragEnd");
},
),
),
)
自定义 Recognizer 解决手势冲突
自定义手势识别器的方式比较麻烦,原理时当确定手势竞争胜出者时,会调用胜出者的acceptGesture 方法,表示“宣布成功”,然后会调用其他手势识别其的rejectGesture 方法,表示“宣布失败”。既然如此,我们可以自定义手势识别器(Recognizer),然后去重写它的rejectGesture 方法:在里面调用acceptGesture 方法,这就相当于它失败是强制将它也变成竞争的成功者了,这样它的回调也就会执行。
class CustomTapGestureRecognizer extends TapGestureRecognizer {
@override
void rejectGesture(int pointer) {
//不,我不要失败,我要成功
//super.rejectGesture(pointer);
//宣布成功
super.acceptGesture(pointer);
}
}
//创建一个新的GestureDetector,用我们自定义的 CustomTapGestureRecognizer 替换默认的
RawGestureDetector customGestureDetector({
GestureTapCallback? onTap,
GestureTapDownCallback? onTapDown,
Widget? child,
}) {
return RawGestureDetector(
child: child,
gestures: {
CustomTapGestureRecognizer:
GestureRecognizerFactoryWithHandlers(
() => CustomTapGestureRecognizer(),
(detector) {
detector.onTap = onTap;
},
)
},
);
}
customGestureDetector( // 替换 GestureDetector
onTap: () => print("2"),
child: Container(
width: 200,
height: 200,
color: Colors.red,
alignment: Alignment.center,
child: GestureDetector(
onTap: () => print("1"),
child: Container(
width: 50,
height: 50,
color: Colors.grey,
),
),
),
);
通过WidgetsBindingObserver的didChangeAppLifecycleState 来获取。通过该接口可以获取是生命周期在AppLifecycleState类中。
1、resumed
可见并能响应用户的输入,同安卓的onResume
2、inactive
处在并不活动状态,无法处理用户响应,同安卓的onPause
3、paused
不可见并不能响应用户的输入,但是在后台继续活动中,同安卓的onStop
下面是生命周期:
初次打开widget时,不执行AppLifecycleState的回调;
按home键或Power键, AppLifecycleState inactive---->AppLifecycleState pause
从后台到前台:AppLifecycleState inactive—>ApplifecycleState resumed
back键退出应用: AppLifecycleState inactive—>AppLifecycleState paused
参考:
https://blog.csdn.net/qq_25529689/article/details/125406500
后退刷新的关键:
从HomePage到SecondPage 使用
// 第一部分需要设置的地方
void navigateSecondPage() {
Route route = MaterialPageRoute(builder: (context) => SecondPage());
Navigator.push(context, route).then(onGoBack);
}
关键也在这里,就是有.then中的刷新方法。
onGoBack(dynamic value) {
refreshData();
setState(() {});
};
从SecondPage后退到HomePage时,使用
//第二部分要设置的地方
Navigator.pop(context);
也就是说,不需要额外的传递参数,后退到HomePage时会根据第一次跳转过来的 Navigator.push中的.then方法自动刷新。
既然 Widget 只是描述一个UI元素的配置信息,那么真正的布局、绘制是由谁来完成的呢?Flutter 框架的的处理流程是这样的:
Widget->Element->Render->Layer
/// 定义
class Echo {
const Echo({
Key? key,
required this.text,
this.backgroundColor = Colors.grey, //默认为灰色
}):super(key:key);
}
/// 使用
Echo(text: "hello world");
build方法有一个context参数,它是BuildContext类的一个实例,表示当前 widget 在 widget 树中的上下文,每一个 widget 都会对应一个 context 对象(因为每一个 widget 都是 widget 树上的一个节点)。实际上,context是当前 widget 在 widget 树中位置中执行”相关操作“的一个句柄(handle),比如它提供了从当前 widget 开始向上遍历 widget 树以及按照 widget 类型查找父级 widget 的方法。
// 在 widget 树中向上查找最近的父级Scaffold
widget
context.findAncestorWidgetOfExactType();
// 直接通过of静态方法来获取ScaffoldState
ScaffoldState _state=Scaffold.of(context);
//定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储
static GlobalKey _globalKey= GlobalKey();
Scaffold(
key: _globalKey )
1、实际上Flutter 最原始的定义组件的方式就是通过定义RenderObject 来实现,而StatelessWidget 和 StatefulWidget 只是提供的两个帮助类。
2、如果组件不会包含子组件,则我们可以直接继承自 LeafRenderObjectWidget ,它是 RenderObjectWidget 的子类,而 RenderObjectWidget 继承自 Widget
3、如果自定义的 widget 可以包含子组件,则可以根据子组件的数量来选择继承SingleChildRenderObjectWidget 或 MultiChildRenderObjectWidget,它们也实现了createElement() 方法,返回不同类型的 Element 对象。
class CustomWidget extends LeafRenderObjectWidget{
@override
RenderObject createRenderObject(BuildContext context) {
// 创建 RenderObject
return RenderCustomObject();
}
@override
void updateRenderObject(BuildContext context, RenderCustomObject renderObject) {
// 更新 RenderObject
super.updateRenderObject(context, renderObject);
}
}
class RenderCustomObject extends RenderBox{
@override
void performLayout() {
// 实现布局逻辑
}
@override
void paint(PaintingContext context, Offset offset) {
// 实现绘制
}
}
debugDumpApp();
debugDumpRenderTree()
debugDumpSemanticsTree()
可以切换debugPrintBeginFrameBanner (opens new window)和debugPrintEndFrameBanner (opens new window)布尔值以将帧的开始和结束打印到控制台。
return WillPopScope(
onWillPop: () async {
后退按钮处理
return false;
},
child: Widget);
Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) {}),); 或者Navigator.of(context).pushNamed(RouterPath.pagePathLogin,{Object arguments});
Navigator.of(context).pop(BuildContext context, [ result ];(result 为页面关闭时返回给上一个页面的数据)
MaterialApp-routes:
MaterialApp-onGenerateRoute:
var args=ModalRoute.of(context).settings.arguments;
MaterialApp(
... //省略无关代码
routes: {
"tip2": (context){
return TipRoute(text: ModalRoute.of(context)!.settings.arguments);
},
},
);
MaterialPageRoute继承自PageRoute类,PageRoute类是一个抽象类,表示占有整个屏幕空间的一个模态路由页面,它还定义了路由构建及切换时过渡动画的相关接口及属性。MaterialPageRoute 是 Material组件库提供的组件,它可以针对不同平台,实现与平台页面切换动画风格一致的路由切换动画
MaterialPageRoute 构造函数的各个参数的意义:
路由钩子生成
注意,onGenerateRoute 只会对命名路由生效。通过name打开路由前的判断处理。
MaterialApp(
... //省略无关代码
onGenerateRoute:(RouteSettings settings){
return MaterialPageRoute(builder: (context){
String routeName = settings.name;
// 如果访问的路由页需要登录,但当前未登录,则直接返回登录页路由,
// 引导用户登录;其他情况则正常打开路由。
}
);
}
);
Navigator.push(context, CupertinoPageRoute(
builder: (context)=>PageB(),
));
//以渐隐渐入动画来实现路由过渡,实现代码如下:
Navigator.push(
context,
PageRouteBuilder(
transitionDuration: Duration(milliseconds: 500), //动画时间为500毫秒
pageBuilder: (BuildContext context, Animation animation,
Animation secondaryAnimation) {
return FadeTransition(
//使用渐隐渐入过渡,
opacity: animation,
child: PageB(), //路由B
);
},
),
);
无论是MaterialPageRoute、CupertinoPageRoute,还是PageRouteBuilder,它们都继承自PageRoute类,而PageRouteBuilder其实只是PageRoute的一个包装,我们可以直接继承PageRoute类来实现自定义路由,
class FadeRoute extends PageRoute {
FadeRoute({
required this.builder,
this.transitionDuration = const Duration(milliseconds: 300),
this.opaque = true,
this.barrierDismissible = false,
this.barrierColor,
this.barrierLabel,
this.maintainState = true,
});
final WidgetBuilder builder;
@override
final Duration transitionDuration;
@override
final bool opaque;
@override
final bool barrierDismissible;
@override
final Color barrierColor;
@override
final String barrierLabel;
@override
final bool maintainState;
@override
Widget buildPage(BuildContext context, Animation animation,
Animation secondaryAnimation) => builder(context);
@override
Widget buildTransitions(BuildContext context, Animation animation,
Animation secondaryAnimation, Widget child) {
return FadeTransition(
opacity: animation,
child: builder(context),
);
}
}
//退出无动画实现
@override
Widget buildTransitions(BuildContext context, Animation animation,
Animation secondaryAnimation, Widget child) {
//当前路由被激活,是打开新路由
if(isActive) {
return FadeTransition(
opacity: animation,
child: builder(context),
);
}else{
//是返回,则不应用过渡动画
return Padding(padding: EdgeInsets.zero);
}
}
Navigator.push(context, FadeRoute(builder: (context) {
return PageB();
}));
dependencies:
pkg1:
path: ../../code/pkg1
dependencies:
pkg1:
git:
url: git://github.com/xxx/pkg1.git
path: packages/package1(根目录是可以不写这个字段)
Color(0xffdc380d); //如果颜色固定可以直接使用整数值
//颜色是一个字符串变量
var c = "dc380d";
Color(int.parse(c,radix:16)|0xFF000000) //通过位运算符将Alpha设置为FF
Color(int.parse(c,radix:16)).withAlpha(255) //通过方法将Alpha设置为FF
Color 类中提供了一个computeLuminance()方法,它可以返回一个[0-1]的一个值,数字越大颜色就越浅
是实现Material Design中的颜色的类,它包含一种颜色的10个级别的渐变色。MaterialColor通过"[]"运算符的索引值来代表颜色的深度,有效的索引有:50,100,200,…,900,数字越大,颜色越深。MaterialColor的默认值为索引等于500的颜色。
用于保存是Material 组件库的主题数据,Material组件需要遵守相应的设计规范,而这些规范可自定义部分都定义在ThemeData中了,所以我们可以通过ThemeData来自定义应用主题。在子组件中,我们可以通过Theme.of方法来获取当前的ThemeData。
flutter:
fonts:
- family: myIcon #指定一个字体名
fonts:
- asset: fonts/iconfont.ttf
- family: Raleway
fonts:
- asset: assets/fonts/Raleway-Regular.ttf
- asset: assets/fonts/Raleway-Medium.ttf
weight: 500
- asset: assets/fonts/Raleway-SemiBold.ttf
weight: 600
- family: AbrilFatface
fonts:
- asset: assets/fonts/abrilfatface/AbrilFatface-Regular.ttf
assets:
- assets/my_icon.png
- assets/background.png
变体(variant)
…/pubspec.yaml
…/graphics/my_icon.png
…/graphics/background.png
…/graphics/dark/background.png
…etc.
flutter:
assets:
- graphics/background.png
加载可以使用: AssetImage(‘graphics/background.png’, package: ‘my_icons’)(其他包的图片资源需指定package,无改参数默认为当前)
…/image.png
…/Mx/image.png
…/Nx/image.png
…etc.
…/my_icon.png
…/2.0x/my_icon.png
…/3.0x/my_icon.png
Android:…/android/app/src/main/res 指南
iOS:…/ios/Runner。该目录中Assets.xcassets/AppIcon.appiconset已经包含占位符图片(见图2-16), 只需将它们替换为适当大小的图片,保留原始文件名称。
启动图
Android:res/drawable/launch_background.xml,通过自定义drawable来实现自定义启动界面(你也可以直接换一张图片)
iOS:Assets.xcassets/LaunchImage.imageset, 拖入图片,并命名为LaunchImage.png、[email protected]、[email protected]。 如果你使用不同的文件名,那您还必须更新同一目录中的Contents.json文件,图片的具体尺寸可以查看苹果官方的标准。
所有这些标志只能在调试模式下工作。通常,Flutter框架中以“debug…” 开头的任何内容都只能在调试模式下工作。
调试动画最简单的方法是减慢它们的速度。为此,请将timeDilation (opens new window)变量(在scheduler库中)设置为大于1.0的数字,例如50.0。 最好在应用程序启动时只设置一次。如果我们在运行中更改它,尤其是在动画运行时将其值改小,则在观察时可能会出现倒退,这可能会导致断言命中,并且这通常会干扰我们的开发工作。
要了解我们的应用程序导致重新布局或重新绘制的原因,我们可以分别设置debugPrintMarkNeedsLayoutStacks (opens new window)和 debugPrintMarkNeedsPaintStacks (opens new window)标志。 每当渲染盒被要求重新布局和重新绘制时,这些都会将堆栈跟踪记录到控制台。如果这种方法对我们有用,我们可以使用services库中的debugPrintStack()方法按需打印堆栈痕迹。
要收集有关Flutter应用程序启动所需时间的详细信息,可以在运行flutter run时使用trace-startup和profile选项。
flutter run --trace-startup --profile
跟踪输出保存为start_up_info.json,在Flutter工程目录在build目录下。输出列出了从应用程序启动到这些跟踪事件(以微秒捕获)所用的时间:
要执行自定义性能跟踪和测量Dart任意代码段的wall/CPU时间(类似于在Android上使用systrace (opens new window))。 使用dart:developer的Timeline (opens new window)工具来包含你想测试的代码块,例如:
Timeline.startSync('interesting function');
// iWonderHowLongThisTakes();
Timeline.finishSync();
然后打开你应用程序的Observatory timeline页面,在“Recorded Streams”中选择‘Dart’复选框,并执行你想测量的功能。
刷新页面将在Chrome的跟踪工具 (opens new window)中显示应用按时间顺序排列的timeline记录。
请确保运行flutter run时带有–profile标志,以确保运行时性能特征与我们的最终产品差异最小。
布局类组件都会包含一个或多个子组件,不同的布局类组件对子组件排列(layout)方式不同,
Flutter 中有两种布局模型:
1.基于 RenderBox 的盒模型布局。
2.基于 Sliver ( RenderSliver ) 按需加载列表布局。
布局流程如下:
1.上层组件向下层组件传递约束(constraints)条件。
2.下层组件确定自己的大小,然后告诉上层组件。注意下层组件的大小必须符合父组件的约束。
3.上层组件确定下层组件相对于自身的偏移和确定自身的大小(大多数情况下会根据子组件的大小来确定自身的大小)。
盒模型布局组件有两个特点:
1.组件对应的渲染对象都继承自 RenderBox 类。
2.在布局过程中父级传递给子级的约束信息由 BoxConstraints 描述。
用 Dart 编写的传统 package,比如 path。其中一些可能包含 Flutter 的特定功能,因此依赖于 Flutter 框架,其使用范围仅限于 Flutter,比如 fluro。
对于纯 Dart 库的 package,只要在 lib/.dart 文件中添加功能实现,或在 lib 目录中的多个文件中添加功能实现。
如果要对 package 进行测试,在 test 目录下添加 单元测试。
使用 Dart 编写的,按需使用 Java 或 Kotlin、Objective-C 或 Swift 分别在 Android 和/或 iOS 平台实现的 package。
原生插件类型 package 的 API 在 Dart 代码中要首先定义好,使用你钟爱的 Flutter 编辑器,打开 hello 主目录,并找到 lib/hello.dart 文件。
在菜单中选择 File > Open,然后选择 hello/example/android/build.gradle 文件;
在Gradle Sync 对话框中,选择 OK;
在“Android Gradle Plugin Update”对话框中,选择“Don’t remind me again for this project”。
插件中与 Android 系统徐相关的代码在 hello/java/com.example.hello/HelloPlugin 这个文件里。
你可以在 Android Studio 中点击运行 ▶ 按钮来运行示例程序。
使用 Xcode 编辑 iOS 平台代码之前,首先确保代码至少被构建过一次(即从 IDE/编辑器执行示例程序,或在终端中执行以下命令: cd hello/example; flutter build ios --no-codesign)。
接下来执行下面步骤:
启动 Xcode
选择“File > Open”,然后选择 hello/example/ios/Runner.xcworkspace 文件。
插件的 iOS 平台代码位于项目导航中的这个位置: Pods/Development Pods/hello/…/…/example/ios/.symlinks/plugins/hello/ios/Classes。
你可以点击运行 ▶ 按钮来运行示例程序。
最后,你需要将 Dart 编写的 API 代码与特定平台的实现相互关联。这是通过 平台通道 完成的。
建议将下列文档添加到所有 package 中:
README.md 文件用来对 package 进行介绍
CHANGELOG.md 文件用来记录每个版本的更改
LICENSE 文件用来阐述 package 的许可条款
API 文档包含所有的公共 API(详情参见下文)
API 文档
当你提交一个 package 时,会自动生成 API 文档并将其提交到 pub.flutter-io.cn/documentation,示例请参见 device_info 文档。
如果你希望在本地开发环境中生成 API 文档,可以使用以下命令:
cd ~/dev/mypackage
-告知文档工具 Flutter SDK 所在位置(请自行更改 Flutter SDK 该在的位置)
export FLUTTER_ROOT=~/dev/flutter # on macOS or Linux (适用于 macOS 或 Linux 操作系统)
set FLUTTER_ROOT=~/dev/flutter # on Windows (适用于 Windows 操作系统)
-运行 dartdoc
工具(已经包含到 Flutter SDK 了):
$FLUTTER_ROOT/bin/cache/dart-sdk/bin/dartdoc # on macOS or Linux (适用于 macOS 或 Linux 操作系统)
%FLUTTER_ROOT%\bin\cache\dart-sdk\bin\dartdoc # on Windows (适用于 Windows 操作系统)
安卓端获取全局上下文
context = flutterPluginBinding.applicationContext;
文件结构
编译调试构建目录
交互通信方式
代码处理
ps: 需要统一的通道名称:CHANNEL = “指定功能的通道名称”
Future _getBatteryLevel() async {
String batteryLevel;
try {
final int result = await platform.invokeMethod('getBatteryLevel');
batteryLevel = 'Battery level at $result % .';
} on PlatformException catch (e) {
batteryLevel = "Failed to get battery level: '${e.message}'.";
}
setState(() {
_batteryLevel = batteryLevel;
});
}
import 'package:pigeon/pigeon.dart';
class SearchRequest {
String query = '';
}
class SearchReply {
String result = '';
}
@HostApi()
abstract class Api {
Future search(SearchRequest request);
}
Dart使用:
import 'generated_pigeon.dart';
Future onClick() async {
SearchRequest request = SearchRequest()..query = 'test';
Api api = SomeApi();
SearchReply reply = await api.search(request);
print('reply: ${reply.result}');
}
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
// This method is invoked on the main thread.
call, result ->
if (call.method == "getBatteryLevel") {
val batteryLevel = getBatteryLevel()
if (batteryLevel != -1) {
result.success(batteryLevel -40)
} else {
result.error("UNAVAILABLE", "Battery level not available.", null)
}
} else {
result.notImplemented()
}
}
}
iOS文件
通道监听:
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let batteryChannel = FlutterMethodChannel(name: "samples.flutter.dev/battery",
binaryMessenger: controller.binaryMessenger)
batteryChannel.setMethodCallHandler({
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
guard call.method == "getBatteryLevel" else {
result(FlutterMethodNotImplemented)
return
}
self.receiveBatteryLevel(result: result)
})
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
Flutter 支持两种集成模式:虚拟显示模式 (Virtual displays) 和混合集成模式 (Hybrid composition) 。
ps: 当前发现集成暂时只能是静态控件,而且是使用通过代码实例化的,如果在各个平台中筛选控件状态则不会生效,只有通过flutter刷新才可以
安卓端平台代码
安卓端实现托管控件必须要实现一个view的实例化逻辑,并且继承PlatformView,同时要声明一个工厂类用来建设控件,工厂类继承 PlatformViewFactory,二者缺一不可
// view的实现类,用来生成相应的view视图
internal class NativeView(context: Context, id: Int, creationParams: Map?) : PlatformView {
private val textView: TextView
override fun getView(): View {
return textView
}
override fun dispose() {}
init {
textView = TextView(context)
textView.textSize = 72f
textView.setBackgroundColor(Color.rgb(255, 255, 255))
textView.text = "Rendered on a native Android view (id: $id)"
}
}
// view工厂类
class NativeViewFactory : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
override fun create(context: Context, viewId: Int, args: Any?): PlatformView {
val creationParams = args as Map?
return NativeView(context, viewId, creationParams)
}
}
// 插件对外入口注册
class PlatformViewPlugin : FlutterPlugin {
override fun onAttachedToEngine(binding: FlutterPluginBinding) {
binding
.platformViewRegistry
.registerViewFactory("", NativeViewFactory())
}
override fun onDetachedFromEngine(binding: FlutterPluginBinding) {}
}
// 插件内部样例应用入口注册
class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
flutterEngine
.platformViewsController
.registry
.registerViewFactory("", NativeViewFactory())
}
}
iOS端平台代码
和安卓差不多,都需要一个view的实现(继承FlutterPlatformView)以及工厂创建类(集成FlutterPlatformViewFactory)
//view实现
class FLNativeView: NSObject, FlutterPlatformView {
private var _view: UIView
init(
frame: CGRect,
viewIdentifier viewId: Int64,
arguments args: Any?,
binaryMessenger messenger: FlutterBinaryMessenger?
) {
_view = UIView()
super.init()
// iOS views can be created here
createNativeView(view: _view)
}
func view() -> UIView {
return _view
}
func createNativeView(view _view: UIView){
_view.backgroundColor = UIColor.blue
let nativeLabel = UILabel()
nativeLabel.text = "Native text from iOS"
nativeLabel.textColor = UIColor.white
nativeLabel.textAlignment = .center
nativeLabel.frame = CGRect(x: 0, y: 0, width: 180, height: 48.0)
_view.addSubview(nativeLabel)
}
}
// 工厂实现
class FLNativeViewFactory: NSObject, FlutterPlatformViewFactory {
private var messenger: FlutterBinaryMessenger
init(messenger: FlutterBinaryMessenger) {
self.messenger = messenger
super.init()
}
func create(
withFrame frame: CGRect,
viewIdentifier viewId: Int64,
arguments args: Any?
) -> FlutterPlatformView {
return FLNativeView(
frame: frame,
viewIdentifier: viewId,
arguments: args,
binaryMessenger: messenger)
}
}
//插件对外入口注册
class FLPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let factory = FLNativeViewFactory(messenger: registrar.messenger)
registrar.register(factory, withId: "")
}
}
// 插件内部样例应用入口注册
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
weak var registrar = self.registrar(forPlugin: "plugin-name")
let factory = FLNativeViewFactory(messenger: registrar!.messenger())
self.registrar(forPlugin: "")!.register(
factory,
withId: "")
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
Dart端代码
因为安卓和iOS视图不是一种,所以实现的时候需要区分安卓和iOS
Widget build(BuildContext context) {
// This is used in the platform side to register the view.
const String viewType = '';
// Pass parameters to the platform side.
final Map creationParams = {};
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return getAndroid(context,viewType,creationParams);
case TargetPlatform.iOS:
return getIOS(context,viewType,creationParams);
default:
throw UnsupportedError('Unsupported platform view');
}
}
Widget getIOS(BuildContext context,viewType,creationParams) {
return UiKitView(
viewType: viewType,
layoutDirection: TextDirection.ltr,
creationParams: creationParams,
creationParamsCodec: const StandardMessageCodec(),
);
}
Widget getAndroid(BuildContext context,viewType,creationParams) {
return PlatformViewLink(
viewType: viewType,
surfaceFactory:
(context, controller) {
return AndroidViewSurface(
controller: controller as AndroidViewController,
gestureRecognizers: const >{},
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
);
},
onCreatePlatformView: (params) {
return PlatformViewsService.initSurfaceAndroidView(
id: params.id,
viewType: viewType,
layoutDirection: TextDirection.ltr,
creationParams: creationParams,
creationParamsCodec: const StandardMessageCodec(),
onFocus: () {
params.onFocusChanged(true);
},
)
..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
..create();
},
);
}
提供了一种在 widget 树中从上到下共享数据的方式,比如我们在应用的根 widget 中通过InheritedWidget共享了一个数据,那么我们便可以在任意子widget 中来获取该共享的数据!这个特性在一些需要在整个 widget 树中共享数据的场景中非常方便!如Flutter SDK中正是通过 InheritedWidget 来共享应用主题(Theme)和 Locale (当前语言环境)信息的。
//会产生didChangeDependencies() 通知的实例化
//定义一个便捷方法,方便子树中的widget获取共享数据
static ShareDataWidget? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType();
}
//不会产生didChangeDependencies() 通知的实例化
static ShareDataWidget of(BuildContext context) {
//return context.dependOnInheritedWidgetOfExactType();
return context.getElementForInheritedWidgetOfExactType().widget;
}
//两者区别:dependOnInheritedWidgetOfExactType() 比 getElementForInheritedWidgetOfExactType()多调了dependOnInheritedElement方法,dependOnInheritedElement方法中主要是注册了依赖关系!看到这里也就清晰了,调用dependOnInheritedWidgetOfExactType() 和 getElementForInheritedWidgetOfExactType()的区别就是前者会注册依赖关系,而后者不会,
//订阅者回调签名
typedef void EventCallback(arg);
class EventBus {
//私有构造函数
EventBus._internal();
//保存单例
static EventBus _singleton = EventBus._internal();
//工厂构造函数
factory EventBus()=> _singleton;
//保存事件订阅者队列,key:事件名(id),value: 对应事件的订阅者队列
final _emap = Map
InheritedProvider 相当于最小的一个Provider。
包名 | 介绍 |
---|---|
Provider (opens new window)& Scoped Model(opens new window) | 这两个包都是基于InheritedWidget的,原理相似 |
Redux(opens new window) | 是Web开发中React生态链中Redux包的Flutter实现 |
MobX(opens new window) | 是Web开发中React生态链中MobX包的Flutter实现 |
BLoC(opens new window) | 是BLoC模式的Flutter实现 |
通知(Notification)是Flutter中一个重要的机制,在widget树中,每一个节点都可以分发通知,通知会沿着当前节点向上传递,所有父节点都可以通过NotificationListener来监听通知。Flutter中将这种由子向父的传递通知的机制称为通知冒泡(Notification Bubbling)。通知冒泡和用户触摸事件冒泡是相似的,但有一点不同:通知冒泡可以中止,但用户触摸事件不行。
除了在可滚动组件在滚动过程中会发出ScrollNotification之外,还有一些其他的通知,如SizeChangedLayoutNotification、KeepAliveNotification 、LayoutChangedNotification等
NotificationListener(
onNotification: (notification){
switch (notification.runtimeType){
case ScrollStartNotification: print("开始滚动"); break;
case ScrollUpdateNotification: print("正在滚动"); break;
case ScrollEndNotification: print("滚动停止"); break;
case OverscrollNotification: print("滚动到边界"); break;
}
},
child: ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(title: Text("$index"),);
}
),
);
//指定监听通知的类型为滚动结束通知(ScrollEndNotification)
NotificationListener(
onNotification: (notification){
//只会在滚动结束时才会触发此回调
print(notification);
},
child: ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(title: Text("$index"),);
}
),
);
1、定义一个通知类,要继承自Notification类;
class MyNotification extends Notification {
MyNotification(this.msg);
final String msg;
}
2、分发通知。
Notification有一个dispatch(context)方法,它是用于分发通知的,我们说过context实际上就是操作Element的一个接口,它与Element树上的节点是对应的,通知会从context对应的Element节点向上冒泡。
onNotification: (notification){
print(notification.msg); //打印通知
return false;
}
函数:
取消对话框:Navigator.of(context).pop()
展示对话框:(Material风格)
Future showDialog({
required BuildContext context,
required WidgetBuilder builder, // 对话框UI的builder
bool barrierDismissible = true, //点击对话框barrier(遮罩)时是否关闭它
})
展示对话框:(普通风格的对话框呢(非Material风格))
Future showGeneralDialog({
required BuildContext context,
required RoutePageBuilder pageBuilder, //构建对话框内部UI
bool barrierDismissible = false, //点击遮罩是否关闭对话框
String? barrierLabel, // 语义化标签(用于读屏软件)
Color barrierColor = const Color(0x80000000), // 遮罩颜色
Duration transitionDuration = const Duration(milliseconds: 200), // 对话框打开/关闭的动画时长
RouteTransitionsBuilder? transitionBuilder, // 对话框打开/关闭的动画
...
})
展示对话框:(底部菜单列表)
// 弹出底部菜单列表模态对话框
Future _showModalBottomSheet() {
return showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return ListView.builder(
itemCount: 30,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text("$index"),
onTap: () => Navigator.of(context).pop(index),
);
},
);
},
);
}
展示对话框:(日历选择器)
Future _showDatePicker1() {
var date = DateTime.now();
return showDatePicker(
context: context,
initialDate: date,
firstDate: date,
lastDate: date.add( //未来30天可选
Duration(days: 30),
),
);
}
Future _showDatePicker2() {
var date = DateTime.now();
return showCupertinoModalPopup(
context: context,
builder: (ctx) {
return SizedBox(
height: 200,
child: CupertinoDatePicker(
mode: CupertinoDatePickerMode.dateAndTime,
minimumDate: date,
maximumDate: date.add(
Duration(days: 30),
),
maximumYear: date.year + 1,
onDateTimeChanged: (DateTime value) {
print(value);
},
),
);
},
);
}
操作弹窗内容方式
class StatefulBuilder extends StatefulWidget {
const StatefulBuilder({
Key? key,
required this.builder,
}) : assert(builder != null),
super(key: key);
final StatefulWidgetBuilder builder;
@override
_StatefulBuilderState createState() => _StatefulBuilderState();
}
class _StatefulBuilderState extends State {
@override
Widget build(BuildContext context) => widget.builder(context, setState);
}
使用时需使用以下代码:
Navigator.push(
context,
PopupViewShow(//继承PopupRoute的类
child: widget,//子页面
showViewKey: key,//使用弹窗的控件key,因为对话框是全屏的,所以需要使用这个做定位使用
),
);
class PopupViewShow extends PopupRoute {
final Duration _duration = Duration(milliseconds: 300);
Widget child;
GlobalKey showViewKey;
final EdgeInsetsGeometry padding;
PopupViewShow({required this.child, required this.showViewKey, padding}) : padding = padding ?? EdgeInsets.zero;
@override
Color? get barrierColor => null;
@override
bool get barrierDismissible => true;
@override
String? get barrierLabel => null;
@override
Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) {
return _Popup(child: child, showViewKey: showViewKey, padding: padding);
}
@override
Duration get transitionDuration => _duration;
}
class _Popup extends StatelessWidget {
final Widget child;
final EdgeInsetsGeometry padding;
GlobalKey showViewKey;
_Popup({required this.child, required this.showViewKey, required this.padding});
@override
Widget build(BuildContext context) {
Offset offset = showViewKey.position();
Rect rect = showViewKey.getRect();//宽高属性获取,需使用插件“rect_getter: ^1.1.0”,并调用弹窗的Widget使用“RectGetter.createGlobalKey()”这个key
return Material(
color: Colors.transparent,
child: GestureDetector(
child: Stack(
children: [
Container(//位置为全屏
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
color: Colors.transparent,
),
Padding(
child: child,
padding: EdgeInsets.fromLTRB(offset.dx, offset.dy + rect.height, 0, 0),//内容位置定位使用
),
],
),
onTap: () {
//点击空白处
Navigator.of(context).pop();
},
),
);
}
}
Animation是一个抽象类,它本身和UI渲染没有任何关系,而它主要的功能是保存动画的插值和状态;其中一个比较常用的Animation类是Animation。Animation对象是一个在一段时间内依次生成一个区间(Tween)之间值的类。Animation对象在整个动画执行过程中输出的值可以是线性的、曲线的、一个步进函数或者任何其他曲线函数等等,这由Curve来决定。 根据Animation对象的控制方式,动画可以正向运行(从起始状态开始,到终止状态结束),也可以反向运行,甚至可以在中间切换方向。Animation还可以生成除double之外的其他类型值,如:Animation 或Animation。在动画的每一帧中,我们可以通过Animation对象的value属性获取动画的当前状态值。
动画通知
我们可以通过Animation来监听动画每一帧以及执行状态的变化,Animation有如下两个方法:
动画过程可以是匀速的、匀加速的或者先加速后减速等。Flutter中通过Curve(曲线)来描述动画过程,我们把匀速动画称为线性的(Curves.linear),而非匀速动画称为非线性的。
我们可以通过CurvedAnimation来指定动画的曲线,如:final CurvedAnimation curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
当然我们也可以创建自己Curve,例如我们定义一个正弦曲线:
class ShakeCurve extends Curve {
@override
double transform(double t) {
return math.sin(t * math.PI * 2);
}
}
AnimationController用于控制动画,它包含动画的启动forward()、停止stop() 、反向播放 reverse()等方法。AnimationController会在动画的每一帧,就会生成一个新的值。默认情况下,AnimationController在给定的时间段内线性的生成从 0.0 到1.0(默认区间)的数字。
要使用 Tween 对象,需要调用其animate()方法,然后传入一个控制器对象。例如,以下代码在 500 毫秒内生成从 0 到 255 的整数值。
final AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
Animation alpha = IntTween(begin: 0, end: 255).animate(controller);
//注意animate()返回的是一个Animation,而不是一个Animatable。
动画的原理其实就是每一帧绘制不同的内容,一般都是指定起始和结束状态,然后在一段时间内从起始状态逐渐变为结束状态,而具体某一帧的状态值会根据动画的进度来算出,因此,Flutter 中给有可能会做动画的一些状态属性都定义了静态的 lerp 方法(线性插值),比如:
//a 为起始颜色,b为终止颜色,t为当前动画的进度[0,1]
Color.lerp(a, b, t);
// Size.lerp(a, b, t)
// Rect.lerp(a, b, t)
// Offset.lerp(a, b, t)
// Decoration.lerp(a, b, t)
// Tween.lerp(t) //起始状态和终止状态在构建 Tween 的时候已经指定了
需要注意,lerp 是线性插值,意思是返回值和动画进度t是成一次函数(y = kx + b)关系,因为一次函数的图像是一条直线,所以叫线性插值。如果我们想让动画按照一个曲线来执行,我们可以对 t 进行映射,比如要实现匀加速效果,则 t’ = at²+bt+c,然后指定加速度 a 和 b 即可(大多数情况下需保证 t’ 的取值范围在[0,1],当然也有一些情况可能会超出该取值范围,比如弹簧(bounce)效果),而不同 Curve 可以按照不同曲线执行动画的的原理本质上就是对 t 按照不同映射公式进行映射。
基本方式:
//线性
class ScaleAnimationRoute extends StatefulWidget {
const ScaleAnimationRoute({Key? key}) : super(key: key);
@override
_ScaleAnimationRouteState createState() => _ScaleAnimationRouteState();
}
//需要继承TickerProvider,如果有多个AnimationController,则应该使用TickerProviderStateMixin。
class _ScaleAnimationRouteState extends State
with SingleTickerProviderStateMixin {
late Animation animation;
late AnimationController controller;
@override
initState() {
super.initState();
controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
//匀速
//图片宽高从0变到300
animation = Tween(begin: 0.0, end: 300.0).animate(controller)
..addListener(() {
setState(() => {});
});
//启动动画(正向执行)
controller.forward();
}
@override
Widget build(BuildContext context) {
return Center(
child: Image.asset(
"imgs/avatar.png",
width: animation.value,
height: animation.value,
),
);
}
@override
dispose() {
//路由销毁时需要释放动画资源
controller.dispose();
super.dispose();
}
}
//弹性
@override
initState() {
super.initState();
controller = AnimationController(
duration: const Duration(seconds: 3), vsync: this);
//使用弹性曲线
animation=CurvedAnimation(parent: controller, curve: Curves.bounceIn);
//图片宽高从0变到300
animation = Tween(begin: 0.0, end: 300.0).animate(animation)
..addListener(() {
setState(() => {});
});
//启动动画
controller.forward();
}
AnimatedWidget简化:
class AnimatedImage extends AnimatedWidget {
const AnimatedImage({
Key? key,
required Animation animation,
}) : super(key: key, listenable: animation);
@override
Widget build(BuildContext context) {
final animation = listenable as Animation;
return Center(
child: Image.asset(
"imgs/avatar.png",
width: animation.value,
height: animation.value,
),
);
}
}
class ScaleAnimationRoute1 extends StatefulWidget {
const ScaleAnimationRoute1({Key? key}) : super(key: key);
@override
_ScaleAnimationRouteState createState() => _ScaleAnimationRouteState();
}
class _ScaleAnimationRouteState extends State
with SingleTickerProviderStateMixin {
late Animation animation;
late AnimationController controller;
@override
initState() {
super.initState();
controller = AnimationController(
duration: const Duration(seconds: 2), vsync: this);
//图片宽高从0变到300
animation = Tween(begin: 0.0, end: 300.0).animate(controller);
//启动动画
controller.forward();
}
@override
Widget build(BuildContext context) {
return AnimatedImage(
animation: animation,
);
}
@override
dispose() {
//路由销毁时需要释放动画资源
controller.dispose();
super.dispose();
}
}
AnimatedBuilder重构:
@override
Widget build(BuildContext context) {
//return AnimatedImage(animation: animation,);
return AnimatedBuilder(
animation: animation,
child: Image.asset("imgs/avatar.png"),
builder: (BuildContext ctx, child) {
return Center(
child: SizedBox(
height: animation.value,
width: animation.value,
child: child,
),
);
},
);
}
好处:
1、不用显式的去添加帧监听器,然后再调用setState() 了,这个好处和AnimatedWidget是一样的。
2、更好的性能:因为动画每一帧需要构建的 widget 的范围缩小了,如果没有builder,setState()将会在父组件上下文中调用,这将会导致父组件的build方法重新调用;而有了builder之后,只会导致动画widget自身的build重新调用,避免不必要的rebuild。
3、通过AnimatedBuilder可以封装常见的过渡效果来复用动画。
我们可以通过Animation的addStatusListener()方法来添加动画状态改变监听器。Flutter中,有四种动画状态,在AnimationStatus枚举类中定义,下面我们逐个说明:
枚举值 | 含义 |
---|---|
dismissed | 动画在起始点停止 |
forward | 动画正在正向执行 |
reverse | 动画正在反向执行 |
completed | 动画在终点停止 |
animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
//动画执行结束时反向执行动画
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
//动画恢复到初始状态时执行动画(正向)
controller.forward();
}
});
Hero 指的是可以在路由(页面)之间“飞行”的 widget,简单来说 Hero 动画就是在路由切换时,有一个共享的widget 可以在新旧路由间切换。由于共享的 widget 在新旧路由页面上的位置、外观可能有所差异,所以在路由切换时会从旧路逐渐过渡到新路由中的指定位置,这样就会产生一个 Hero 动画。
Hero(
tag: "avatar", //唯一标记,前后两个路由页Hero的tag必须相同
child: ClipOval(
child: Image.asset(
"imgs/avatar.png",
width: 50.0,
),
),
)
有些时候我们可能会需要一些复杂的动画,这些动画可能由一个动画序列或重叠的动画组成,比如:有一个柱状图,需要在高度增长的同时改变颜色,等到增长到最大高度后,我们需要在X轴上平移一段距离。可以发现上述场景在不同阶段包含了多种动画,要实现这种效果,使用交织动画(Stagger Animation)会非常简单。交织动画需要注意以下几点:
//高度动画
height = Tween(
begin: .0,
end: 300.0,
).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(
0.0, 0.6, //间隔,前60%的动画时间
curve: Curves.ease,
),
),
);
color = ColorTween(
begin: Colors.green,
end: Colors.red,
).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(
0.0, 0.6, //间隔,前60%的动画时间
curve: Curves.ease,
),
),
);
padding = Tween(
begin: const EdgeInsets.only(left: .0),
end: const EdgeInsets.only(left: 100.0),
).animate(
CurvedAnimation(
parent: controller,
curve: const Interval(
0.6, 1.0, //间隔,后40%的动画时间
curve: Curves.ease,
),
),
);
class SlideTransitionX extends AnimatedWidget {
SlideTransitionX({
Key? key,
required Animation position,
this.transformHitTests = true,
this.direction = AxisDirection.down,
required this.child,
}) : super(key: key, listenable: position) {
switch (direction) {
case AxisDirection.up:
_tween = Tween(begin: const Offset(0, 1), end: const Offset(0, 0));
break;
case AxisDirection.right:
_tween = Tween(begin: const Offset(-1, 0), end: const Offset(0, 0));
break;
case AxisDirection.down:
_tween = Tween(begin: const Offset(0, -1), end: const Offset(0, 0));
break;
case AxisDirection.left:
_tween = Tween(begin: const Offset(1, 0), end: const Offset(0, 0));
break;
}
}
final bool transformHitTests;
final Widget child;
final AxisDirection direction;
late final Tween _tween;
@override
Widget build(BuildContext context) {
final position = listenable as Animation;
Offset offset = _tween.evaluate(position);
if (position.status == AnimationStatus.reverse) {
switch (direction) {
case AxisDirection.up:
offset = Offset(offset.dx, -offset.dy);
break;
case AxisDirection.right:
offset = Offset(-offset.dx, offset.dy);
break;
case AxisDirection.down:
offset = Offset(offset.dx, -offset.dy);
break;
case AxisDirection.left:
offset = Offset(-offset.dx, offset.dy);
break;
}
}
return FractionalTranslation(
translation: offset,
transformHitTests: transformHitTests,
child: child,
);
}
}