Flutter 学习
参照:https://book.flutterchina.club/ 目前进度:https://book.flutterchina.club/chapter6/animatedlist.html Material Design所有图标可以在其官网查看:https://material.io/tools/icons/
一、基础
基础显示模块为:StatelessWidget 相当于–安卓的Activity;
基础渲染布局的位置:StatelessWidget-build 返回 new MaterialApp 相当于–安卓的setContentView();
页面标题:MaterialApp-title 相当于–安卓的manifist的label
主题:MaterialApp-theme
标题栏:MaterialApp-home-Scaffold-appBar;
标题栏左侧:MaterialApp-home-Scaffold-appBar-leading;
标题栏右侧:MaterialApp-home-Scaffold-appBar- actions: [];
内容主体:MaterialApp-home-Scaffold-body;
悬浮按钮:MaterialApp-home-Scaffold-floatingActionButton
内容区域:Text控件(不可变)或者StatefulWidget(可变);
私有变量:以“_”下划线为前缀的变量;
绑定State状态控制:StatefulWidget-createState
点击事件:Widget-onTap
触发页面刷新:setState((){})
跳转到下一个页面: Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) {}),); 或者Navigator.of(context).pushNamed(RouterPath.pagePathLogin);
页面路由注册:MaterialApp-routes: <String, WidgetBuilder>{RouterPath.pagePathLogin: (BuildContext context) => Login(),}
调试:debugger(when: offset > 30.0);
切换焦点:FocusScope.of(context).requestFocus(FocusNode());
隐藏软键盘:FocusNode().unfocus();
焦点改变监听:FocusNode().addListener((){print(focusNode.hasFocus);});
屏幕宽度:double.infinity
获取当前RenderObject大小:context.size
获取当前Widget主题:
二、控件
1.基础
文本—Text()
该组件可让您创建一个带格式的文本。
- pair:文字文本
- textAlign:文本的对齐方式
- maxLines:指定文本显示的最大行数
- overflow:如果有多余的文本,可以通过overflow来指定截断方式,默认是直接截断,本例中指定的截断方式TextOverflow.ellipsis,它会将多余文本截断后以省略符“…”表示
- textScaleFactor:文本相对于当前字体大小的缩放因子,默认值将为1.0;
- TextStyle.color:文本颜色;
- TextStyle.fontSize:文本字体大小;
- TextStyle.height:文本行高,但它并不是一个绝对值,而是一个因子,具体的行高等于fontSize*height;
- TextStyle.fontFamily:文本字体;
- TextStyle.textScaleFactor:跟随系统的字体大小;
- TextStyle.background:文本背景;
- TextStyle.decoration:文本装饰,下划线等;
- TextStyle.decorationStyle:文本装饰,样式;
- TextStyle.inherit:是否继承默认样式;
不同样式显示文本—TextSpan
- style:默认样式;
- text:显示文本;
- children:是一个TextSpan的数组,也就是说TextSpan可以包括其他TextSpan;
- recognizer:用于对该文本片段上用于手势进行识别处理;
– 点击处理:recognizer: TapGestureRecognizer()…onTap = () {}
图标按钮—IconButton()
是一个可点击的Icon,不包括文字,默认没有背景,点击后会出现背景
漂浮按钮—ElevatedButton()
默认带有阴影和灰色背景。按下后,阴影会变大
文本按钮—TextButton()
默认背景透明并不带阴影。按下后,会有背景色
边框按钮—OutlineButton()
默认有一个边框,不带阴影且背景透明。按下后,边框颜色会变亮、同时出现背景和阴影(较弱)
图片加载—Image()
数据源可以是asset、文件、内存以及网络。 ImageProvider:是一个抽象类,主要定义了图片数据获取的接口load(),从不同的数据源获取图片需要实现不同的ImageProvider ,如AssetImage是实现了从Asset中加载图片的 ImageProvider,而NetworkImage 实现了从网络加载图片的 ImageProvider。 一个必选的image参数,它对应一个 ImageProvider。
- AssetImage:资源图片加载;
- NetworkImage:网络图片加载
属性
- width:图片宽度;
- height:图片高度;
- color:图片的混合色值,相当于tint;
- colorBlendMode:混合模式,相当于tint渲染模式;
- fit:缩放模式,模式是在BoxFit中定义,它是一个枚举类型;
– fill:会拉伸填充满显示空间,图片本身长宽比会发生变化,图片会变形。 – cover:会按图片的长宽比放大后居中填满显示空间,图片不会变形,超出显示空间部分会被剪裁。 – contain:这是图片的默认适应规则,图片会在保证图片本身长宽比不变的情况下缩放以适应当前显示空间,图片不会变形。 – fitWidth:图片的宽度会缩放到显示空间的宽度,高度会按比例缩放,然后居中显示,图片不会变形,超出显示空间部分会被剪裁。 – fitHeight:图片的高度会缩放到显示空间的高度,宽度会按比例缩放,然后居中显示,图片不会变形,超出显示空间部分会被剪裁。 – none:图片没有适应策略,会在显示空间内显示图片,如果图片比显示空间大,则显示空间只会显示图片中间部分。 - alignment:对齐方式;
- repeat:重复方式;
圆形图片—CircleAvatar()
- backgroundColor:需要显示的背景图片provide
- radius:图片大小
字体图标—Text.style.fontFamily
所有图标 开启逻辑:
flutter:
uses-material-design: true
开关按钮—Switch()
只能定义宽度,高度也是固定的,不保存状态;
- onChanged:状态改变监听;
- activeColor:选中状态颜色;
选择按钮—Checkbox()
大小是固定的,无法自定义,不保存状态;
- onChanged:状态改变监听;
- activeColor:选中状态颜色;
- tristate:表示是否为三态,其默认值为false ,这时 Checkbox 有两种状态即“选中”和“不选中”,对应的 value 值为true和false ;如果tristate值为true时,value 的值会增加一个状态null,读者可以自行测试;
文本输入框—TextField()
- controller:(TextEditingController)编辑框的控制器,通过它可以设置/获取编辑框的内容、选择编辑内容、监听编辑文本改变事件。大多数情况下我们都需要显式提供一个controller来与文本框交互。如果没有提供controller,则TextField内部会自动创建一个。
- focusNode:(FocusNode)用于控制TextField是否占有当前键盘的输入焦点。它是我们和键盘交互的一个句柄(handle)。
- decoration:(InputDecoration)用于控制TextField的外观显示,如提示文本、背景颜色、边框等。
- keyboardType:(TextInputType)用于设置该输入框默认的键盘输入类型,取值如下:
– text 文本输入键盘 – multiline 多行文本,需和maxLines配合使用(设为null或大于1) – number 数字;会弹出数字键盘 – phone 优化后的电话号码输入键盘;会弹出数字键盘并显示“* #” – datetime 优化后的日期输入键盘;Android上会显示“: -” – emailAddress 优化后的电子邮件地址;会显示“@ .” – url 优化后的url输入键盘; 会显示“/ .” - textInputAction:(TextInputAction)键盘动作按钮图标(即回车键位图标),它是一个枚举值,有多个可选值,例如->TextInputAction.search
- style:(TextStyle)正在编辑的文本样式。
- textAlign:(TextAlign)输入框内编辑文本在水平方向的对齐方式。
- autofocus:是否自动获取焦点。
- obscureText:是否隐藏正在编辑的文本,如用于输入密码的场景等,文本内容会用“?”替换。
- maxLines:输入框的最大行数,默认为1;如果为null,则无行数限制。
- maxLength:代表输入框文本的最大长度,设置后输入框右下角会显示输入的文本计数;
- maxLengthEnforcement:决定当输入文本长度超过maxLength时如何处理,如截断、超出等。
- toolbarOptions:长按或鼠标右击时出现的菜单,包括 copy、cut、paste 以及 selectAll。
- onChange:输入框内容改变时的回调函数;注:内容改变事件也可以通过controller来监听
- onEditingComplete:输入框输入完成时触发,函数无参数;
- onSubmitted:回调是ValueChanged类型,它接收当前输入内容做为参数;
- inputFormatters:用于指定输入格式;当用户输入内容改变时,会根据指定的格式来校验。
- enable:如果为false,则输入框会被禁用,禁用状态不接收输入和事件,同时显示禁用态样式(在其decoration中定义)。
- cursorWidth:输入框光标宽度;
- cursorRadius:输入框光标圆角;
- cursorColor:输入框光标颜色;
- decoration.labelText:输入框文本;
- decoration.prefixIcon:输入框图标;
- decoration.enabledBorder:未获得焦点样式;
- decoration.focusedBorder:获得焦点样式;
- decoration.hintStyle:提示颜色;
- decoration.hintText:提示文本;
- decoration.border:(InputBorder)下划线或者说是边框;
表单—Form()
实际业务中,在正式向服务器提交数据前,都会对各个输入框数据进行合法性校验,但是对每一个TextField都分别进行校验将会是一件很麻烦的事。还有,如果用户想清除一组TextField的内容,除了一个一个清除有没有什么更好的办法呢?为此,Flutter提供了一个Form 组件,它可以对输入框进行分组,然后进行一些统一操作,如输入内容校验、输入框重置以及输入内容保存。 FormState:FormState为Form的State类,可以通过Form.of()或GlobalKey获得。我们可以通过它来对Form的子孙FormField进行统一操作。我们看看其常用的三个方法:
- FormState.validate():调用此方法后,会调用Form子孙FormField的validate回调,如果有一个校验失败,则返回false,所有校验失败项都会返回用户返回的错误提示。
- FormState.save():调用此方法后,会调用Form子孙FormField的save回调,用于保存表单内容
- FormState.reset():调用此方法后,会将子孙FormField的内容清空。
登录按钮的onPressed方法中不能通过Form.of(context)来获取FormState,原因是,此处的context为FormTestRoute的context,而Form.of(context)是根据所指定context向根去查找,而FormState是在FormTestRoute的子树中,所以不行。正确的做法是通过Builder来构建登录按钮,Builder会将widget节点的context作为回调参数:
属性:
- autovalidate:是否自动校验输入内容;当为true时,每一个子 FormField 内容发生变化时都会自动校验合法性,并直接显示错误信息。否则,需要通过调用FormState.validate()来手动校验。
- onWillPop:决定Form所在的路由是否可以直接返回(如点击返回按钮),该回调返回一个Future对象,如果 Future 的最终结果是false,则当前路由不会返回;如果为true,则会返回到上一个路由。此属性通常用于拦截返回按钮。
- onChanged:Form的任意一个子FormField内容发生变化时会触发此回调。
表单子元素—FormField()
Form的子孙元素必须是FormField类型,FormField是一个抽象类,定义几个属性,FormState内部通过它们来完成操作
- onSaved:(FormFieldSetter )保存回调;
- validator:(FormFieldValidator )保存回调;
- initialValue:初始值;
- autovalidate:是否自动校验;
表单输入控件—TextFormField()
继承自FormField类,也是TextField的一个包装类,所以除了FormField定义的属性之外,它还包括TextField的属性
线性进度条----LinearProgressIndicator()
- value:value表示当前的进度,取值范围为[0,1];如果value为null时则指示器会执行一个循环动画(模糊进度);当value不为null时,指示器为一个具体进度的进度条;
- backgroundColor:指示器的背景色;
- valueColor:指示器的进度条颜色;值得注意的是,该值类型是Animation,这允许我们对进度条的颜色也可以指定动画。如果我们不需要对进度条颜色执行动画,换言之,我们想对进度条应用一种固定的颜色,此时我们可以通过AlwaysStoppedAnimation来指定;
圆形进度条—CircularProgressIndicator()
- value:value表示当前的进度,取值范围为[0,1];如果value为null时则指示器会执行一个循环动画(模糊进度);当value不为null时,指示器为一个具体进度的进度条;
- backgroundColor:指示器的背景色;
- valueColor:指示器的进度条颜色;值得注意的是,该值类型是Animation,这允许我们对进度条的颜色也可以指定动画。如果我们不需要对进度条颜色执行动画,换言之,我们想对进度条应用一种固定的颜色,此时我们可以通过AlwaysStoppedAnimation来指定;
- strokeWidth:圆形进度条的粗细;
2.约束
盒模型配置—BoxConstraints(最大最小宽高)
盒模型额外约束—ConstrainedBox()
盒模型固定宽高—SizedBox()
拦截父级约束—UnconstrainedBox()
指定子组件的长宽比—AspectRatio()
指定最大宽高—LimitedBox()
可以根据父容器宽高的百分比来设置子组件宽高—FractionallySizedBox()
3.布局
弹性布局—Flex()
Flex组件可以沿着水平或垂直方向排列子组件,如果你知道主轴方向,使用Row或Column会方便一些,因为Row和Column都继承自Flex,参数基本相同,所以能使用Flex的地方基本上都可以使用Row或Column。Flex本身功能是很强大的,它也可以和Expanded组件配合实现弹性布局。接下来我们只讨论Flex和弹性布局相关的属性(其他属性已经在介绍Row和Column时介绍过了)。
弹性布局比例扩展—Expanded()相对于安卓等比布局
Expanded 只能作为 Flex 的孩子(否则会报错),它可以按比例“扩伸”Flex子组件所占用的空间。因为 Row和Column 都继承自 Flex,所以 Expanded 也可以作为它们的孩子。 flex参数为弹性系数,如果为 0 或null,则child是没有弹性的,即不会被扩伸占用的空间。如果大于0,所有的Expanded按照其 flex 的比例来分割主轴的全部空闲空间。 Spacer的功能是占用指定比例的空间,实际上它只是Expanded的一个包装类,相当于空白占位
流式布局—Wrap、Flow
溢出部分则会自动折行
- spacing:主轴方向子widget的间距
- runSpacing:纵轴方向的间距
- runAlignment:纵轴方向的对齐方式
层叠布局—(Stack、Positioned)
层叠布局和 Web 中的绝对定位、Android 中的 Frame 布局是相似的,子组件可以根据距父容器四个角的位置来确定自身的位置。层叠布局允许子组件按照代码中声明的顺序堆叠起来。Flutter中使用Stack和Positioned这两个组件来配合实现绝对定位。Stack允许子组件堆叠,而Positioned用于根据Stack的四个角来确定子组件的位置。
- Positioned:left、top 、right、 bottom分别代表离Stack左、上、右、底四边的距离。width和height用于指定需要定位元素的宽度和高度。注意,Positioned的width、height 和其他地方的意义稍微有点区别,此处用于配合left、top 、right、 bottom来定位组件,举个例子,在水平方向时,你只能指定left、right、width三个属性中的两个,如指定left和width后,right会自动算出(left+width),如果同时指定三个属性则会报错,垂直方向同理。
- alignment:此参数决定如何去对齐没有定位(没有使用Positioned)或部分定位的子组件。所谓部分定位,在这里特指没有在某一个轴上定位:left、right为横轴,top、bottom为纵轴,只要包含某个轴上的一个定位属性就算在该轴上有定位。默认居中
- textDirection:和Row、Wrap的textDirection功能一样,都用于确定alignment对齐的参考系,即:textDirection的值为TextDirection.ltr,则alignment的start代表左,end代表右,即从左往右的顺序;textDirection的值为TextDirection.rtl,则alignment的start代表右,end代表左,即从右往左的顺序。
- fit:此参数用于确定没有定位的子组件如何去适应Stack的大小。StackFit.loose表示使用子组件的大小,StackFit.expand表示扩伸到Stack的大小。
- clipBehavior:此属性决定对超出Stack显示空间的部分如何剪裁,Clip枚举类中定义了剪裁的方式,Clip.hardEdge 表示直接剪裁,不应用抗锯齿,更多信息可以查看源码注释。
对齐与相对定位—Align()
简单的调整一个子元素在父元素中的位置的话,使用Align组件会更简单一些。
- alignment:表示子组件在父组件中的起始位置。
- widthFactor、heightFactor:用于确定Align 组件本身宽高的属性;它们是两个缩放因子,会分别乘以子元素的宽、高,最终的结果就是Align 组件的宽高。如果值为null,则组件的宽高将会占用尽可能多的空间。
父布局信息获取—LayoutBuilder()
通过 LayoutBuilder,我们可以在布局过程中拿到父组件传递的约束信息,然后我们可以根据约束信息动态的构建不同的布局。
布局结束后布局信息回调—AfterLayout()
可以在子组件布局完成后执行一个回调,并同时将 RenderObject 对象作为参数传递。
Row、 Column
这些具有弹性空间的布局类Widget可让您在水平(Row)和垂直(Column)方向上创建灵活的布局。其设计是基于web开发中的Flexbox布局模型。
- children: [] 子控件列表
- textDirection: 表示水平方向子组件的布局顺序(是从左往右还是从右往左),默认为系统当前Locale环境的文本方向(如中文、英语都是从左往右,而阿拉伯语是从右往左)。即开始布局顺序,是从左侧开始布局第一个还是右侧开始布局第一个
- mainAxisSize: 表示Row在主轴(水平)方向占用的空间,默认是MainAxisSize.max,表示尽可能多的占用水平方向的空间,此时无论子 widgets 实际占用多少水平空间,Row的宽度始终等于水平方向的最大宽度;而MainAxisSize.min表示尽可能少的占用水平空间,当子组件没有占满水平剩余空间,则Row的实际宽度等于所有子组件占用的的水平空间;相当于是安卓的match还是wrap_content
- mainAxisAlignment: 表示子组件在Row所占用的水平空间内对齐方式,如果mainAxisSize值为MainAxisSize.min,则此属性无意义,因为子组件的宽度等于Row的宽度。只有当mainAxisSize的值为MainAxisSize.max时,此属性才有意义,MainAxisAlignment.start表示沿textDirection的初始方向对齐,如textDirection取值为TextDirection.ltr时,则MainAxisAlignment.start表示左对齐,textDirection取值为TextDirection.rtl时表示从右对齐。而MainAxisAlignment.end和MainAxisAlignment.start正好相反;MainAxisAlignment.center表示居中对齐。读者可以这么理解:textDirection是mainAxisAlignment的参考系。相当于安卓的layout_gravity(水平方向)
- verticalDirection: 表示Row纵轴(垂直)的对齐方向,默认是VerticalDirection.down,表示从上到下。即开始布局顺序,是从底下开始第一个还是从顶部开始第一个
容器(Row、Column等).crossAxisAlignment: 表示子组件在纵轴方向的对齐方式,Row的高度等于子组件中最高的子元素高度,它的取值和MainAxisAlignment一样(包含start、end、 center三个值),不同的是crossAxisAlignment的参考系是verticalDirection,即verticalDirection值为VerticalDirection.down时crossAxisAlignment.start指顶部对齐,verticalDirection值为VerticalDirection.up时,crossAxisAlignment.start指底部对齐;而crossAxisAlignment.end和crossAxisAlignment.start正好相反;相当于安卓的layout_gravity(垂直方向)
Stack
取代线性布局 (译者语:和Android中的LinearLayout相似),Stack允许子 widget 堆叠, 你可以使用 Positioned 来定位他们相对于Stack的上下左右四条边的位置。Stacks是基于Web开发中的绝度定位(absolute positioning )布局模型设计的。
3.容器
布局填充—Padding()
可以给其子节点添加填充(留白),和边距效果类似。
- EdgeInsets.fromLTRB:分别指定四个方向的填充。
- EdgeInsets.all:所有方向均使用相同数值的填充。
- EdgeInsets.only:可以设置具体某个方向的填充(可以同时指定多个方向)。
- EdgeInsets.symmetric:用于设置对称方向的填充,vertical指top和bottom,horizontal指left和right。
装饰容器—DecoratedBox()
可以在其子组件绘制前(或后)绘制一些装饰(Decoration),如背景、边框、渐变等。
- decoration:代表将要绘制的装饰,它的类型为Decoration。Decoration是一个抽象类,它定义了一个接口 createBoxPainter(),子类的主要职责是需要通过实现它来创建一个画笔,该画笔用于绘制装饰。
- position:此属性决定在哪里绘制Decoration,它接收DecorationPosition的枚举类型,该枚举类有两个值:background:在子组件之后绘制,即背景装饰。foreground:在子组件之上绘制,即前景。
装饰容器常用—DecoratedBox.decoration(BoxDecoration())
- color:颜色
- image:图片
- border:边框
- borderRadius:圆角
- boxShadow:阴影,可以指定多个
- gradient:渐变
- backgroundBlendMode:背景混合模式
- shape:形状
变换—Transform()
可以在其子组件绘制时对其应用一些矩阵变换来实现一些特效。 Transform.translate接收一个offset参数,可以在绘制时沿x、y轴对子组件平移指定的距离。 Transform.rotate可以对子组件进行旋转变换 Transform.scale可以对子组件进行缩小或放大
变换—RotatedBox()
变换是在layout阶段,会影响在子组件的位置和大小。
组件—Container()
Container 可让您创建矩形视觉元素。container 可以装饰为一个BoxDecoration, 如 background、一个边框、或者一个阴影。 Container 也可以具有边距(margins)、填充(padding)和应用于其大小的约束(constraints)。另外, Container可以使用矩阵在三维空间中对其进行变换。 是一个组合类容器,它本身不对应具体的RenderObject,它是DecoratedBox、ConstrainedBox、Transform、Padding、Align等组件组合的一个多功能容器,所以我们只需通过一个Container组件可以实现同时需要装饰、变换、限制的场景。
- padding:容器内补白,属于decoration的装饰范围
- margin:容器外补白,不属于decoration的装饰范围
- color:背景色
- decoration:背景装饰
- foregroundDecoration:前景装饰
- width:容器的宽度
- height:容器的高度
- constraints:容器大小的限制条件
- transform:变换
剪裁
Clip()
ClipOval()
沿宽高进行圆形或椭圆裁剪
ClipRRect()
圆角矩形剪裁
ClipRect()
默认剪裁掉子组件布局空间之外的绘制内容(溢出部分剪裁)
ClipPath()
按照自定义的路径剪裁
继承CustomClipper()
getClip() 是用于获取剪裁区域的接口,由于图片大小是60×60,我们返回剪裁区域为Rect.fromLTWH(10.0, 15.0, 40.0, 30.0),即图片中部40×30像素的范围。 shouldReclip() 接口决定是否重新剪裁。如果在应用中,剪裁区域始终不会发生变化时应该返回false,这样就不会触发重新剪裁,避免不必要的性能开销。如果剪裁区域会发生变化(比如在对剪裁区域执行一个动画),那么变化后应该返回true来重新执行剪裁。
空间适配—FittedBox()
单独的一个控件超出父容器限制处理
- fit:适配方式
- alignment:对齐方式
- clipBehavior:是否剪裁
页面骨架—Scaffold()
包含:一个导航栏、导航栏右边有一个分享按钮、有一个抽屉菜单、有一个底部导航、右下角有一个悬浮的动作按钮
- appBar:导航栏
- appBar.leading:导航栏最左侧Widget,常见为抽屉菜单按钮或返回按钮。
- appBar.automaticallyImplyLeading:如果leading为null,是否自动实现默认的leading按钮
- appBar.title:页面标题
- appBar.actions:导航栏右侧菜单
- appBar.bottom:导航栏底部菜单,通常为Tab按钮组
- appBar.elevation:导航栏阴影
- appBar.centerTitle:标题是否居中
- appBar.backgroundColor:背景颜色
- drawer:抽屉
- bottomNavigationBar:底部导航
- floatingActionButton:悬浮按钮
4.可滚动组件
布局模型
主要由三个角色组成:Scrollable、Viewport 和 Sliver:
- Scrollable :用于处理滑动手势,确定滑动偏移,滑动偏移变化时构建 Viewport 。
- Viewport:显示的视窗,即列表的可视区域;
- Sliver:视窗里显示的元素。
具体布局过程:
- Scrollable 监听到用户滑动行为后,根据最新的滑动偏移构建 Viewport 。
- Viewport 将当前视口信息和配置信息通过 SliverConstraints 传递给 Sliver。
- Sliver 中对子组件(RenderBox)按需进行构建和布局,然后确认自身的位置、绘制等信息,保存在 geometry 中(一个 SliverGeometry 类型的对象)。
Scrollable()
用于处理滑动手势,确定滑动偏移,滑动偏移变化时构建 Viewport
Viewport()
用于渲染当前视口中需要显示 Sliver
- offset:用户的滚动偏移
- cacheExtent、cacheExtentStyle:CacheExtentStyle 是一个枚举,有 pixel 和 viewport 两个取值。当 cacheExtentStyle 值为 pixel 时,cacheExtent 的值为预渲染区域的具体像素长度;当值为 viewport 时,cacheExtent 的值是一个乘数,表示有几个 viewport 的长度,最终的预渲染区域的像素长度为:cacheExtent * viewport 的积, 这在每一个列表项都占满整个 Viewport 时比较实用,这时 cacheExtent 的值就表示前后各缓存几个页面。
Sliver()
Sliver 主要作用是对子组件进行构建和布局,比如 ListView 的 Sliver 需要实现子组件(列表项)按需加载功能,只有当列表项进入预渲染区域时才会去对它进行构建和布局、渲染。 Sliver 对应的渲染对象类型是 RenderSliver,RenderSliver 和 RenderBox 的相同点是都继承自 RenderObject 类,不同点是在布局的时候约束信息不同。
Scrollbar()
是一个Material风格的滚动指示器(滚动条),如果要给可滚动组件添加滚动条,只需将Scrollbar作为可滚动组件的任意一个父级组件即可
- axisDirection:滚动方向
- physics:它决定可滚动组件如何响应用户操作,比如用户滑动完抬起手指后,继续执行动画;或者滑动到边界时,如何显示。
- controller:控制滚动位置和监听滚动事件
- viewportBuilder:构建 Viewport 的回调。当用户滑动时,Scrollable 会调用此回调构建新的 Viewport,同时传递一个 ViewportOffset 类型的 offset 参数,该参数描述 Viewport 应该显示那一部分内容。
CupertinoScrollbar()
是 iOS 风格的滚动条,如果你使用的是Scrollbar,那么在iOS平台它会自动切换为CupertinoScrollbar。
SingleChildScrollView()
类似于Android中的ScrollView,它只能接收一个子组件
ListView()
ListView是最常用的可滚动组件之一,它可以沿一个方向线性排布所有子组件,并且它也支持列表项懒加载(在需要时才会创建)。
- itemExtent:该参数如果不为null,则会强制children的“长度”为itemExtent的值;这里的“长度”是指滚动方向上子组件的长度,也就是说如果滚动方向是垂直方向,则itemExtent代表子组件的高度;如果滚动方向为水平方向,则itemExtent就代表子组件的宽度。
- prototypeItem:如果我们知道列表中的所有列表项长度都相同但不知道具体是多少,这时我们可以指定一个列表项,该列表项被称为 prototypeItem(列表项原型)。
- shrinkWrap:该属性表示是否根据子组件的总长度来设置ListView的长度,默认值为false 。默认情况下,ListView会在滚动方向尽可能多的占用空间。当ListView在一个无边界(滚动方向上)的容器中时,shrinkWrap必须为true。
- addAutomaticKeepAlives:该属性我们将在介绍 PageView 组件时详细解释。
- addRepaintBoundaries:该属性表示是否将列表项(子组件)包裹在RepaintBoundary组件中。RepaintBoundary 读者可以先简单理解为它是一个”绘制边界“,将列表项包裹在RepaintBoundary中可以避免列表项不必要的重绘,但是当列表项重绘的开销非常小(如一个颜色块,或者一个较短的文本)时,不添加RepaintBoundary反而会更高效(具体原因会在本书后面 Flutter 绘制原理相关章节中介绍)。如果列表项自身来维护是否需要添加绘制边界组件,则此参数应该指定为 false。
ListView.builder适合列表项比较多或者列表项不确定的情况
- itemBuilder:它是列表项的构建器,类型为IndexedWidgetBuilder,返回值为一个widget。当列表滚动到具体的index位置时,会调用该构建器构建列表项。
- itemCount:列表项的数量,如果为null,则为无限列表。
ListView.separated可以在生成的列表项之间添加一个分割组件,它比ListView.builder多了一个separatorBuilder参数,该参数是一个分割组件生成器。
ps:ListView 中的列表项组件都是 RenderBox,并不是 Sliver, 这个一定要注意。 一个 ListView 中只有一个Sliver,对列表项进行按需加载的逻辑是 Sliver 中实现的。 ListView 的 Sliver 默认是 SliverList,如果指定了 itemExtent ,则会使用 SliverFixedExtentList;如果 prototypeItem 属性不为空,则会使用 SliverPrototypeExtentList,无论是是哪个,都实现了子组件的按需加载模型。
滚动监听
ScrollController
- initialScrollOffset:初始滚动位置
- keepScrollOffset:是否保存滚动位置
- offset:可滚动组件当前的滚动位置。
- jumpTo(double offset)、animateTo(double offset,…):这两个方法用于跳转到指定的位置,它们不同之处在于,后者在跳转时会执行一个动画,而前者不会。
- addListener(()=>print(controller.offset)):添加滚动监听;
- ListView(key: PageStorageKey(1), …):通过不同的key的PageStorageKey来存储各自列表的状态数据,用户恢复;
- controller.positions.elementAt(0):读取记录不同列表的ScrollPosition数据;
原理: 当ScrollController和可滚动组件关联时,可滚动组件首先会调用ScrollController的createScrollPosition()方法来创建一个ScrollPosition来存储滚动位置信息,接着,可滚动组件会调用attach()方法,将创建的ScrollPosition添加到ScrollController的positions属性中,这一步称为“注册位置”,只有注册后animateTo() 和 jumpTo()才可以被调用。 当可滚动组件销毁时,会调用ScrollController的detach()方法,将其ScrollPosition对象从ScrollController的positions属性中移除,这一步称为“注销位置”,注销后animateTo() 和 jumpTo() 将不能再被调用。 需要注意的是,ScrollController的animateTo() 和 jumpTo()内部会调用所有ScrollPosition的animateTo() 和 jumpTo(),以实现所有和该ScrollController关联的可滚动组件都滚动到指定的位置。
NotificationListener
滚动监听的类 接收到滚动事件时,参数类型为ScrollNotification,它包括一个metrics属性,它的类型是ScrollMetrics,该属性包含当前ViewPort及滚动位置等信息:
- pixels:当前滚动位置。
- maxScrollExtent:最大可滚动长度。
- extentBefore:滑出ViewPort顶部的长度;此示例中相当于顶部滑出屏幕上方的列表长度。
- extentInside:ViewPort内部长度;此示例中屏幕显示的列表部分的长度。
- extentAfter:列表中未滑入ViewPort部分的长度;此示例中列表底部未显示到屏幕范围部分的长度。
- atEdge:是否滑到了可滚动组件的边界(此示例中相当于列表顶或底部)
三、控件通用属性
DefaultTextStyle:当前widget节点内部所有文本控件的默认样式 padding:内边距 package:指定报名下的资源 onPressed:点击事件; *.color:渲染颜色 *.icon:图标,按钮的话是默认在左侧添加一个图标; 可滚动组件.scrollDirection:滑动的主轴 可滚动组件.reverse:滑动方向是否反向 可滚动组件.controller:这些属性最终会透传给对应的 Scrollable 和 Viewport 可滚动组件.physics:这些属性最终会透传给对应的 Scrollable 和 Viewport 可滚动组件.cacheExtent:这些属性最终会透传给对应的 Scrollable 和 Viewport
四、函数
Int.isOdd:此整数是否是基数
五、手势
六、生命周期
1.State生命周期
- createState----------只调用一次,通常在该回调中做一些一次性的操作,如状态初始化、订阅子树的事件通知等
- initState----------只调用一次,此时View没有渲染,但是StatefulWidget 已经被加载到渲染树里了
- didChangeDependencies----------当StatefulWidget依赖的InheritedWidget发生变化之后,才会调用
- build----------调用场景:在调用initState()之后、在调用didUpdateWidget()之后、在调用setState()之后、在调用didChangeDependencies()之后、在State对象从树中一个位置移除后(会调用deactivate)又重新插入到树的其他位置之后。
- reassemble----------此回调是专门为了开发调试而提供的,在热重载(hot reload)时会被调用,此回调在Release模式下永远不会被调用。
- addPostFrameCallback ----------StatefulWidget渲染结束之后的回调,只会调用一次
- didUpdateWidget ----------组件状态改变时候调用
- deactivate----------当 State 对象从树中被移除时,会调用此回调。在一些场景下,Flutter 框架会将 State 对象重新插到树中,如包含此 State 对象的子树在树的一个位置移动到另一个位置时(可以通过GlobalKey 来实现)。如果移除后没有重新插入到树中则紧接着会调用dispose()方法。
- dispose----------当 State 对象从树中被永久移除时调用;通常在此回调中释放资源。
2.App生命周期
通过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
七、Flutter 四棵树
既然 Widget 只是描述一个UI元素的配置信息,那么真正的布局、绘制是由谁来完成的呢?Flutter 框架的的处理流程是这样的: Widget->Element->Render->Layer
- 根据 Widget 树生成一个 Element 树,Element 树中的节点都继承自 Element 类。
- 根据 Element 树生成 Render 树(渲染树),渲染树中的节点都继承自RenderObject 类。
- 根据渲染树生成 Layer 树,然后上屏显示,Layer 树中的节点都继承自 Layer 类。
八、定义对象以及使用
/// 定义
class Echo {
const Echo({
Key? key,
required this.text,
this.backgroundColor = Colors.grey, //默认为灰色
}):super(key:key);
}
/// 使用
Echo(text: "hello world");
九、Context
build方法有一个context参数,它是BuildContext类的一个实例,表示当前 widget 在 widget 树中的上下文,每一个 widget 都会对应一个 context 对象(因为每一个 widget 都是 widget 树上的一个节点)。实际上,context是当前 widget 在 widget 树中位置中执行”相关操作“的一个句柄(handle),比如它提供了从当前 widget 开始向上遍历 widget 树以及按照 widget 类型查找父级 widget 的方法。
1.函数
// 在 widget 树中向上查找最近的父级Scaffold widget context.findAncestorWidgetOfExactType(); // 直接通过of静态方法来获取ScaffoldState ScaffoldState _state=Scaffold.of(context); //定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储 static GlobalKey _globalKey= GlobalKey(); Scaffold( key: _globalKey )
十、自定义 Widget
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) {
// 实现绘制
}
}
1.控制台输入显示树结构
debugDumpApp(); debugDumpRenderTree() debugDumpSemanticsTree()
2.要找出相对于帧的开始/结束事件发生的位置
可以切换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: <String, WidgetBuilder>{RouterPath.pagePathLogin: (BuildContext context) => Login(),}
页面路由钩子
MaterialApp-onGenerateRoute: <String, WidgetBuilder>{RouterPath.pagePathLogin: (BuildContext context) => Login(),}
获取路由参数
var args=ModalRoute.of(context).settings.arguments;
注册表配置
MaterialApp(
... //省略无关代码
routes: {
"tip2": (context){
return TipRoute(text: ModalRoute.of(context)!.settings.arguments);
},
},
);
MaterialPageRoute继承自PageRoute类,PageRoute类是一个抽象类,表示占有整个屏幕空间的一个模态路由页面,它还定义了路由构建及切换时过渡动画的相关接口及属性。MaterialPageRoute 是 Material组件库提供的组件,它可以针对不同平台,实现与平台页面切换动画风格一致的路由切换动画
- 对于 Android,当打开新页面时,新的页面会从屏幕底部滑动到屏幕顶部;当关闭页面时,当前页面会从屏幕顶部滑动到屏幕底部后消失,同时上一个页面会显示到屏幕上。
- 对于 iOS,当打开页面时,新的页面会从屏幕右侧边缘一直滑动到屏幕左边,直到新页面全部显示到屏幕上,而上一个页面则会从当前屏幕滑动到屏幕左侧而消失;当关闭页面时,正好相反,当前页面会从屏幕右侧滑出,同时上一个页面会从屏幕左侧滑入。
MaterialPageRoute 构造函数的各个参数的意义:
- builder(WidgetBuilder) 是一个WidgetBuilder类型的回调函数,它的作用是构建路由页面的具体内容,返回值是一个widget。我们通常要实现此回调,返回新路由的实例。
- settings(RouteSettings) 包含路由的配置信息,如路由名称、是否初始路由(首页)。
- maintainState(bool):默认情况下,当入栈一个新路由时,原来的路由仍然会被保存在内存中,如果想在路由没用的时候释放其所占用的所有资源,可以设置maintainState为 false。
- **fullscreenDialog(bool)**表示新的路由页面是否是一个全屏的模态对话框,在 iOS 中,如果- fullscreenDialog为true,新页面将会从屏幕底部滑入(而不是水平方向)。
路由钩子生成 注意,onGenerateRoute 只会对命名路由生效。通过name打开路由前的判断处理。
MaterialApp(
... //省略无关代码
onGenerateRoute:(RouteSettings settings){
return MaterialPageRoute(builder: (context){
String routeName = settings.name;
// 如果访问的路由页需要登录,但当前未登录,则直接返回登录页路由,
// 引导用户登录;其他情况则正常打开路由。
}
);
}
);
十二、参数配置
- name:应用或包名称。
- description: 应用或包的描述、简介。
- version:应用或包的版本号。
- dependencies:应用或包依赖的其他包或插件。
- dev_dependencies:开发环境依赖的工具包(而不是flutter应用本身依赖的包)。
- flutter:flutter相关的配置选项。
1.本地包依赖
dependencies:
pkg1:
path: ../../code/pkg1
2.git依赖
dependencies:
pkg1:
git:
url: git://github.com/xxx/pkg1.git
path: packages/package1(根目录是可以不写这个字段)
十三、资源管理
1.指定 assets(会打包到程序中)
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
2.加载 assets
- 通过rootBundle (opens new window)对象加载:每个Flutter应用程序都有一个rootBundle (opens new window)对象, 通过它可以轻松访问主资源包,直接使用package:flutter/services.dart中全局静态的rootBundle对象来加载asset即可。
- 通过 DefaultAssetBundle (opens new window)加载:建议使用 DefaultAssetBundle (opens new window)来获取当前 BuildContext 的AssetBundle。 这种方法不是使用应用程序构建的默认 asset bundle,而是使父级 widget 在运行时动态替换的不同的 AssetBundle,这对于本地化或测试场景很有用。
3.不同分辨率图片处理
加载可以使用: 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
4.不同平台处理
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、LaunchImage@2x.png、LaunchImage@3x.png。 如果你使用不同的文件名,那您还必须更新同一目录中的Contents.json文件,图片的具体尺寸可以查看苹果官方的标准。
十四、调试相关
1.可视化调试
- 我们也可以通过设置debugPaintSizeEnabled为true以可视方式调试布局问题。 这是来自rendering库的布尔值。它可以在任何时候启用,并在为true时影响绘制。 设置它的最简单方法是在void main()的顶部设置。当它被启用时,所有的盒子都会得到一个明亮的深青色边框,padding(来自widget如Padding)显示为浅蓝色,子widget周围有一个深蓝色框, 对齐方式(来自widget如Center和Align)显示为黄色箭头. 空白(如没有任何子节点的Container)以灰色显示。
- debugPaintBaselinesEnabled (opens new window)做了类似的事情,但对于具有基线的对象,文字基线以绿色显示,表意(ideographic)基线以橙色显示。
- debugPaintPointersEnabled (opens new window)标志打开一个特殊模式,任何正在点击的对象都会以深青色突出显示。 这可以帮助我们确定某个对象是否以某种不正确的方式进行hit测试(Flutter检测点击的位置是否有能响应用户操作的widget),例如,如果它实际上超出了其父项的范围,首先不会考虑通过hit测试。
- 如果我们尝试调试合成图层,例如以确定是否以及在何处添加RepaintBoundary widget,则可以使用debugPaintLayerBordersEnabled (opens new window)标志, 该标志用橙色或轮廓线标出每个层的边界,或者使用debugRepaintRainbowEnabled (opens new window)标志, 只要他们重绘时,这会使该层被一组旋转色所覆盖。
- debugDumpApp() 控制台打印树结构
- debugDumpRenderTree() 控制台打印树结构
- debugDumpSemanticsTree() 控制台打印树结构
所有这些标志只能在调试模式下工作。通常,Flutter框架中以“debug…” 开头的任何内容都只能在调试模式下工作。
2.调试动画
调试动画最简单的方法是减慢它们的速度。为此,请将timeDilation (opens new window)变量(在scheduler库中)设置为大于1.0的数字,例如50.0。 最好在应用程序启动时只设置一次。如果我们在运行中更改它,尤其是在动画运行时将其值改小,则在观察时可能会出现倒退,这可能会导致断言命中,并且这通常会干扰我们的开发工作。
3.调试性能问题
要了解我们的应用程序导致重新布局或重新绘制的原因,我们可以分别设置debugPrintMarkNeedsLayoutStacks (opens new window)和 debugPrintMarkNeedsPaintStacks (opens new window)标志。 每当渲染盒被要求重新布局和重新绘制时,这些都会将堆栈跟踪记录到控制台。如果这种方法对我们有用,我们可以使用services库中的debugPrintStack()方法按需打印堆栈痕迹。
4.统计应用启动时间
要收集有关Flutter应用程序启动所需时间的详细信息,可以在运行flutter run时使用trace-startup和profile选项。 flutter run --trace-startup --profile 跟踪输出保存为start_up_info.json,在Flutter工程目录在build目录下。输出列出了从应用程序启动到这些跟踪事件(以微秒捕获)所用的时间:
- 进入Flutter引擎时.
- 展示应用第一帧时.
- 初始化Flutter框架时.
- 完成Flutter框架初始化时.
5.跟踪Dart代码性能
要执行自定义性能跟踪和测量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)方式不同,
- LeafRenderObjectWidget—非容器类组件基类—Widget树的叶子节点,用于没有子节点的widget,通常基础组件都属于这一类,如Image。
- SingleChildRenderObjectWidget—单子组件基类—包含一个子Widget,如:ConstrainedBox、DecoratedBox等
- MultiChildRenderObjectWidget —多子组件基类—包含多个子Widget,一般都有一个children参数,接受一个Widget数组。如Row、Column、Stack等
Flutter 中有两种布局模型: 1.基于 RenderBox 的盒模型布局。 2.基于 Sliver ( RenderSliver ) 按需加载列表布局。 布局流程如下: 1.上层组件向下层组件传递约束(constraints)条件。 2.下层组件确定自己的大小,然后告诉上层组件。注意下层组件的大小必须符合父组件的约束。 3.上层组件确定下层组件相对于自身的偏移和确定自身的大小(大多数情况下会根据子组件的大小来确定自身的大小)。 盒模型布局组件有两个特点: 1.组件对应的渲染对象都继承自 RenderBox 类。 2.在布局过程中父级传递给子级的约束信息由 BoxConstraints 描述。
|