前言
忙里偷闲终于把Flutter中文网大致浏览了一遍,首先感谢Flutter中文网的作者大大对该网站的维护。经过这几个月的学习,对Flutter有了大致的了解,在这段时间对Flutter做下总结和笔记,然后用Flutter写个demo,对就是我以前写的ShowTime,现在有java,kotlin版本,然后就是Flutter版本的了,一开心就想把啥东西都往里集成,哈哈。废话说多了,作为一个Android开发者,我是对照着Android系统知识来学习Flutter的,我把Flutter大致分成了五部分:
- UI
- Animation
- 网络
- 数据持久化
- 其他
FlutterUI-----Widget
Flutter布局机制的核心就是widget。在Flutter中,几乎所有东西都是一个widget --甚至布局模型都是widget。Widgets 是用于构建UI的类.Widgets 用于布局和UI元素.通过简单的widget来构建复杂的widget.flutter工程中的每一个页面都是一个widget树,或者说是一个widget
Stateful(有状态) 和 Stateless(无状态)widgets
widget分为有状态的和无状态。
- 无状态:就是一个静态的页面,没有交互。StatelessWidget 标识为一个无状态的widget 。
- 有状态:为对用户输入做出相应的反应 - 应用程序通常会携带一些状态。StatefulWidget 标识为一个有状态的widget,在Flutter中,事件流是“向上”传递的,而状态流是“向下”传递的(译者语:这类似于React/Vue中父子组件通信的方式:子widget到父widget是通过事件通信,而父到子是通过状态),重定向这一流程的共同父元素是State。
- 有些widgets是有状态的, 有些是无状态的
- 如果用户与widget交互,widget会发生变化,那么它就是有状态的.
- widget的状态(state)是一些可以更改的值, 如一个slider滑动条的当前值或checkbox是否被选中.
- widget的状态保存在一个State对象中, 它和widget的布局显示分离。
- 当widget状态改变时, State 对象调用setState(), 告诉框架去重绘widget.
创建一个无状态的widget
让widget直接继承StatelessWidget就可以了
class CustomStatelessWidget extends StatelessWidget{
@override
Widget build(BuildContext context) {
// TODO: implement build
return null;
}
}
创建一个有状态的widget
- 要创建一个自定义有状态widget,需创建两个类:StatefulWidget和State
- 状态对象包含widget的状态和build() 方法。
- 当widget的状态改变时,状态对象调用setState(),告诉框架重绘widget
//这只是做一个直观的列子
//通过继承StatelessWidget(无状态)或StatefulWidget(有状态)来区分无状态或有状态
class AnimatedListSample extends StatefulWidget{
@override
State<StatefulWidget> createState() => new _AnimatedListSampleState();
}
class _AnimatedListSampleState extends State<AnimatedListSample>{
Widget _itemBuilder(BuildContext context, int index, Animation<double> animation){
return new CardItem(animation: animation,
item: _list[index],
selected: _selectedItem==_list[index],
onTip: (){
//通过setState修改其状态
setState(() {
_selectedItem=_selectedItem == _list[index]?null:_list[index];
});
},);
}
}
Widget的状态可以通过多种方式进行管理
- widget可以在内部处理它自己的状态,因此它继承StatefulWidget重写createState()来创建状态对象.
- 父widget管理 widget状态。
- 混搭管理(父widget和widget自身都管理状态))
- 如果状态是用户数据,如复选框的选中状态、滑块的位置,则该状态最好由父widget管理,父管理:通过子widget的构造函数,将父中处理交互事件的函数作为参数传递给子widget,然后子widget通过该函数将状态回调给父widget。
- 如果所讨论的状态是有关界面外观效果的,例如动画,那么状态最好由widget本身来管理.
- 如果有疑问,首选是在父widget中管理状态
响应widget生命周期事件
自定义State类存储可变信息 - 可以在widget的生命周期内改变逻辑和内部状态, 调用setState()是至关重要的,因为这告诉框架,widget的状态已经改变,应该重绘。
在StatefulWidget调用createState之后,框架将新的状态对象插入树中,然后调用状态对象的initState。 子类化State可以重写initState,以完成仅需要执行一次的工作。 例如,您可以重写initState以配置动画或订阅platform services。initState的实现中需要调用super.initState。
当一个状态对象不再需要时,框架调用状态对象的dispose。 您可以覆盖该dispose方法来执行清理工作。例如,您可以覆盖dispose取消定时器或取消订阅platform services。 dispose典型的实现是直接调用super.dispose。
class LifecycleWatch extends StatefulWidget{
@override
State<StatefulWidget> createState() =>LifecycleState();
}
class LifecycleState extends State<LifecycleWatch> with WidgetsBindingObserver{
AppLifecycleState _appLifecyclestate;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
super.dispose();
WidgetsBinding.instance.removeObserver(this);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
setState(() {
_appLifecyclestate=state;
});
}
Widget showChildWidget(){
if(_appLifecyclestate==null){
return new Text('This widget has not observed any lifecycle changes.', textDirection: TextDirection.ltr);
}else{
return new Text('The most recent lifecycle state this widget observed was: $_appLifecyclestate.',
textDirection: TextDirection.ltr);
}
}
@override
Widget build(BuildContext context) {
return showChildWidget;
}
}
Flutter中的手势
Flutter中的手势系统有两个独立的层。第一层有原始指针(pointer)事件,它描述了屏幕上指针(例如,触摸,鼠标和触控笔)的位置和移动。 第二层有手势,描述由一个或多个指针移动组成的语义动作。
Pointers
指针(Pointer)代表用户与设备屏幕交互的原始数据。有四种类型的指针事
- PointerDownEvent 指针接触到屏幕的特定位置
- PointerMoveEvent指针从屏幕上的一个位置移动到另一个位置
- PointerUpEvent 指针停止接触屏幕
- PointerCancelEvent 指针的输入事件不再针对此应用(事件取消)
手势
手势表示可以从多个单独的指针事件(甚至可能是多个单独的指针)识别的语义动作(例如,轻敲,拖动和缩放)。 完整的一个手势可以分派多个事件,对应于手势的生命周期(例如,拖动开始,拖动更新和拖动结束)
- Tap
- onTapDown 指针已经在特定位置与屏幕接触
- onTapUp 指针停止在特定位置与屏幕接触
- onTap tap事件触发
- onTapCancel 先前指针触发的onTapDown不会在触发tap事件
- 双击
- onDoubleTap 用户快速连续两次在同一位置轻敲屏幕.
- 长按
- onLongPress 指针在相同位置长时间保持与屏幕接触
- 垂直拖动
- onVerticalDragStart 指针已经与屏幕接触并可能开始垂直移动
- onVerticalDragUpdate 指针与屏幕接触并已沿垂直方向移动.
- onVerticalDragEnd 先前与屏幕接触并垂直移动的指针不再与屏幕接触,并且在停止接触屏幕时以特定速度移动
- 水平拖动
- onHorizontalDragStart 指针已经接触到屏幕并可能开始水平移动
- onHorizontalDragUpdate 指针与屏幕接触并已沿水平方向移动
- onHorizontalDragEnd 先前与屏幕接触并水平移动的指针不再与屏幕接触,并在停止接触屏幕时以特定速度移动
使用 GestureDetector,从widget层监听手势
GestureDetector:该 widget并不具有显示效果,而是检测由用户做出的手势。可以使用GestureDetector来检测各种输入手势,包括点击、拖动和缩放。许多widget都会使用一个GestureDetector为其他widget提供可选的回调。 例如,IconButton、 RaisedButton、 和FloatingActionButton ,它们都有一个onPressed回调,它会在用户点击该widget时被触发。
手势消歧
在屏幕上的指定位置,可能会有多个手势检测器。所有这些手势检测器在指针事件流经过并尝试识别特定手势时监听指针事件流。 GestureDetector widget决定是哪种手势。
当屏幕上给定指针有多个手势识别器时,框架通过让每个识别器加入一个“手势竞争场”来确定用户想要的手势。“手势竞争场”使用以下规则确定哪个手势胜出
- 在任何时候,识别者都可以宣布失败并离开“手势竞争场”。如果在“竞争场”中只剩下一个识别器,那么该识别器就是赢家
- 在任何时候,识别者都可以宣布胜利,这会导致胜利,并且所有剩下的识别器都会失败
例如,在消除水平和垂直拖动的歧义时,两个识别器在接收到指针向下事件时进入“手势竞争场”。识别器观察指针移动事件。 如果用户将指针水平移动超过一定数量的逻辑像素,则水平识别器将声明胜利,并且手势将被解释为水平拖拽。 类似地,如果用户垂直移动超过一定数量的逻辑像素,垂直识别器将宣布胜利。(判断是水平移动还是横向移动)
当只有水平(或垂直)拖动识别器时,“手势竞争场”是有益的。在这种情况下,“手势竞争场”将只有一个识别器,并且水平拖动将被立即识别,这意味着水平移动的第一个像素可以被视为拖动,用户不需要等待进一步的手势消歧。
触摸事件流
指针按下事件(以及该指针的后续事件)然后被分发到由_命中测试_发现的最内部的widget。 从那里开始,这些事件会冒泡在widget树中向上冒泡,这些事件会从最内部的widget被分发到到widget根的路径上的所有小部件(译者语:这和web中事件冒泡机制相似), 没有机制取消或停止冒泡过程(译者语:这和web不同,web中的时间冒泡是可以停止的)。
Android中的触摸事件分发是从最顶层的view一直检索到最后的子view,子view不处理的话,然后逐级回到顶级view
Canvas draw/paint
在Android中,您可以使用Canvas在屏幕上绘制自定义形状。
Flutter有两个类可以帮助绘制画布,CustomPaint和CustomPainter,它们实现算法然后绘制到画布。
class SignaturePainter extends CustomPainter{
SignaturePainter({this.paints});
final List<Offset>paints;
@override
void paint(Canvas canvas, Size size) {
Paint paint = new Paint()
..color=Colors.red
..strokeWidth=3.0
..strokeCap = StrokeCap.round;
for(int i =0;i<paints.length-1;i++){
if(paints[i]!=null&&paints[i+1]!=null){
canvas.drawLine(paints[1], paints[i+1], paint);
}
}
}
@override
bool shouldRepaint(SignaturePainter oldDelegate) {
return oldDelegate.paints!=paints;
}
}
class Signature extends StatefulWidget {
SignatureState createState() => new SignatureState();
}
class SignatureState extends State<Signature> {
List<Offset> _points = <Offset>[];
Widget build(BuildContext context) {
return new GestureDetector(
onPanUpdate: (DragUpdateDetails details) {
setState(() {
RenderBox referenceBox = context.findRenderObject();
Offset localPosition =
referenceBox.globalToLocal(details.globalPosition);
_points = new List.from(_points)..add(localPosition);
});
},
onPanEnd: (DragEndDetails details) => _points.add(null),
child: new CustomPaint(painter: new SignaturePainter(_points)),
);
}
}
自定义 Widgets
在Android中,您通常会继承View或已经存在的某个控件,然后覆盖其绘制方法来实现自定义View。
在Flutter中,一个自定义widget通常是通过组合其它widget来实现的,而不是继承。
基础 widget
- Row、 Column: 这些具有弹性空间的布局类Widget可让您在水平(Row)和垂直(Column)方向上创建灵活的布局。其设计是基于web开发中的Flexbox布局模型。
- Stack: 取代线性布局 (译者语:和Android中的LinearLayout相似),Stack允许子 widget 堆叠, 你可以使用 Positioned 来定位他们相对于Stack的上下左右四条边的位置。Stacks是基于Web开发中的绝度定位(absolute positioning )布局模型设计的。
- Container: Container 可让您创建矩形视觉元素。container 可以装饰为一个BoxDecoration, 如 background、一个边框、或者一个阴影。 Container 也可以具有边距(margins)、填充(padding)和应用于其大小的约束(constraints)。另外, Container可以使用矩阵在三维空间中对其进行变换。
- Material 组件:Scaffold:Material Design布局结构的基本实现。此类提供了用于显示drawer、snackbar和底部sheet的API Widgets 总览 - Material 组件。
Container
一个便利的小部件,结合了常见的绘画,定位和大小调整小部件
在重绘期间,首先应用给定的transform,然后绘制decorationz装饰空间,然后绘制子控件,最后显示foregroundDecoration。
- 窗口小部件没有子窗口,没有height,没有width,没有约束,并且父窗口提供无限制约束,则Container会尝试尽可能小。
- 窗口小部件没有孩子,没有对齐,而是一个height,width或 限制提供,则Container尝试给出这些限制和家长的约束相结合,以尽可能小。
- 窗口小部件没有子窗口,没有height,没有width,没有约束,没有对齐,但是父窗口提供了有界约束,那么Container会扩展以适应父窗口提供 的约束。
- 窗口小部件具有对齐,并且父窗口提供无限制约束,则Container会尝试围绕子窗口调整自身大小
- 窗口小部件具有对齐,并且父窗口提供有界约束,则Container会尝试展开以适合父窗口,然后根据对齐方式将子项置于其自身内部。
- 窗口小部件有一个子,但没有height,没有width,没有 约束,没有对齐,Container将约束从父级传递给子级,并调整自身大小以匹配子级。
- Container构造
Container{ Key key, AlignmentGeometry alignment, EdgeInsetsGeometry padding, Color color, Decoration decoration, Decoration foregroundDecoration, double width, double height, BoxConstraints constraints, EdgeInsetsGeometry margin, Matrix4 transform, Widget child })
属性
- alignment → AlignmentGeometry:子控件的对其方式
- padding → EdgeInsetsGeometry:空白空间装饰子控件。
- color → Color:背景色
- decoration → Decoration:子控件背后的装饰
- foregroundDecoration → Decoration:子控件的前景装饰
- constraints → BoxConstraints:适用于子控件的其他限制
- margin → EdgeInsetsGeometry:装饰和子控件周围的空白空间
- transform → Matrix4:在绘制容器之前应用的转换矩阵。
Row
在水平方向上排列子widget的列表。 要使子项扩展以填充可用的水平空间,可以将子项包装在Expanded小部件中。只有一个child,可以使用Align或Center来定位child。
Row的布局分六步进行
- 使用无界水平约束和传入垂直约束将每个子布局为可变布局(例如,未展开的控件 )。如果crossAxisAlignment是 CrossAxisAlignment.stretch,则使用与传入的最大高度匹配的紧密垂直约束。
- 根据其可变系数,将具有非可变控件(例如,扩展的那些)的孩子之间的剩余水平空间划分。例如,flex系数为2.0的孩子将获得两倍于水平空间的孩子,其灵活系数为1.0。
- 使用与步骤1中相同的垂直约束对每个剩余子项进行布局,但不使用无界水平约束,而是使用基于步骤2中分配的空间量的水平约束。具有FlexFit.tight的Flexible.fit属性的子项是给定严格约束(即,强制填充分配的空间),具有FlexFit.loose的Flexible.fit属性的子项被赋予松散约束(即,不强制填充分配的空间)。
- 所述的高度行是子控件(这将始终满足传入垂直约束)的最大高度。
- Row的宽度由mainAxisSize属性确定。如果mainAxisSize属性是MainAxisSize.max,则 Row的宽度是传入约束的最大宽度。如果mainAxisSize 属性是MainAxisSize.min,则Row的宽度是子节点宽度的总和(受传入约束限制)
- 根据mainAxisAlignment和crossAxisAlignment确定每个子项的位置 。例如,如果 mainAxisAlignment是MainAxisAlignment.spaceBetween,则尚未分配给子项的任何水平空间均匀分配并放置在子项之间。
构造
Row({Key key, MainAxisAlignment mainAxisAlignment: MainAxisAlignment.start, MainAxisSize mainAxisSize: MainAxisSize.max, CrossAxisAlignment crossAxisAlignment: CrossAxisAlignment.center, TextDirection textDirection, VerticalDirection verticalDirection: VerticalDirection.down, TextBaseline textBaseline, List children: const [] })
- hildren → List:row中的子控件
- mainAxisAlignment → MainAxisAlignment:如何将孩子放在主轴上。MainAxisAlignment有MainAxisAlignment如下属性
- center:将孩子放在尽可能靠近主轴中间的位置。
- end: 将孩子放在尽可能靠近主轴末端的位置。 如果在水平方向上使用此值,则必须使用TextDirection来确定结尾是左侧还是右侧。 如果在垂直方向上使用此值,则必须使用VerticalDirection来确定结尾是顶部还是底部。
- spaceAround:将自由空间均匀地放置在孩子之间,以及在第一个孩之前和最后一个孩子之后放置均分的一半空间(其他子控件间的一半长度空间)。
- spaceBetween :将自由空间均匀放置在孩子之间。
- spaceEvenly:将自由空间均匀地放置在孩子之间,第一个子控件之前和最后一个子控件之后也有等长的空间。
- start :将孩子放在尽可能靠近主轴起点的位置。 如果在水平方向上使用此值,则必须使用TextDirection来确定开头是左侧还是右侧。 如果在垂直方向上使用此值,则必须使用VerticalDirection来确定开始是顶部还是底部。
- values→const List < MainAxisAlignment >:此枚举中值的常量列表,按其声明顺序排列。
- mainAxisSize → MainAxisSize:主轴应占用多少空间。
- crossAxisAlignment → CrossAxisAlignment:如何将子控件放在十字轴上。CrossAxisAlignment有如下属性
- baseline :将孩子放在十字轴上,使他们的基线匹配。 如果主轴是垂直的,则将该值视为start (因为基线始终是水平的)。
- center :放置孩子,使他们的中心与十字轴的中间对齐。 这是默认的横轴对齐方式。
- end :将孩子尽可能靠近十字轴的末端。 例如,在TextDirection为TextDirection.ltr的列(具有垂直轴的flex)中 ,这会沿着列的右边缘对齐子项的右边缘。 如果在水平方向上使用此值,则必须使用TextDirection来确定结尾是左侧还是右侧。 如果在垂直方向上使用此值,则必须使用VerticalDirection来确定结尾是顶部还是底部
- start :将孩子的起始边缘与十字轴的起始侧对齐。 例如,在TextDirection为TextDirection.ltr的列(具有垂直轴的flex)中 ,这会沿着列的左边缘对齐子项的左边缘。 如果在水平方向上使用此值,则必须使用TextDirection来确定开头是左侧还是右侧。 如果在垂直方向上使用此值,则必须使用VerticalDirection来确定开始是顶部还是底部。
- stretch:要求孩子填充十字轴。 这会导致传递给子节点的约束在十字轴上变紧。
- values→const List < CrossAxisAlignment >:此枚举中值的常量列表,按其声明顺序排列。
- textDirection → TextDirection:确定为水平,以及如何解释奠定孩子出去 start,并end在水平方向上。
- verticalDirection → VerticalDirection:确定垂直放置孩子以及如何解释start和end垂直方向的顺序 。
- textBaseline → TextBaseline:如果根据基线对齐项目,则使用哪个基线。
Column
在垂直方向上排列子widget的列表。要使子项扩展以填充可用的垂直空间,可以将子项包装在Expanded小部件中。只有一个孩子,可以考虑使用Align或Center来定位孩子。 此widget和 Row 属性布局步骤还有构造几乎一样只是主轴和副轴方向互换
Stack
一个小部件,它将子节点相对于其边框定位。 如果要以简单的方式重叠多个子项,此类很有用,例如,具有一些文本和图像,用渐变覆盖并且按钮附加到底部。
Stack小部件的每个子节点都已定位或未定位。定位子项是包含在具有至少一个非null属性的定位窗口小部件中的子项。堆栈大小自身包含所有未定位的子项,这些子项根据对齐方式定位 (默认为从左到右环境中的左上角和从右到左环境中的右上角)。然后根据它们的顶部,右侧,底部和左侧属性相对于堆叠放置定位的子项。
构造
Stack({ Key key, AlignmentGeometry alignment:AlignmentDirectional.topStart, TextDirection textDirection, StackFit fit:StackFit.loose, Overflow overflow:Overflow.clip, List < Widget > children:const [] })
属性
- alignment → AlignmentGeometry 如何将堆叠中未定位和部分定位的子项对齐。
- fit → StackFit 如何调整堆栈中未定位的子项的大小。
- textDirection → TextDirection 用于解决对齐的文本方向。
- 4.overflow → Overflow 是否应该剪掉溢出的孩子。
总结
Flutter中的页面都是一个一个widget堆叠起来的,虽然其中widget的缩进看着很麻烦,但是由于页面是一个一个widget,可重用性就大大增强,对页面进行适当的文件分级也是必要的,当作一个技能来掌握就可以了。