UI 功能控件
SafeArea
用于在屏幕安全区中显示布局。当我们没有使用Scaffold或未设置AppBar时,页面的布局会伸展到系统状态栏下,如果我们不需要这种沉浸式状态栏效果,那么就可以使用SafeArea跳过状态栏区域(包括底部导航栏)。
显示与隐藏
Offstage具有简单的隐藏功能,属性为true时表示隐藏,且不占用空间Visibility比Offstage具有更多功能,visible属性为false时表示隐藏Opacity该控件提供透明度的设置能力,当完全透明时,亦可实现隐藏控件的效果
Visibility 属性
| 属性 | 类型 | 简述 |
|---|---|---|
| replacement | Widget |
不可见时显示的控件,仅当maintainState为false时有效 |
| visible | bool |
子控件是否可见 |
| maintainState | bool |
不可见时是否维持状态 |
| maintainAnimation | bool |
不可见时是否维持子控件动画 |
| maintainSize | bool |
不可见时是否保留空间 |
| maintainInteractivity | bool |
不可见时是否保留交互性 |
裁剪
ClipOval子控件为正方形时剪裁为内切圆,若为矩形时,剪裁为内切椭圆ClipRRect将子控件剪裁为圆角矩形ClipRect剪裁溢出部分ClipPath路径裁剪,可配合CustomClipper实现各种不规则效果
除此外,还有一个控件CircleAvatar也具有类似的功能,但这是一个视图控件,而不是功能控件,用于头像显示。
变换 Transform
Transform可以对子控件做一系列变换操作。需要注意的是,它的变换是在绘制阶段进行的,而不是布局(layout)阶段,因此无论对子控件应用何种变换,其占用空间的大小和在屏幕上的位置都是在一开始确定的,不会变化的。
常用变换
- 平移
- 旋转
- 缩放
- 斜切
Transform控件通常有两种使用方式,一种使用默认构造方法,另一种则使用命名构造方法。默认构造方法更强大灵活,命名构造方法则更简单。
命名构造方法如下
Transform.translateTransform.scaleTaransform.rotate
Container(color: Colors.red,
child: Transform.translate(
// x正方向、y正方向各平移20个单位
offset: Offset(20.0, 20.0),
child: Text("hello world"),
)),
Center(
child: Container(color: Colors.red,
child: Transform.rotate(
// 围绕Z轴顺时针旋转90度
angle:math.pi/2,
child: Text("hello world"),
)),
)
使用默认构造方法时,transform属性是必传,此时需要使用 Matrix4 类作为 4D 矩阵
/// 需注意导包
import 'package:vector_math/vector_math_64.dart' as v;
Container(color: Colors.red,
child: Transform(
// x正方向、y正方向各平移20个单位,此时设置z轴无效
transform: Matrix4.translation(v.Vector3(20,20,0)),
child: Text("hello world"),
))
Matrix4 的常用构造方法
scale缩放transform平移rotationZ绕z轴旋转rotationX绕x轴旋转rotationY绕y轴旋转skewX沿x轴方向斜切skewY沿y轴方向斜切skew沿x、y轴共同矩阵斜切
直接使用Matrix4 的命名构造方法还是有些繁琐,还涉及到导入一些数学库,因此真正推荐的写法是使用identity构造方法来初始化一个Matrix4对象,然后调用对应的功能方法,示例如下
Container(color: Colors.red,
child: Transform(
// x正方向、y正方向各平移20个单位,此时设置z轴无效
transform: Matrix4.identity()
..translate(20.0, 20.0, 0.0),
child: Text("hello world"),
))
通过这种链式调用,在后面连续调用其他变换方法,可同时组合多种变换。
需要注意,斜切变换只能使用命名构造方法实现
Center(
child: Container(color: Colors.red,
child: Transform(
// 逆时针方向,从x轴扭曲旋转30度
transform: Matrix4.skewY(-math.pi/6),
child: Text("hello world"),
)),
)
注意,除了直接使用Transform控件,还可以通过设置Container的transform属性来实现同样的变换功能,其用法与Transform相同。
MediaQuery
MediaQuery主要用于查询媒体相关的数据,使用MediaQuery.of(context)可返回一个MediaQueryData类型的数据,通常不会直接将MediaQuery作为一个控件使用,但它也可以作为Widget控件树中的控件使用。
MediaQueryData 的属性
| 属性名 | 类型 | 简述 |
|---|---|---|
| size | Size |
获取屏幕宽、高。单位为逻辑像素,非物理像素。物理像素 = size*devicePixelRatio |
| devicePixelRatio | double |
设备像素比(密度)。单位逻辑像素对应的物理像素数量 |
| textScaleFactor | double |
单位逻辑像素的字体像素数,若设为1.5,则放大50% |
| platformBrightness | Brightness |
平台当前亮度模式(iOS夜间模式、安卓9以上支持) |
| viewInsets | EdgeInsets |
被系统遮挡的部分,通常指键盘。viewInsets.bottom表示键盘的高度 |
| padding | EdgeInsets |
被系统遮挡的部分,此处指“刘海屏”和安卓底部导航栏高度 |
| viewPadding | EdgeInsets |
被系统遮挡的部分,独立于padding和viewInsets,通常是全屏 |
| systemGestureInsets | EdgeInsets |
沿着屏幕边缘的区域,系统在这里消耗某些输入事件,并阻止将这些事件传递给APP。APP应避免将手势检测器定位在系统手势识别的区域内 |
| physicalDepth | double |
设备的最大深度(主要在Fuchsia系统上设置) |
| alwaysUse24HourFormat | bool |
是否是24小时制 |
| accessibleNavigation | bool |
否使用TalkBack或VoiceOver等辅助功能与程序进行交互 |
| invertColors | bool |
是否支持颜色反转 |
| highContrast | bool |
仅iOS 13以上支持。通过“设置”->“辅助功能”->“增加对比度” |
| disableAnimations | bool |
平台是否要求尽可能禁用或减少动画 |
| boldText | bool |
平台是否要求使用粗体 |
| orientation | Orientation |
是横屏还是竖屏 |
需要注意,MediaQuery必须在MaterialApp的作用域下使用,即在MaterialApp控件之后使用。
// 屏幕大小
Size mSize = MediaQuery.of(context).size;
// 密度
double mRatio = MediaQuery.of(context).devicePixelRatio;
// 设备真实像素
double width = mSize.width * mRatio;
double height = mSize.height * mRatio;
// 上下边距 (状态栏 和 内置导航键)
double topPadding = MediaQuery.of(context).padding.top;
double bottomPadding = MediaQuery.of(context).padding.bottom;
返回拦截 WillPopScope
Flutter中可以通过WillPopScope来实现返回按钮(iOS上的滑动返回)拦截。
WillPopScope中的onWillPop属性是一个回调函数,当用户点击返回按钮时会被调用(或手势操作)。该回调需要返回一个Future对象,如果返回的Future最终值为false时,则当前路由不出栈(不会返回);最终值为true时,当前路由出栈退出。可以通过这个回调来决定是否退出。
WillPopScope(
onWillPop: () async {
if (_lastPressedAt == null ||
DateTime.now().difference(_lastPressedAt) > Duration(seconds: 1)) {
//两次点击间隔超过1秒则重新计时
_lastPressedAt = DateTime.now();
return false;
}
return true;
},
child: Container(
alignment: Alignment.center,
child: Text("1秒内连续点击两次返回键才退出"),
)
);
Builder
使用一个闭包来创建Widget。它的主要用途有两个
- 获取某个控件中的上下文对象(BuildContext)
- 使用一个函数来构建Widget,这样可以在构建前做一些初始化操作
// 以下局部主题修改不生效,则需要使用Builder获取正确的上下文对象。
MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.orange,
primaryColor: Colors.orange,
),
home: Scaffold(
appBar: AppBar(
title: Text(
"Flutter",
style: TextStyle(color: Theme.of(context).accentColor),
),
),
body: Container(
alignment: Alignment.center,
child: Theme(
data: Theme.of(context).copyWith(primaryColor: Colors.red),
child: Text(
"测试",
style: TextStyle(color: Theme.of(context).primaryColor),
),
),
),
),
);
模糊处理 BackdropFilter
该控件主要用于模糊处理,它不仅可以处理图片,也可以处理任意的其他控件。但通常不建议使用模糊处理,对渲染性能影响很大。
模糊图层使用 ImageFilter.blur 设置模糊度,一般是在 0.0-10.0 之间,数值越大模糊度越高,超过 10.0 时完全不可见。另外蒙层还需要设置一个色值,通常可使用 withOpacity 方法设置透明度,一般是在 0.0-1.0 之间。
示例
Stack(
alignment: Alignment.center,
children: <Widget>[
Container(
width: 300,
height: 400,
child: Image.network('https://c-ssl.duitang.com/uploads/item/201810/07/20181007131933_qhjkl.thumb.1000_0.jpg'),
),
BackdropFilter(
filter: ImageFilter.blur(sigmaX: 4.0,sigmaY: 4.0),
child: Center(
child: Container(
color: Colors.red.withOpacity(0),
),
),
)
],
)
截图 RepaintBoundary
可用于截取当前屏幕的Widget的截图,只需要套在想要截图的控件的外层。如要获取全屏截图,将RepaintBoundary包裹在最外层即可。
GlobalKey _gKey = new GlobalKey();
Uint8List imgData ;
@override
Widget build(BuildContext context) {
return RepaintBoundary(
key: _gKey,
child: Scaffold(
appBar: AppBar(
title: Text("Flutter Widget"),
),
body: Stack(
fit: StackFit.expand,
alignment: Alignment.center,
children: <Widget>[
Container(
width: 300,
height: 400,
child: imgData == null?
Image.network('https://c-ssl.duitang.com/uploads/item/201810/07/20181007131933_qhjkl.thumb.1000_0.jpg')
:Image.memory(imgData),
),
FlatButton(
child: Text("截图"),
onPressed: () async {
var data = await _capture();
setState(() {
imgData = data;
});
},
)
],
),
),
);
}
// 返回图片的二进制数据
Future<Uint8List> _capture() async {
RenderRepaintBoundary boundary = _gKey.currentContext.findRenderObject();
ui.Image image = await boundary.toImage();
// 获取png格式数据
ByteData byteData = await image.toByteData(format: ui.ImageByteFormat.png);
Uint8List pngBytes = byteData.buffer.asUint8List();
return pngBytes;
}
主题 Theme
Theme 控件为Material APP 定义了主题数据(ThemeData)。在Flutter 中已预定义了一系列的主题,许多控件或部分或全部应用了这些主题,因此当更改了预定义主题后,所有使用了这些主题的Widget也都会发生相应的变化。
Theme 主要描述了应用程序的颜色和排版选择。主题分为两种:
全局
Theme是由应用程序根MaterialApp创建的主题MaterialApp( title: title, theme: ThemeData( primaryColor: Colors.red, ///... ), );
局部
Theme在应用程序某个区域范围中用于覆盖全局主题,实现灵活的差异化// 对于修改主题的控件,使用Theme包裹 Theme( data: ThemeData( accentColor: Colors.yellow, //... ), child: Text('Hello World'), );
如需获取主题,可使用如下方式
Container(
color: Theme.of(context).accentColor,
chile: Text(
'Text with a background color',
style: Theme.of(context).textTheme.title,
),
);
有时候我们不想要覆盖所有的主题属性,这时候可以扩展父主题
Theme(
/// 使用 copyWith 找到并扩展父主题
data: Theme.of(context).copyWith(accentColor: Colors.yellow),
child: FloatingActionButton(
onPressed: null,
child: Icon(Icons.add),
),
);
Flutter 中主要通过ThemeData去保存应用的主题及样式等信息,因此需要重点了解该类的属性。
| 属性名 | 类型 | 简述 |
|---|---|---|
| brightness | Brightness |
应用的整体主题亮度(可用于适配夜间模式) |
| primarySwatch | MaterialColor |
Material 定义的主题颜色样本。它是具有十种颜色阴影的颜色样本 |
| primaryColor | Color |
主色,决定导航栏颜色 |
| primaryColorBrightness | Brightness |
primaryColor的亮度 |
| primaryColorLight | Color |
primaryColor的较浅版本 |
| primaryColorDark | Color |
primaryColor的较深版本 |
| accentColor | Color |
小控件的前景色(按钮、文本、覆盖边缘效果等) |
| accentColorBrightness | Brightness |
accentColor的亮度 |
| canvasColor | Color |
MaterialType.canvas 的默认颜色 |
| scaffoldBackgroundColor | Color |
为Scaffold下的Material默认色,用于app的背景色 |
| bottomAppBarColor | Color |
bottomAppBarColor的默认颜色 |
| cardColor | Color |
用在卡片(Card)上的Material的颜色 |
| dividerColor | Color |
Divider和PopupMenuDivider的颜色,也用于ListTile之间、DataTable的行之间等 |
| highlightColor | Color |
用于溅墨动画或指示菜单被选中时的高亮颜色 |
| splashColor | Color |
溅墨效果颜色(水波纹) |
| splashFactory | InteractiveInkFeatureFactory |
定义InkWall和InkResponse的外观 |
| selectedRowColor | Color |
高亮选定行的颜色 |
| unselectedWidgetColor | Color |
用于处于非活动(但已启用)状态的小控件的颜色。例如未选中的复选框 |
| disabledColor | Color |
禁用状态下小控件的颜色 |
| buttonColor | Color |
RaisedButtons使用的默认填充色 |
| buttonTheme | ButtonThemeData |
定义按钮部件的默认配置 |
| secondaryHeaderColor | Color |
选定行时PaginatedDataTable标题的颜色 |
| textSelectionColor | Color |
文本框(如TextField)中文本被选中的颜色 |
| cursorColor | Color |
文本框中光标的颜色 |
| textSelectionHandleColor | Color |
用于调整当前选定文本部分的句柄的颜色 |
| backgroundColor | Color |
与primaryColor形成对比的颜色,例如用作进度条的剩余部分 |
| dialogBackgroundColor | Color |
Dialog的背景色 |
| indicatorColor | Color |
TabBar中选中的指示器颜色 |
| hintColor | Color |
用于提示文本或占位符文本的颜色,例如在TextField中 |
| errorColor | Color |
用于输入验证错误的颜色,例如在TextField中 |
| toggleableActiveColor | Color |
用于突出显示Switch、Radio和Checkbox等可切换小部件的活动状态的颜色 |
| fontFamily | String |
字体类型 |
| textTheme | TextTheme |
与卡片和画布对比的文本颜色 |
| primaryTextTheme | TextTheme |
与primaryColor形成对比的文本主题 |
| accentTextTheme | TextTheme |
与accentColor形成对比的文本主题 |
| inputDecorationTheme | InputDecorationTheme |
InputDecorator、TextField和TextFormField的默认InputDecoration值基于此主题 |
| iconTheme | IconThemeData |
与卡片和画布颜色形成对比的图标主题 |
| primaryIconTheme | IconThemeData |
与primaryColor形成对比的图标主题 |
| accentIconTheme | IconThemeData |
与accentColor形成对比的图标主题 |
| sliderTheme | SliderThemeData |
用于呈现Slider的颜色和形状 |
| tabBarTheme | TabBarTheme |
用于自定义选项卡指示器的大小、形状和颜色的主题 |
| cardTheme | CardTheme |
Card的颜色和样式 |
| chipTheme | ChipThemeData |
Chip的颜色和样式 |
| platform | TargetPlatform |
小控件应该适应目标的平台,应该被用来根据平台的约定来样式化UI元素 |
| materialTapTargetSize | MaterialTapTargetSize |
配置某些Material部件的命中测试大小 |
| pageTransitionsTheme | PageTransitionsTheme |
每个目标平台的默认MaterialPageRoute转换 |
| appBarTheme | AppBarTheme |
用于自定义Appbar的颜色、高度、亮度、iconTheme和textTheme的主题 |
| bottomAppBarTheme | BottomAppBarTheme |
自定义BottomAppBar的形状、高度和颜色的主题 |
| colorScheme | ColorScheme |
一组13种颜色,可用于配置大多数组件的颜色属性 |
| dialogTheme | DialogTheme |
自定义Dialog的主题形状 |
| typography | Typography |
用于配置TextTheme、primaryTextTheme和accentTextTheme的颜色和几何文本主题值 |
| cupertinoOverrideTheme | CupertinoThemeData |
用来覆盖Cupertino主题的样式 |
/// 判断当前是否是夜间模式
bool isDarkMode(BuildContext context){
return Theme.of(context).brightness == Brightness.dark;
}
最近更新:
Flutter 1.17版本完成了2018 Material Design规范的Type Scale部分的实现,更新了TextTheme API以匹配当前的Material规范,但保留了旧名称,以使你的代码不会中断。 但是,旧名称已被弃用,因此你将收到警告,以鼓励你采用新名称。
TextStyle的名称和配置

异步 UI
FutureBuilder
FutureBuilder是一个将Dart异步操作和异步UI更新结合起来的控件。由于网络请求,数据库读取等都是耗时的异步操作,通常可以使用该控件将这些耗时操作与UI状态切换进行无缝结合。
其future参数传入一个需要异步执行的任务,而builder则表示根据任务的不同状态构建不同的UI。builder中的snapshot表示任务在执行过程中的不同状态的快照。
Container(
child: FutureBuilder(
future: _buildFuture,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
print('future 还未执行');
return Container();
case ConnectionState.waiting:
print('future 正在执行');
return Center(
child: CircularProgressIndicator(),
);
case ConnectionState.done:
print('future 执行完成');
if (snapshot.hasError) return Text('Error: ${snapshot.error}');
print(snapshot.data);
return _buildListView(context, snapshot);
default:
return Container();
}
},
)
需要注意,在使用FutureBuilder时,如果我们调用setState(或其他原因)重建UI,那么会导致对应的future任务被多次执行,造成不必要的重绘甚至是状态错误。对于这个问题,一般有两种处理方式
- 使用
AsyncMemoizer类,缓存异步任务的返回值,而不是重新执行异步任务。具有用法是使用AsyncMemoizer的runOnce方法在外面包装我们需要执行的异步任务。 - 在外部仅初始化一次异步任务(通常是
initState中),并将异步任务的future作为成员变量存起来,然后再传递给FutureBuilder
StreamBuilder
用法上与FutureBuilder基本相同,它是将Dart的Stream与UI结合起来的控件。Stream与Future的区别,在第一季课程《Flutter 全栈式开发之Dart编程指南》中做了详细说明,Stream本质是一个异步事件的序列,这里不再赘述。
需要注意的是,它的ConnectionState状态中多了一个ConnectionState.active,表示Stream处于激活状态(流上已经有数据传递了)
公众号“编程之路从0到1”