常用widgets
文本及样式
Text
1 | Text("Hello world", |
textAlign
:文本的对齐方式;可以选择左对齐、右对齐还是居中】
maxLines
、overflow
:指定文本显示的最大行数,默认情况下,文本是自动折行的,如果指定此参数,则文本最多不会超过指定的行。如果有多余的文本,可以通过overflow
来指定截断方式,默认是直接截断
textScaleFactor
:代表文本相对于当前字体大小的缩放因子
TextStyle
TextStyle
用于指定文本显示的样式如颜色、字体、粗细、背景等。
1 | Text("Hello world", |
TextSpan
在上面的例子中,Text的所有文本内容只能按同一种样式,如果我们需要对一个Text内容的不同部分按照不同的样式显示,这时就可以使用TextSpan
。
1 | const TextSpan({ |
1 | Text.rich(TextSpan( |
_tapRecognizer
,它是点击链接后的一个处理器
DefaultTextStyle
在Widget树中,文本的样式默认是可以被继承的,如果在Widget树的某一个节点处设置一个默认的文本样式,那么该节点的子树中所有文本都会默认使用这个样式,而DefaultTextStyle
正是用于设置默认文本样式的。
1 | DefaultTextStyle( |
上面中第一和第二个文本样式都是默认的,第三个按自己的样式显示。
字体
可以在Flutter应用程序中使用不同的字体。在Flutter中使用字体分两步完成。首先在pubspec.yaml
中声明它们,以确保它们会打包到应用程序中。然后通过TextStyle
属性使用字体。
在asset中声明
1 | flutter: |
使用字体
1 | // 声明文本样式 |
Package中的字体
要使用Package中定义的字体,必须提供package
参数
1 | const textStyle = const TextStyle( |
一个包也可以只提供字体文件而不需要在pubspec.yaml中声明。 这些文件应该存放在包的lib/
文件夹中。字体文件不会自动绑定到应用程序中,应用程序可以在声明字体时有选择地使用这些字体。假设一个名为my_package的包中有一个字体文件:
1 | lib/fonts/Raleway-Medium.ttf |
然后,应用程序可以声明一个字体,如下面的示例所示:
1 | flutter: |
lib/
是隐含的,所以它不应该包含在asset路径中。
在这种情况下,由于应用程序本地定义了字体,所以在创建TextStyle时可以不指定package
参数:
1 | const textStyle = const TextStyle( |
按钮
RaisedButton
RaisedButton
即"漂浮"按钮,它默认带有阴影和灰色背景。按下后,阴影会变大。
1 | RaisedButton( |
FlatButton
FlatButton
即扁平按钮,默认背景透明并不带阴影。按下后,会有背景色
1 | FlatButton( |
OutlineButton
OutlineButton
默认有一个边框,不带阴影且背景透明。按下后,边框颜色会变亮、同时出现背景和阴影(较弱)。
1 | OutlineButton( |
IconButton
IconButton
是一个可点击的Icon,不包括文字,默认没有背景,点击后会出现背景。
1 | IconButton( |
带图标的按钮
RaisedButton
、FlatButton
、OutlineButton
都有一个icon
构造函数,通过它可以轻松创建带图标的按钮。
1 | RaisedButton.icon( |
自定义按钮外观
按钮外观可以通过其属性来定义,不同按钮属性大同小异。
1 | const FlatButton({ |
Image及ICON
Image
Image
组件来加载并显示图片,Image
的数据源可以是asset、文件、内存以及网络。Image
widget有一个必选的image
参数,它对应一个ImageProvider。
ImageProvider
ImageProvider
是一个抽象类,主要定义了图片数据获取的接口load()
,从不同的数据源获取图片需要实现不同的ImageProvider
,如AssetImage
是实现了从Asset中加载图片的ImageProvider,而NetworkImage
实现了从网络加载图片的ImageProvider。
从asset中加载图片
- 在工程根目录下创建一个
images目录
,并将图片avatar.png拷贝到该目录。 - 在
pubspec.yaml
中的flutter
部分添加如下内容:
1 | assets: |
- 加载该图片
1 | Image( |
Image也提供了一个快捷的构造函数Image.asset
1 | Image.asset("images/avatar.png", |
从网络加载图片
1 | Image( |
Image也提供了一个快捷的构造函数Image.network
1 | Image.network( |
参数
1 | const Image({ |
width
、height
:用于设置图片的宽、高,当不指定宽高时,图片会根据当前父容器的限制,尽可能的显示其原始大小,如果只设置width
、height
的其中一个,那么另一个属性默认会按比例缩放,但可以通过下面介绍的fit
属性来指定适应规则。fit
:该属性用于在图片的显示空间和图片本身大小不同时指定图片的适应模式。适应模式是在BoxFit
中定义,它是一个枚举类型,有如下值:fill
:会拉伸填充满显示空间,图片本身长宽比会发生变化,图片会变形。cover
:会按图片的长宽比放大后居中填满显示空间,图片不会变形,超出显示空间部分会被剪裁。contain
:这是图片的默认适应规则,图片会在保证图片本身长宽比不变的情况下缩放以适应当前显示空间,图片不会变形。fitWidth
:图片的宽度会缩放到显示空间的宽度,高度会按比例缩放,然后居中显示,图片不会变形,超出显示空间部分会被剪裁。fitHeight
:图片的高度会缩放到显示空间的高度,宽度会按比例缩放,然后居中显示,图片不会变形,超出显示空间部分会被剪裁。none
:图片没有适应策略,会在显示空间内显示图片,如果图片比显示空间大,则显示空间只会显示图片中间部分。
Image缓存
Flutter框架对加载过的图片是有缓存的(内存),默认最大缓存数量是1000,最大缓存空间为100M
ICON
Flutter中,可以像Web开发一样使用iconfont,iconfont即“字体图标”,它是将图标做成字体文件,然后通过指定不同的字符而显示不同的图片。
有如下优势:
- 体积小:可以减小安装包大小。
- 矢量的:iconfont都是矢量图标,放大不会影响其清晰度。
- 可以应用文本样式:可以像文本一样改变字体图标的颜色、大小对齐等。
- 可以通过TextSpan和文本混用。
使用Material Design字体图标
Flutter默认包含了一套Material Design的字体图标,在pubspec.yaml
文件中的配置如下
1 | flutter: |
Material Design所有图标可以在其官网查看:https://material.io/tools/icons/
1 | String icons = ""; |
这样可以显示3个icon
使用自定义字体图标
-
导入字体图标文件;这一步和导入字体文件相同,假设我们的字体图标文件保存在项目根目录下,路径为"fonts/iconfont.ttf":
1
2
3
4fonts:
- family: myIcon #指定一个字体名
fonts:
- asset: fonts/iconfont.ttf -
为了使用方便,我们定义一个
MyIcons
类,功能和Icons
类一样:将字体文件中的所有图标都定义成静态变量:1
2
3
4
5
6
7
8
9
10
11
12
13
14class MyIcons{
// book 图标
static const IconData book = const IconData(
0xe614,
fontFamily: 'myIcon',
matchTextDirection: true
);
// 微信图标
static const IconData wechat = const IconData(
0xec7d,
fontFamily: 'myIcon',
matchTextDirection: true
);
}
单选开关和复选框
Material 组件库中提供了Material风格的单选开关Switch
和复选框Checkbox
。它们都是继承自StatefulWidget
,但它们本身不会保存当前选中状态,选中状态都是由父组件来管理的。当Switch
或Checkbox
被点击时,会触发它们的onChanged
回调,我们可以在此回调中处理选中状态改变逻辑
1 | Switch( |
输入框及表单
Material组件库中提供了输入框组件TextField
和表单组件Form
。
TextField
TextField
用于文本输入
重要属性:
controller
:编辑框的控制器,通过它可以设置/获取编辑框的内容、选择编辑内容、监听编辑文本改变事件。大多数情况下我们都需要显式提供一个controller
来与文本框交互。如果没有提供controller
,则TextField
内部会自动创建一个。focusNode
:用于控制TextField
是否占有当前键盘的输入焦点。它是我们和键盘交互的一个句柄(handle)。InputDecoration
:用于控制TextField
的外观显示,如提示文本、背景颜色、边框等。keyboardType
:用于设置该输入框默认的键盘输入类型。textInputAction
:键盘动作按钮图标(即回车键位图标),它是一个枚举值,有多个可选值style
:正在编辑的文本样式。textAlign
: 输入框内编辑文本在水平方向的对齐方式。autofocus
: 是否自动获取焦点。obscureText
:是否隐藏正在编辑的文本,如用于输入密码的场景等,文本内容会用“•”替换。maxLines
:输入框的最大行数,默认为1;如果为null
,则无行数限制。maxLength
和maxLengthEnforced
:maxLength
代表输入框文本的最大长度,设置后输入框右下角会显示输入的文本计数。maxLengthEnforced
决定当输入文本长度超过maxLength
时是否阻止输入,为true
时会阻止输入,为false
时不会阻止输入但输入框会变红。onChange
:输入框内容改变时的回调函数;注:内容改变事件也可以通过controller
来监听。onEditingComplete
和onSubmitted
:这两个回调都是在输入框输入完成时触发,比如按了键盘的完成键(对号图标)或搜索键(🔍图标)。不同的是两个回调签名不同,onSubmitted
回调是ValueChanged<String>
类型,它接收当前输入内容做为参数,而onEditingComplete
不接收参数。inputFormatters
:用于指定输入格式;当用户输入内容改变时,会根据指定的格式来校验。enable
:如果为false
,则输入框会被禁用,禁用状态不接收输入和事件,同时显示禁用态样式(在其decoration
中定义)。cursorWidth
、cursorRadius
和cursorColor
:这三个属性是用于自定义输入框光标宽度、圆角和颜色的。
1 | TextField( |
获取输入内容
获取输入内容有两种方式:
- 定义两个变量,用于保存用户名和密码,然后在
onChange
触发时,各自保存一下输入内容。 - 通过
controller
直接获取。
第一种方式比较简单,不在举例,我们来重点看一下第二种方式:
定义一个controller
:
1 | //定义一个controller |
然后设置输入框controller:
1 | TextField( |
通过controller获取输入框内容
1 | print(_unameController.text) |
监听文本变化
监听文本变化也有两种方式:
-
设置
onChange
回调,如:1
2
3
4
5
6TextField(
autofocus: true,
onChanged: (v) {
print("onChange: $v");
}
) -
通过
controller
监听,如:1
2
3
4
5
6
7
void initState() {
//监听输入改变
_unameController.addListener((){
print(_unameController.text);
});
}
控制焦点
焦点可以通过FocusNode
和FocusScopeNode
来控制,默认情况下,焦点由FocusScope
来管理,它代表焦点控制范围,可以在这个范围内可以通过FocusScopeNode
在输入框之间移动焦点、设置默认焦点等。我们可以通过FocusScope.of(context)
来获取Widget树中默认的FocusScopeNode
1 | FocusNode focusNode1 = new FocusNode(); |
监听焦点状态
1 | // focusNode绑定输入框 |
自定义样式
虽然我们可以通过decoration
属性来定义输入框样式
1 | TextField( |
表单Form
1 | Form({ |
autovalidate
:是否自动校验输入内容;当为true
时,每一个子FormField内容发生变化时都会自动校验合法性,并直接显示错误信息。否则,需要通过调用FormState.validate()
来手动校验。onWillPop
:决定Form
所在的路由是否可以直接返回(如点击返回按钮),该回调返回一个Future
对象,如果Future的最终结果是false
,则当前路由不会返回;如果为true
,则会返回到上一个路由。此属性通常用于拦截返回按钮。onChanged
:Form
的任意一个子FormField
内容发生变化时会触发此回调。
FormField
Form
的子孙元素必须是FormField
类型。
1 | const FormField({ |
为了方便使用,Flutter提供了一个TextFormField
组件,它继承自FormField
类。
FormState
FormState
为Form
的State
类,可以通过Form.of()
或GlobalKey
获得
其常用的三个方法:
FormState.validate()
:调用此方法后,会调用Form
子孙FormField的validate
回调,如果有一个校验失败,则返回false,所有校验失败项都会返回用户返回的错误提示。FormState.save()
:调用此方法后,会调用Form
子孙FormField
的save
回调,用于保存表单内容FormState.reset()
:调用此方法后,会将子孙FormField
的内容清空。
1 | child: Form( |
进度指示器Progress
Material 组件库中提供了两种进度指示器:LinearProgressIndicator
和CircularProgressIndicator
,它们都可以同时用于精确的进度指示和模糊的进度指示。
LinearProgressIndicator
LinearProgressIndicator是一个线性、条状的进度条
1 | LinearProgressIndicator({ |
value
:value
表示当前的进度,取值范围为[0,1];如果value
为null
时则指示器会执行一个循环动画(模糊进度);当value
不为null
时,指示器为一个具体进度的进度条。backgroundColor
:指示器的背景色。valueColor
: 指示器的进度条颜色;值得注意的是,该值类型是Animation<Color>
,这允许我们对进度条的颜色也可以指定动画。如果我们不需要对进度条颜色执行动画,换言之,我们想对进度条应用一种固定的颜色,此时我们可以通过AlwaysStoppedAnimation
来指定。
1 | // 模糊进度条(会执行一个动画) |
CircularProgressIndicator
CircularProgressIndicator
是一个圆形进度条,定义如下:
1 | CircularProgressIndicator({ |
前三个参数和LinearProgressIndicator
相同,不再赘述。strokeWidth
表示圆形进度条的粗细。示例如下:
1 | // 模糊进度条(会执行一个旋转动画) |
自定义尺寸
其实LinearProgressIndicator
和CircularProgressIndicator
都是取父容器的尺寸作为绘制的边界的。知道了这点,我们便可以通过尺寸限制类Widget,如ConstrainedBox
、SizedBox
1 | // 线性进度条高度指定为3 |
布局widgets
线性布局
Row
在水平方向创建灵活的布局。
1 | body: Row( |
textDirection
:表示水平方向子组件的布局顺序(是从左往右还是从右往左),默认为系统当前Locale环境的文本方向(如中文、英语都是从左往右,而阿拉伯语是从右往左)。mainAxisSize
:表示Row
在主轴(水平)方向占用的空间,默认是MainAxisSize.max
,表示尽可能多的占用水平方向的空间,此时无论子widgets实际占用多少水平空间,Row
的宽度始终等于水平方向的最大宽度;而MainAxisSize.min
表示尽可能少的占用水平空间,当子组件没有占满水平剩余空间,则Row
的实际宽度等于所有子组件占用的的水平空间;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
的参考系。verticalDirection
:表示Row
纵轴(垂直)的对齐方向,默认是VerticalDirection.down
,表示从上到下。crossAxisAlignment
:表示子组件在纵轴方向的对齐方式,Row
的高度等于子组件中最高的子元素高度,它的取值和MainAxisAlignment
一样(包含start
、end
、center
三个值),不同的是crossAxisAlignment
的参考系是verticalDirection
,即verticalDirection
值为VerticalDirection.down
时crossAxisAlignment.start
指顶部对齐,verticalDirection
值为VerticalDirection.up
时,crossAxisAlignment.start
指底部对齐;而crossAxisAlignment.end
和crossAxisAlignment.start
正好相反;children
:子组件数组。
Column
在垂直方向创建灵活的布局。
1 | body: Column( |
如果Row
里面嵌套Row
,或者Column
里面再嵌套Column
,那么只有最外面的Row
或Column
会占用尽可能大的空间,里面Row
或Column
所占用的空间为实际大小
对齐 widgets
你可以使用 mainAxisAlignment
和 crossAxisAlignment
属性控制行或列如何对齐其子项。
MainAxisAlignment:主轴方向上的对齐方式,会对child的位置起作用,默认是start。
其中MainAxisAlignment枚举值:
- center:将children放置在主轴的中心;
- end:将children放置在主轴的末尾;
- spaceAround:将主轴方向上的空白区域均分,使得children之间的空白区域相等,但是首尾child的空白区域为1/2;
- spaceBetween:将主轴方向上的空白区域均分,使得children之间的空白区域相等,首尾child都靠近首尾,没有间隙;
- spaceEvenly:将主轴方向上的空白区域均分,使得children之间的空白区域相等,包括首尾child;
- start:将children放置在主轴的起点;
CrossAxisAlignment:children在交叉轴方向的对齐方式
CrossAxisAlignment枚举值有如下几种:
- baseline:在交叉轴方向,使得children的baseline对齐;
- center:children在交叉轴上居中展示;
- end:children在交叉轴上末尾展示;
- start:children在交叉轴上起点处展示;
- stretch:让children填满交叉轴方向;
1 | body: Column( |
MainAxisSize
在主轴方向占有空间的值,默认是max。
- max:根据传入的布局约束条件,最大化主轴方向的可用空间;
- min:与max相反,是最小化主轴方向的可用空间;
弹性布局
Flex
Flex组件可以沿着水平或垂直方向排列子组件,如果你知道主轴方向,使用
Row或
Column会方便一些,因为
Row和
Column都继承自
Flex,参数基本相同,所以能使用Flex的地方基本上都可以使用
Row或
Column。
1 | Flex({ |
Expanded
可以按比例“扩伸” Row
、Column
和Flex
子组件所占用的空间。当某个布局太大而超出屏幕时,受影响的边缘会出现黄色和黑色条纹图案。通过使用 Expanded
widget,可以调整 widgets 的大小以适合行或列。
1 | body: Row( |
如果不加,文本不会换行,且右边会出现黄色和黑色条纹图案,加了这几个text宽度会平分屏幕,同时自动换行
同时可以给 Expanded 设置宽度系数 flex: 2,那个该Expanded就会是其他的两倍。
Spacer
的功能是占用指定比例的空间,实际上它只是Expanded
的一个包装类。
流式布局
Flutter中通过Wrap
和Flow
来支持流式布局,比如超出屏幕的Row换成Wrap后溢出部分则会自动折行。
Wrap
1 | Wrap({ |
看一下Wrap
特有的几个属性:
spacing
:主轴方向子widget的间距runSpacing
:纵轴方向的间距runAlignment
:纵轴方向的对齐方式
1 | Wrap( |
放不下会自动换行
Flow
我们一般很少会使用Flow
,因为其过于复杂,需要自己实现子widget的位置转换,在很多场景下首先要考虑的是Wrap
是否满足需求。Flow
主要用于一些需要自定义布局策略或性能要求较高(如动画中)的场景。Flow
有如下优点:
- 性能好;
Flow
是一个对子组件尺寸以及位置调整非常高效的控件,Flow
用转换矩阵在对子组件进行位置调整的时候进行了优化:在Flow
定位过后,如果子组件的尺寸或者位置发生了变化,在FlowDelegate
中的paintChildren()
方法中调用context.paintChild
进行重绘,而context.paintChild
在重绘时使用了转换矩阵,并没有实际调整组件位置。 - 灵活;由于我们需要自己实现
FlowDelegate
的paintChildren()
方法,所以我们需要自己计算每一个组件的位置,因此,可以自定义布局策略。
缺点:
- 使用复杂。
- 不能自适应子组件大小,必须通过指定父容器大小或实现
TestFlowDelegate
的getSize
返回固定大小。
层叠布局
Stack
Stack
允许子组件堆叠
1 | Stack({ |
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
的大小。overflow
:此属性决定如何显示超出Stack
显示空间的子组件;值为Overflow.clip
时,超出部分会被剪裁(隐藏),值为Overflow.visible
时则不会。
Positioned
Positioned
用于根据Stack
的四个角来确定子组件的位置。
1 | const Positioned({ |
left
、top
、right
、 bottom
分别代表离Stack
左、上、右、底四边的距离。width
和height
用于指定需要定位元素的宽度和高度。注意,Positioned
的width
、height
和其它地方的意义稍微有点区别,此处用于配合left
、top
、right
、 bottom
来定位组件,举个例子,在水平方向时,你只能指定left
、right
、width
三个属性中的两个,如指定left
和width
后,right
会自动算出(left
+width
),如果同时指定三个属性则会报错,垂直方向同理。
1 | children: <Widget>[ |
对齐和定位
Align
Align
组件可以调整子组件的位置,并且可以根据子组件的宽高来确定自身的的宽高
1 | Align({ |
alignment
: 需要一个AlignmentGeometry
类型的值,表示子组件在父组件中的起始位置。AlignmentGeometry
是一个抽象类,它有两个常用的子类:Alignment
和FractionalOffset
,我们将在下面的示例中详细介绍。widthFactor
和heightFactor
是用于确定Align
组件本身宽高的属性;它们是两个缩放因子,会分别乘以子元素的宽、高,最终的结果就是Align
组件的宽高。如果值为null
,则组件的宽高将会占用尽可能多的空间。
1 | child: Align( |
FlutterLogo
是Flutter SDK提供的一个组件,内容就是Flutter的商标。
1 | Align( |
过同时指定widthFactor
和heightFactor
为2也是可以达到同样的效果。
Align
和Stack
/Positioned
都可以用于指定子元素相对于父元素的偏移,但它们还是有两个主要区别:
- 定位参考系统不同;
Stack
/Positioned
定位的的参考系可以是父容器矩形的四个顶点;而Align
则需要先通过alignment
参数来确定坐标原点,不同的alignment
会对应不同原点,最终的偏移是需要通过alignment
的转换公式来计算出。 Stack
可以有多个子元素,并且子元素可以堆叠,而Align
只能有一个子元素,不存在堆叠
Alignment
Alignment有两个属性x
、y
,分别表示在水平和垂直方向的偏移。Alignment
Widget会以矩形的中心点作为坐标原点,即Alignment(0.0, 0.0)
。x
、y
的值从-1到1分别代表矩形左边到右边的距离和顶部到底边的距离
Alignment
可以通过其坐标转换公式将其坐标转为子元素的具体偏移坐标:
1 | (Alignment.x*childWidth/2+childWidth/2, Alignment.y*childHeight/2+childHeight/2) |
FractionalOffset
FractionalOffset
继承自 Alignment
,它和 Alignment
唯一的区别就是坐标原点不同!FractionalOffset
的坐标原点为矩形的左侧顶。
FractionalOffset
的坐标转换公式为:
1 | 实际偏移 = (FractionalOffse.x * childWidth, FractionalOffse.y * childHeight) |
Center
居中摆放组件
1 | body: Center( |
容器类组件
Padding
Padding
Padding
可以给其子节点添加填充(留白),和边距效果类似。
1 | Padding({ |
EdgeInsetsGeometry
是一个抽象类,开发中,我们一般都使用EdgeInsets
类,
EdgeInsets
我们看看EdgeInsets
提供的便捷方法:
fromLTRB(double left, double top, double right, double bottom)
:分别指定四个方向的填充。all(double value)
: 所有方向均使用相同数值的填充。only({left, top, right ,bottom })
:可以设置具体某个方向的填充(可以同时指定多个方向)。symmetric({ vertical, horizontal })
:用于设置对称方向的填充,vertical
指top
和bottom
,horizontal
指left
和right
。
尺寸限制类容器
尺寸限制类容器用于限制容器大小,Flutter中提供了多种这样的容器,如ConstrainedBox
、SizedBox
、UnconstrainedBox
、AspectRatio
等
ConstrainedBox
ConstrainedBox
用于对子组件添加额外的约束
1 | Widget redBox=DecoratedBox( |
1 | ConstrainedBox( |
最终宽度为50像素。
BoxConstraints
BoxConstraints用于设置限制条件,它的定义如下:
1 | const BoxConstraints({ |
BoxConstraints还定义了一些便捷的构造函数,用于快速生成特定限制规则的BoxConstraints,如BoxConstraints.tight(Size size)
,它可以生成给定大小的限制;const BoxConstraints.expand()
可以生成一个尽可能大的用以填充另一个容器的BoxConstraints。
SizedBox
SizedBox
用于给子元素指定固定的宽高
1 | SizedBox( |
实际上SizedBox
只是ConstrainedBox
的一个定制
多重限制
如果某一个组件有多个父级ConstrainedBox
限制,对于minWidth
和minHeight
来说,是取父子中相应数值较大的,对于max,则取最小的,即两者子集。
UnconstrainedBox
UnconstrainedBox
不会对子组件产生任何限制,它允许其子组件按照其本身大小绘制。一般情况下,我们会很少直接使用此组件,但在"去除"多重限制的时候也许会有帮助
其它限制类容器
除了上面介绍的这些常用的尺寸限制类容器外,还有一些其他的尺寸限制类容器,比如AspectRatio
,它可以指定子组件的长宽比、LimitedBox
用于指定最大宽高、FractionallySizedBox
可以根据父容器宽高的百分比来设置子组件宽高等。
装饰容器
DecoratedBox
DecoratedBox
可以在其子组件绘制前(或后)绘制一些装饰(Decoration),如背景、边框、渐变等。
1 | const DecoratedBox({ |
-
decoration
:代表将要绘制的装饰,它的类型为Decoration
。Decoration
是一个抽象类,它定义了一个接口createBoxPainter()
,子类的主要职责是需要通过实现它来创建一个画笔,该画笔用于绘制装饰。 -
position
:此属性决定在哪里绘制Decoration
,它接收DecorationPosition
的枚举类型,该枚举类有两个值:background
:在子组件之后绘制,即背景装饰。foreground
:在子组件之上绘制,即前景。
BoxDecoration
我们通常会直接使用BoxDecoration
类,它是一个Decoration的子类,实现了常用的装饰元素的绘制。
1 | BoxDecoration({ |
1 | DecoratedBox( |
变换Transform
Transform
可以在其子组件绘制时对其应用一些矩阵变换来实现一些特效
1 | Container( |
平移
Transform.translate
接收一个offset
参数,可以在绘制时沿x
、y
轴对子组件平移指定的距离。
1 | DecoratedBox( |
效果是文本向左上有一些移动,背景在原地
旋转
Transform.rotate
可以对子组件进行旋转变换
1 | DecoratedBox( |
1 | import 'dart:math' as math; |
缩放
Transform.scale
可以对子组件进行缩小或放大,如:
1 | DecoratedBox( |
Transform
的变换是应用在绘制阶段,而并不是应用在布局(layout)阶段,所以无论对子组件应用何种变化,其占用空间的大小和在屏幕上的位置都是固定不变的,因为这些是在布局阶段就确定的。因此,变换之后本来不会重合的组件也有可能会叠在一起。
RotatedBox
RotatedBox
和Transform.rotate
功能相似,它们都可以对子组件进行旋转变换,但是有一点不同:RotatedBox
的变换是在layout阶段,会影响在子组件的位置和大小。
1 | Row( |
Container
Container
是一个组合类容器,它本身不对应具体的RenderObject
,它是DecoratedBox
、ConstrainedBox、Transform
、Padding
、Align
等组件组合的一个多功能容器,所以我们只需通过一个Container
组件可以实现同时需要装饰、变换、限制的场景。
1 | Container({ |
Container
的大多数属性在介绍其它容器时都已经介绍过了,不再赘述,但有两点需要说明:
- 容器的大小可以通过
width
、height
属性来指定,也可以通过constraints
来指定;如果它们同时存在时,width
、height
优先。实际上Container内部会根据width
、height
来生成一个constraints
。 color
和decoration
是互斥的,如果同时设置它们则会报错!实际上,当指定color
时,Container
内会自动创建一个decoration
。
1 | Container( |
Padding和Margin
1 | Container( |
Scaffold、TabBar、底部导航
Scaffold
一个完整的路由页可能会包含导航栏、抽屉菜单(Drawer)以及底部Tab导航菜单等。如果每个路由页面都需要开发者自己手动去实现这些,这会是一件非常麻烦且无聊的事。幸运的是,Flutter Material组件库提供了一些现成的组件来减少我们的开发任务。Scaffold
是一个路由页的骨架,我们使用它可以很容易地拼装出一个完整的页面。
AppBar | 一个导航栏骨架 |
---|---|
MyDrawer | 抽屉菜单 |
BottomNavigationBar | 底部导航栏 |
FloatingActionButton | 漂浮按钮 |
AppBar
AppBar
是一个Material风格的导航栏,通过它可以设置导航栏标题、导航栏菜单、导航栏底部的Tab标题等。下面我们看看AppBar的定义:
1 | AppBar({ |
如果给Scaffold
添加了抽屉菜单,默认情况下Scaffold
会自动将AppBar
的leading
设置为菜单按钮(如上面截图所示),点击它便可打开抽屉菜单。如果我们想自定义菜单图标,可以手动来设置leading
,如:
1 | Scaffold( |
TabBar
Material组件库中提供了一个TabBar
组件,它可以快速生成Tab
菜单
1 | TabController _tabController; //需要定义一个Controller |
Tab
组件有三个可选参数,除了可以指定文字外,还可以指定Tab菜单图标,或者直接自定义组件样式。Tab
组件定义如下:
1 | Tab({ |
TabBarView
通过TabController
去监听Tab菜单的切换去切换Tab页
1 | _tabController.addListener((){ |
TabBarView
组件,通过它不仅可以轻松的实现Tab页,而且可以非常容易的配合TabBar来实现同步切换和滑动状态同步
1 | Scaffold( |
Drawer
Scaffold
的drawer
和endDrawer
属性可以分别接受一个Widget来作为页面的左、右抽屉菜单
举例:
1 | class MyDrawer extends StatelessWidget { |
FloatingActionButton
FloatingActionButton
是Material设计规范中的一种特殊Button,通常悬浮在页面的某一个位置作为某种常用动作的快捷入口,我们可以通过Scaffold
的floatingActionButton
属性来设置一个FloatingActionButton
,同时通过floatingActionButtonLocation
属性来指定其在页面中悬浮的位置。
底部Tab导航栏
我们可以通过Scaffold
的bottomNavigationBar
属性来设置底部导航,过Material组件库提供的BottomNavigationBar
和BottomNavigationBarItem
两种组件来实现Material风格的底部导航栏。
1 | bottomNavigationBar: BottomAppBar( |
上面代码中没有控制打洞位置的属性,实际上,打洞的位置取决于FloatingActionButton
的位置,上面FloatingActionButton
的位置为:
1 | floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, |
BottomAppBar
的shape
属性决定洞的外形,CircularNotchedRectangle
实现了一个圆形的外形,我们也可以自定义外形
剪裁(Clip)
Flutter中提供了一些剪裁函数,用于对组件进行剪裁。
剪裁Widget | 作用 |
---|---|
ClipOval | 子组件为正方形时剪裁为内贴圆形,为矩形时,剪裁为内贴椭圆 |
ClipRRect | 将子组件剪裁为圆角矩形 |
ClipRect | 剪裁子组件到实际占用的矩形大小(溢出部分剪裁) |
1 | ClipRect(//将溢出部分剪裁 |
1 | Widget avatar = Image.asset("imgs/avatar.png", width: 60.0); |
CustomClipper
我们可以使用CustomClipper
来自定义剪裁区域
首先,自定义一个CustomClipper
:
1 | class MyClipper extends CustomClipper<Rect> { |
getClip()
是用于获取剪裁区域的接口,由于图片大小是60×60,我们返回剪裁区域为Rect.fromLTWH(10.0, 15.0, 40.0, 30.0)
,即图片中部40×30像素的范围。shouldReclip()
接口决定是否重新剪裁。如果在应用中,剪裁区域始终不会发生变化时应该返回false
,这样就不会触发重新剪裁,避免不必要的性能开销。如果剪裁区域会发生变化(比如在对剪裁区域执行一个动画),那么变化后应该返回true
来重新执行剪裁。
然后,我们通过ClipRect
来执行剪裁,为了看清图片实际所占用的位置,我们设置一个红色背景:
1 | DecoratedBox( |
可以看到我们的剪裁成功了,但是图片所占用的空间大小仍然是60×60(红色区域),这是因为剪裁是在layout完成后的绘制阶段进行的,所以不会影响组件的大小,这和Transform
原理是相似的。
Card
Card只有一个child,可以放置任何widget,在 Flutter 中,Card
有轻微的圆角和阴影来使它具有 3D 效果。改变 Card
的 elevation
属性可以控制阴影效果。
1 | body: Card( |
可滚动组件
滚动组件都直接或间接包含一个Scrollable
组件,因此它们包括一些共同的属性:
1 | Scrollable({ |
-
axisDirection
滚动方向。 -
physics
:此属性接受一个ScrollPhysics:类型的对象,它决定可滚动组件如何响应用户操作,比如用户滑动完抬起手指后,继续执行动画;或者滑动到边界时,如何显示。默认情况下,Flutter会根据具体平台分别使用不同的
ScrollPhysics对象,应用不同的显示效果,如当滑动到边界时,继续拖动的话,在iOS上会出现弹性效果,而在Android上会出现微光效果。如果你想在所有平台下使用同一种效果,可以显式指定一个固定的
ScrollPhysics:lutter SDK中包含了两个ScrollPhysics的子类,他们可以直接使用:
ClampingScrollPhysics
:Android下微光效果。BouncingScrollPhysics
:iOS下弹性效果。
-
controller
:此属性接受一个ScrollController
对象。ScrollController
的主要作用是控制滚动位置和监听滚动事件。默认情况下,Widget树中会有一个默认的PrimaryScrollController
,如果子树中的可滚动组件没有显式的指定controller
,并且primary
属性值为true
时(默认就为true
),可滚动组件会使用这个默认的PrimaryScrollController
。这种机制带来的好处是父组件可以控制子树中可滚动组件的滚动行为,例如,Scaffold
正是使用这种机制在iOS中实现了点击导航栏回到顶部的功能。
Scrollbar
Scrollbar
是一个Material风格的滚动指示器(滚动条),如果要给可滚动组件添加滚动条,只需将Scrollbar
作为可滚动组件的任意一个父级组件即可
1 | Scrollbar( |
ViewPort
ViewPort是指一个Widget的实际显示区域。
基于Sliver的延迟构建
通常可滚动组件的子组件可能会非常多、占用的总高度也会非常大;如果要一次性将子组件全部构建出将会非常昂贵!为此,Flutter中提出一个Sliver(中文为“薄片”的意思)概念,如果一个可滚动组件支持Sliver模型,那么该滚动可以将子组件分成好多个“薄片”(Sliver),只有当Sliver出现在视口中时才会去构建它,这种模型也称为“基于Sliver的延迟构建模型”。可滚动组件中有很多都支持基于Sliver的延迟构建模型,如ListView
、GridView
,但是也有不支持该模型的,如SingleChildScrollView
。
SingleChildScrollView
SingleChildScrollView
类似于Android中的ScrollView
,它只能接收一个子组件
1 | SingleChildScrollView({ |
除了上一节我们介绍过的可滚动组件的通用属性外,我们重点看一下reverse
和primary
两个属性:
reverse
:该属性API文档解释是:是否按照阅读方向相反的方向滑动,如:scrollDirection
值为Axis.horizontal
,如果阅读方向是从左到右(取决于语言环境,阿拉伯语就是从右到左)。reverse
为true
时,那么滑动方向就是从右往左。其实此属性本质上是决定可滚动组件的初始滚动位置是在“头”还是“尾”,取false
时,初始滚动位置在“头”,反之则在“尾”,读者可以自己试验。primary
:指是否使用widget树中默认的PrimaryScrollController
;当滑动方向为垂直方向(scrollDirection
值为Axis.vertical
)并且没有指定controller
时,primary
默认为true
.
1 | Widget build(BuildContext context) { |
ListView
ListView
是最常用的可滚动组件之一,它可以沿一个方向线性排布所有子组件,并且它也支持基于Sliver的延迟构建模型。
1 | ListView({ |
上面参数分为两组:第一组是可滚动组件的公共参数,本章第一节中已经介绍过,不再赘述;第二组是ListView
各个构造函数(ListView
有多个构造函数)的共同参数,我们重点来看看这些参数,:
itemExtent
:该参数如果不为null
,则会强制children
的“长度”为itemExtent
的值;这里的“长度”是指滚动方向上子组件的长度,也就是说如果滚动方向是垂直方向,则itemExtent
代表子组件的高度;如果滚动方向为水平方向,则itemExtent
就代表子组件的宽度。在ListView
中,指定itemExtent
比让子组件自己决定自身长度会更高效,这是因为指定itemExtent
后,滚动系统可以提前知道列表的长度,而无需每次构建子组件时都去再计算一下,尤其是在滚动位置频繁变化时(滚动系统需要频繁去计算列表高度)。shrinkWrap
:该属性表示是否根据子组件的总长度来设置ListView
的长度,默认值为false
。默认情况下,ListView
的会在滚动方向尽可能多的占用空间。当ListView
在一个无边界(滚动方向上)的容器中时,shrinkWrap
必须为true
。addAutomaticKeepAlives
:该属性表示是否将列表项(子组件)包裹在AutomaticKeepAlive
组件中;典型地,在一个懒加载列表中,如果将列表项包裹在AutomaticKeepAlive
中,在该列表项滑出视口时它也不会被GC(垃圾回收),它会使用KeepAliveNotification
来保存其状态。如果列表项自己维护其KeepAlive
状态,那么此参数必须置为false
。addRepaintBoundaries
:该属性表示是否将列表项(子组件)包裹在RepaintBoundary
组件中。当可滚动组件滚动时,将列表项包裹在RepaintBoundary
中可以避免列表项重绘,但是当列表项重绘的开销非常小(如一个颜色块,或者一个较短的文本)时,不添加RepaintBoundary
反而会更高效。和addAutomaticKeepAlive
一样,如果列表项自己维护其KeepAlive
状态,那么此参数必须置为false
。
默认构造函数
默认构造函数有一个children
参数,它接受一个Widget列表(List)。这种方式适合只有少量的子组件的情况,因为这种方式需要将所有children
都提前创建好。
1 | ListView( |
ListView.builder
ListView.builder
适合列表项比较多(或者无限)的情况,因为只有当子组件真正显示的时候才会被创建,也就说通过该构造函数创建的ListView
是支持基于Sliver的懒加载模型的。
1 | ListView.builder({ |
itemBuilder
:它是列表项的构建器,类型为IndexedWidgetBuilder
,返回值为一个widget。当列表滚动到具体的index
位置时,会调用该构建器构建列表项。itemCount
:列表项的数量,如果为null
,则为无限列表。
1 | ListView.builder( |
ListView.separated
ListView.separated
可以在生成的列表项之间添加一个分割组件,它比ListView.builder
多了一个separatorBuilder
参数,该参数是一个分割组件生成器。
1 | Widget build(BuildContext context) { |
添加固定列表头
1 |
|
GridView
GridView
可以构建一个二维网格列表
1 | GridView({ |
SliverGridDelegateWithFixedCrossAxisCount
该子类实现了一个横轴为固定数量子元素的layout算法,其构造函数为:
1 | SliverGridDelegateWithFixedCrossAxisCount({ |
crossAxisCount
:横轴子元素的数量。此属性值确定后子元素在横轴的长度就确定了,即ViewPort横轴长度除以crossAxisCount
的商。mainAxisSpacing
:主轴方向的间距。crossAxisSpacing
:横轴方向子元素的间距。childAspectRatio
:子元素在横轴长度和主轴长度的比例。由于crossAxisCount
指定后,子元素横轴长度就确定了,然后通过此参数值就可以确定子元素在主轴的长度。
1 | GridView( |
GridView.count
GridView.count
构造函数内部使用了SliverGridDelegateWithFixedCrossAxisCount
,我们通过它可以快速的创建横轴固定数量子元素的`GridView
1 | GridView.count( |
SliverGridDelegateWithMaxCrossAxisExtent
该子类实现了一个横轴子元素为固定最大长度的layout算法
1 | SliverGridDelegateWithMaxCrossAxisExtent({ |
maxCrossAxisExtent
为子元素在横轴上的最大长度,之所以是“最大”长度,是因为横轴方向每个子元素的长度仍然是等分的
1 | GridView( |
GridView.extent
GridView.extent构造函数内部使用了SliverGridDelegateWithMaxCrossAxisExtent,我们通过它可以快速的创建纵轴子元素为固定最大长度的的GridView
1 | GridView.extent( |
GridView.builder
可以通过GridView.builder
来动态创建子widget
1 | GridView.builder( |
其中itemBuilder
为子widget构建器。
1 | Widget build(BuildContext context) { |
List和GridView不能放在Row和Colum中,因为无法测出尺寸,可以写死尺寸或者套一个Expanded。
Pub上有一个包“flutter_staggered_grid_view” ,它实现了一个交错GridView的布局模型,可以很轻松的实现item宽高不相等的GridView。
CustomScrollView
CustomScrollView
是可以使用Sliver来自定义滚动模型(效果)的组件,比如顶部需要一个GridView
,底部需要一个ListView
,而要求整个页面的滑动效果是统一的。
为了能让可滚动组件能和CustomScrollView
配合使用,Flutter提供了一些可滚动组件的Sliver版,如SliverList、SliverGrid等
1 | import 'package:flutter/material.dart'; |
滚动监听及控制
可以用ScrollController
来控制可滚动组件的滚动位置。
ScrollController
1 | ScrollController({ |
常用的属性和方法:
offset
:可滚动组件当前的滚动位置。jumpTo(double offset)
、animateTo(double offset,...)
:这两个方法用于跳转到指定的位置,它们不同之处在于,后者在跳转时会执行一个动画,而前者不会。
滚动监听
1 | controller.addListener(()=>print(controller.offset)) |
滚动位置恢复
PageStorage
是一个用于保存页面(路由)相关数据的组件,子树中的Widget可以通过指定不同的PageStorageKey
来存储各自的数据或状态。
每次滚动结束,可滚动组件都会将滚动位置offset
存储到PageStorage
中,当可滚动组件重新创建时再恢复。如果ScrollController.keepScrollOffset
为false
,则滚动位置将不会被存储,可滚动组件重新创建时会使用ScrollController.initialScrollOffset
;ScrollController.keepScrollOffset
为true
时,可滚动组件在第一次创建时,会滚动到initialScrollOffset
处,因为这时还没有存储过滚动位置。在接下来的滚动中就会存储、恢复滚动位置,而initialScrollOffset
会被忽略。
当一个路由中包含多个可滚动组件时,如果你发现在进行一些跳转或切换操作后,滚动位置不能正确恢复,这时你可以通过显式指定PageStorageKey
来分别跟踪不同的可滚动组件的位置,如:
1 | ListView(key: PageStorageKey(1), ... ); |
ScrollPosition
ScrollPosition是用来保存可滚动组件的滚动位置的。一个ScrollController
对象可以同时被多个可滚动组件使用,ScrollController
会为每一个可滚动组件创建一个ScrollPosition
对象,这些ScrollPosition
保存在ScrollController
的positions
属性中(List<ScrollPosition>
)。ScrollPosition
是真正保存滑动位置信息的对象,offset
只是一个便捷属性:
1 | double get offset => position.pixels; |
一个ScrollController
虽然可以对应多个可滚动组件,但是有一些操作,如读取滚动位置offset
,则需要一对一!但是我们仍然可以在一对多的情况下,通过其它方法读取滚动位置,举个例子,假设一个ScrollController
同时被两个可滚动组件使用,那么我们可以通过如下方式分别读取他们的滚动位置:
1 | ... |
我们可以通过controller.positions.length
来确定controller
被几个可滚动组件使用。
ScrollPosition
有两个常用方法:animateTo()
和 jumpTo()
,它们是真正来控制跳转滚动位置的方法,ScrollController
的这两个同名方法,内部最终都会调用ScrollPosition
的。
ScrollController控制原理
ScrollController
的另外三个方法:
1 | ScrollPosition createScrollPosition( |
当ScrollController
和可滚动组件关联时,可滚动组件首先会调用ScrollController
的createScrollPosition()
方法来创建一个ScrollPosition
来存储滚动位置信息,接着,可滚动组件会调用attach()
方法,将创建的ScrollPosition
添加到ScrollController
的positions
属性中,这一步称为“注册位置”,只有注册后animateTo()
和 jumpTo()
才可以被调用。
当可滚动组件销毁时,会调用ScrollController
的detach()
方法,将其ScrollPosition
对象从ScrollController
的positions
属性中移除,这一步称为“注销位置”,注销后animateTo()
和 jumpTo()
将不能再被调用。
需要注意的是,ScrollController
的animateTo()
和 jumpTo()
内部会调用所有ScrollPosition
的animateTo()
和 jumpTo()
,以实现所有和该ScrollController
关联的可滚动组件都滚动到指定的位置。
滚动监听
Flutter Widget树中子Widget可以通过发送通知(Notification)与父(包括祖先)Widget通信。父级组件可以通过NotificationListener
组件来监听自己关注的通知。
可滚动组件在滚动时会发送ScrollNotification
类型的通知,ScrollBar
正是通过监听滚动通知来实现的。通过NotificationListener
监听滚动事件和通过ScrollController
有两个主要的不同:
- 通过NotificationListener可以在从可滚动组件到widget树根之间任意位置都能监听。而
ScrollController
只能和具体的可滚动组件关联后才可以。 - 收到滚动事件后获得的信息不同;
NotificationListener
在收到滚动事件时,通知中会携带当前滚动位置和ViewPort的一些信息,而ScrollController
只能获取当前滚动位置。
1 |
|
在接收到滚动事件时,参数类型为ScrollNotification
,它包括一个metrics
属性,它的类型是ScrollMetrics
,该属性包含当前ViewPort及滚动位置等信息:
pixels
:当前滚动位置。maxScrollExtent
:最大可滚动长度。extentBefore
:滑出ViewPort顶部的长度;此示例中相当于顶部滑出屏幕上方的列表长度。extentInside
:ViewPort内部长度;此示例中屏幕显示的列表部分的长度。extentAfter
:列表中未滑入ViewPort部分的长度;此示例中列表底部未显示到屏幕范围部分的长度。atEdge
:是否滑到了可滚动组件的边界(此示例中相当于列表顶或底部)。