Flutter容器类组件
容器类Widget与布局类Widget都用作用户界面设计,两者的不同在于:
- 布局类Widget一般都需要接收一个widget数组(children),他们直接或间接继承自(或包含)MultiChildRenderObjectWidget ;而容器类Widget一般只需要接收一个子Widget(child),他们直接或间接继承自(或包含)SingleChildRenderObjectWidget。
- 布局类Widget是按照一定的排列方式来对其子Widget进行排列;而容器类Widget一般只是包装其子Widget,对其添加一些修饰(补白或背景色等)、变换(旋转或剪裁等)、或限制(大小等)。
⚠️注意, Flutter官方并没有对Widget进行如此划分。中文版《Flutter实战》对其分类主要是方便讨论和对Widget功能区分记忆。
1.填充(Paddinig)
1.1 Padding介绍
Padding组件在Android、IOS端只是一个属性,但在Flutter中Padding是一个独立的Widget。
Padding通常用于设置子Widget到父Widget的边距,这部分边距可以称为父组件的内边距,或者子Widget的外边距。
⚠️注意:在Flutter中不存在名为Margin的Widget,因为内外边距也可以通过Padding来完成。
源码分析:
代码语言:txt复制const Padding({
Key key,
@required this.padding, // EdgeInsetsGeometry类型(抽象类),使用EdgeInsets
Widget child,
})
其中的属性this.padding要求传入抽象类EdgeInsetsGeometry,常用其子类EdgeInsets。
1.2 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。
1.3 Padding演练
代码语言:java复制class PaddingDemo extends StatelessWidget {
const PaddingDemo({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
children: [
// Padding Widget
Padding(
padding: EdgeInsets.all(8),
child: Text("莫听穿林打叶声,何妨吟啸且徐行,竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生!"),
),
// 普通Widget
Text("莫听穿林打叶声,何妨吟啸且徐行,竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生!"),
],
);
}
}
效果图如下:
<center>
undefined
</center>
2.装饰容器(DecoratedBox)
2.1 DecoratedBox介绍
DecoratedBox
可以在其子组件绘制前(或后)绘制一些装饰(Decoration),如背景、边框、渐变等。
DecoratedBox
的定义如下:
const DecoratedBox({
Key? key,
required this.decoration,
this.position = DecorationPosition.background,
Widget? child,
})
- decoration:代表将要绘制的装饰,它的类型为Decoration。Decoration是一个抽象类,它定义了一个接口 createBoxPainter(),子类的主要职责是需要通过实现它来创建一个画笔,该画笔用于绘制装饰。
- position:此属性决定在哪里绘制Decoration,它接收DecorationPosition的枚举类型,该枚举类有两个值: background:在子组件之后绘制,即背景装饰。 foreground:在子组件之上绘制,即前景。
其中的属性this.decoration要求传入抽象类Decoration,常用其子类BoxDecoration。
2.2 BoxDecoration类介绍
BoxDecoration
的定义如下:
const BoxDecoration({
Color color, //颜色
DecorationImage image,//图片
BoxBorder border, //边框
BorderRadiusGeometry borderRadius, //圆角
List<BoxShadow> boxShadow, //阴影,可以指定多个
Gradient gradient, //渐变
BlendMode backgroundBlendMode, //背景混合模式
BoxShape shape = BoxShape.rectangle, //形状
})
2.3 DecoratedBox演练
代码语言:txt复制class DecoratedBoxDemo extends StatelessWidget {
const DecoratedBoxDemo({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: DecoratedBox(
decoration: BoxDecoration(
// 线性颜色渐变
gradient: LinearGradient(
colors: [Colors.blue, Colors.lightBlueAccent],
),
// 圆角
borderRadius: BorderRadius.circular(3.0),
// 阴影
boxShadow: [
BoxShadow(
color: Colors.black54,
offset: Offset(2.0, 2.0),
blurRadius: 4.0
)
]
),
// DecoratedBox的子Widget
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 80, vertical: 18),
child: Text(
"装饰测试",
style: TextStyle(color: Colors.white),
),
),
),
);
}
}
效果图如下:
<center>
undefined
</center>
3.变换(Transform)
Transform
可以在其子组件绘制时对其应用一些矩阵变换来实现一些特效。
3.1 Transform - 倾斜
代码语言:txt复制class HomeBody extends StatelessWidget {
const HomeBody({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
child: Transform(
alignment: Alignment.topRight,
transform: Matrix4.skewY(0.3),
child: Text("Transform倾斜示例"),
),
),
);
}
}
效果图如下:
<center>
undefined
</center>
3.2 Transform - 平移
Transform.translate
接收一个offset
参数,可以在绘制时沿x
、y
轴对子组件平移指定的距离。
class HomeBody extends StatelessWidget {
const HomeBody({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: DecoratedBox(
decoration:BoxDecoration(color: Colors.red),
//默认原点为左上角,左移20像素,向上平移5像素
child: Transform.translate(
offset: Offset(-20.0, -5.0),
child: Text("Transform平移"),
),
),
);
}
}
效果图如下:
<center>
undefined
</center>
3.3 Transform - 旋转
Transform.rotate
可以对子组件进行旋转变换,如:
class HomeBody extends StatelessWidget {
const HomeBody({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
child: Transform.rotate(
angle: math.pi, // 旋转180度
child: Text("Transform旋转"),
)
),
);
}
}
效果图如下:
<center>
undefined
</center>
3.4 Transform - 缩放
Transform.scale
可以对子组件进行缩小或放大,如:
class HomeBody extends StatelessWidget {
const HomeBody({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
child: Transform.scale(
scale: 1.5,
child: Text("Transform缩放"),
)
),
);;
}
}
效果图如下:
<center>
undefined
</center>
3.5 Transform注意事项
Transform
的变换是应用在绘制阶段,而并不是应用在布局(layout)阶段,所以无论对子组件应用何种变化,其占用空间的大小和在屏幕上的位置都是固定不变的,因为这些是在布局阶段就确定的。
class HomeBody extends StatelessWidget {
const HomeBody({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
child: Transform.scale(scale: 1.5, child: Text("绘制阶段")),
),
Text("紧靠重叠", style: TextStyle(color: Colors.green))
],
),
);
}
}
效果图如下:
<center>
undefined
</center>
由于第一个Text
应用变换(放大)后,其在绘制时会放大,但其占用的空间依然为红色部分,所以第二个Text
会紧挨着红色部分,最终就会出现文字重合。
- 由于矩阵变化只会作用在绘制阶段,所以在某些场景下,在UI需要变化时,可以直接通过矩阵变化来达到视觉上的UI改变,而不需要去重新触发build流程,这样会节省layout的开销,所以性能会比较好。如之前介绍的Flow组件,它内部就是用矩阵变换来更新UI,除此之外,Flutter的动画组件中也大量使用了Transform以提高性能。
3.6 RotatedBox
RotatedBox
和Transform.rotate
功能相似,它们都可以对子组件进行旋转变换,但是有一点不同:RotatedBox
的变换是在layout阶段,会影响在子组件的位置和大小。我们将上面介绍Transform.rotate
时的示例改一下:
class HomeBody extends StatelessWidget {
const HomeBody({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 第一个组件使用 RotatedBox进行旋转
DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
child: RotatedBox(
quarterTurns: 2,
child: Text("RotatedBox示例"),
),
),
// 第二个组件紧邻左组件
Text("紧靠", style: TextStyle(color: Colors.green))
]
)
);
}
}
效果图如下:
<center>
undefined
</center>
由于RotatedBox是作用于layout阶段,所以子组件会旋转180度(而不只是绘制的内容),decoration会作用到子组件所占用的实际空间上,所以最终就是上图的效果。该示例可以和前面Transform.rotate示例对比理解。
4.Container(组合类容器)
4.1 Container介绍
Container
是一个组合类容器,它是DecoratedBox
、ConstrainedBox
、Transform
、Padding
、Align
等组件组合的一个多功能容器,所以我们只需通过一个Container
组件可以实现同时需要装饰、变换、限制的场景。下面是Container
的定义:
Container({
this.alignment,
this.padding, //容器内补白,属于decoration的装饰范围
Color color, // 背景色
Decoration decoration, // 背景装饰
Decoration foregroundDecoration, //前景装饰
double width,//容器的宽度
double height, //容器的高度
BoxConstraints constraints, //容器大小的限制条件
this.margin,//容器外补白,不属于decoration的装饰范围
this.transform, //变换
this.child,
...
})
大多数属性在介绍其它容器时都已经介绍过了,不再赘述,但有两点需要说明:
- 容器的大小可以通过width、height属性来指定,也可以通过constraints来指定,如果同时存在时,width、height优先。实际上Container内部会根据width、height来生成一个constraints。
- color和decoration是互斥的,实际上,当指定color时,Container内会自动创建一个decoration。
4.2 Container演练
代码语言:txt复制class HomeBody extends StatelessWidget {
const HomeBody({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blueAccent, Colors.lightBlueAccent],
),
boxShadow: [BoxShadow(
color: Colors.black54,
offset: Offset(2.0, 2.0),
blurRadius: 4.0
)]
),
transform: Matrix4.rotationZ(0.4),
width: 100,
height: 100,
child: Icon(Icons.pets, size: 32, color: Colors.white,),
),
);
}
}
效果图如下:
<center>
undefined
</center>
4.3 Padding和Margin介绍
padding与margin都是常用的设置空间的属性,下面用代码演示区分二者的区别。
代码语言:txt复制class HomeBody extends StatelessWidget {
const HomeBody({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
margin: EdgeInsets.all(20),
color: Colors.blue,
child: Text("Margin and Padding"),
),
Container(
padding: EdgeInsets.all(20),
color: Colors.blue,
child: Text("Margin and Padding"),
)
],
),
);
}
}
效果图如下:
<center>
undefined
</center>
直观的感觉就是margin的留白是在容器外部,而padding的留白是在容器内部,读者需要记住这个差异。事实上,Container内margin和padding都是通过Padding 组件来实现的,上面的示例代码实际上等价于:
代码语言:txt复制class HomeBody extends StatelessWidget {
const HomeBody({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(padding: EdgeInsets.all(20),
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.blue),
child: Text("Margin and Padding"),
),
),
DecoratedBox(
decoration: BoxDecoration(color: Colors.blue),
child: Padding(
padding: EdgeInsets.all(20),
child: Text("Margin and Padding"),
),
)
],
),
);
}
}
5. Scaffold(脚手架)
5.1 Scaffold介绍
一个完整的路由页可能会包含导航栏、抽屉菜单(Drawer)以及底部 Tab 导航菜单等。Flutter Material 组件库提供了一些现成的组件来减少我们的开发任务。Scaffold,中文称之为脚手架,为开发者提供了路由页面的整体架构,开发者可以借助它快速便携地实现一个完整的页面。
下面构造一个完整的路由页面对其进行讲解:
- 导航栏
- 导航栏右侧分享按钮
- 抽屉菜单
- 底部导航栏
- 悬浮按钮
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int _selectedIndex = 1;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Scaffold示例'),
actions: [
IconButton(
onPressed: () {
print("分享按钮被点击");
},
icon: Icon(Icons.share)
)
],
),
drawer: null, // 后面会对drawer进行讲解
bottomNavigationBar: BottomNavigationBar(
backgroundColor: Colors.white,
unselectedItemColor: Colors.grey,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: "首页", activeIcon: Icon(Icons.home, color: Colors.deepOrange)),//, activeIcon: Icon(Icons.home, color: Colors.deepOrange,)),
BottomNavigationBarItem(icon: Icon(Icons.search), label: "分类", activeIcon: Icon(Icons.search, color: Colors.deepOrange)),
BottomNavigationBarItem(icon: Icon(Icons.shopping_cart), label: "购物车", activeIcon: Icon(Icons.shopping_cart, color: Colors.deepOrange)),
BottomNavigationBarItem(icon: Icon(Icons.perm_identity), label: "我", activeIcon: Icon(Icons.perm_identity, color: Colors.deepOrange)),
],
currentIndex: _selectedIndex,
fixedColor: Colors.deepOrange,
onTap: (index) {
setState(() {
_selectedIndex = index;
});
},
),
body: Text("Scaffold示例"),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
print("浮窗按钮被点击");
},
),
);
}
}
效果图如下:
<center>
undefined
</center>
在上面这段代码中,用到了以下组件:
<center>
组件名称 | 解释 |
---|---|
appBar | 导航栏 |
drawer | 抽屉菜单 |
bottomNavigationBar | 底部导航栏 |
floatingActionButton | 浮动按钮 |
</center>
5.2 AppBar
AppBar是一个Material风格的导航栏,通过它可以设置导航栏标题、导航栏菜单、导航栏底部的Tab标题等。下面我们看看AppBar的定义:
代码语言:txt复制AppBar({
Key? key,
this.leading, //导航栏最左侧Widget,常见为抽屉菜单按钮或返回按钮。
this.automaticallyImplyLeading = true, //如果leading为null,是否自动实现默认的leading按钮
this.title,// 页面标题
this.actions, // 导航栏右侧菜单
this.bottom, // 导航栏底部菜单,通常为Tab按钮组
this.elevation = 4.0, // 导航栏阴影
this.centerTitle, //标题是否居中
this.backgroundColor,
... //其它属性见源码注释
})
初始的导航栏只有文字,如果给Scaffold添加了抽屉菜单,则会给Scaffold首页导航栏左侧增加菜单按钮,正如上面所示。
若想自定义菜单图标,则可以手动设置AppBar的Leading属性。如:
代码语言:txt复制appBar: AppBar(
title: Text('Scaffold示例'),
leading: Builder(builder: (context) {
return IconButton(
onPressed: (){
// 调用父类打开抽屉
Scaffold.of(context).openDrawer();
},
icon: Icon(Icons.dashboard, color:Colors.orange));
}),
actions: [
IconButton(
onPressed: () {
print("分享按钮被点击");
},
icon: Icon(Icons.share)
)
],
),
效果图如下:
<center>
undefined
</center>
代码中打开抽屉菜单的方法在ScaffoldState中,通过Scaffold.of(context)可以获取父级最近的Scaffold 组件的State对象。
5.3 Drawer介绍
Scaffold
的drawer
和endDrawer
属性可以分别接受一个Widget
来作为页面的左、右抽屉菜单。如果开发者提供了抽屉菜单,那么当用户手指从屏幕左(或右)侧向里滑动时便可打开抽屉菜单。下面的Drawer是一个简单实现:
drawer: Drawer(
child: Column(
children: [
Padding(padding: EdgeInsets.only(left: 20, top: 100),
child: Row(
children: [
Container(
height:100,
width: 100,
child: Image.asset("assets/images/room.jpg"),
),
SizedBox(width: 10),
Text("leiyangyuan", style: TextStyle(fontWeight: FontWeight.bold))
],
),
),
],
),
),
效果图如下:
<center>
undefined
</center>
5.4 FloatingActionButton介绍
FloatingActionButton是Material设计规范中的一种特殊Button,通常悬浮在页面的某一个位置作为某种常用动作的快捷入口,如首页示例中页面右下角的"➕"号按钮。我们可以通过Scaffold的floatingActionButton属性来设置一个FloatingActionButton,同时通过floatingActionButtonLocation属性来指定其在页面中悬浮的位置。
5.5 BottomNavigationBar介绍
我们可以通过Scaffold的bottomNavigationBar属性来设置底部导航,如本节开始示例所示,我们通过Material组件库提供的BottomNavigationBar和BottomNavigationBarItem两种组件来实现Material风格的底部导航栏。可以看到首页示例的代码实现非常简单。
下面这部分代码可以绘出一个美观的底部导航栏:
代码语言:txt复制 bottomNavigationBar: BottomAppBar(
color: Colors.white,
shape: CircularNotchedRectangle(), // 导航栏底部打圆形孔
child: Row(
children: [
IconButton(onPressed: (){
},icon: Icon(Icons.home)),
SizedBox(),
IconButton(onPressed: () {
}, icon: Icon(Icons.business))
],
mainAxisAlignment: MainAxisAlignment.spaceAround,
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
print("浮窗按钮被点击");
},
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
上面的代码通过将底部导航栏挖出一个圆形孔,然后将浮动按钮至于底部导航栏中间,以达到下面的效果:
<center>
undefined
</center>
5.6 页面body
最后就是页面的 Body 部分了,Scaffold 有一个 body 属性,接收一个 Widget,我们可以传任意的 Widget ,在后面介绍滑动组件时,会涉及到 TabBarView,它是一个可以进行页面切换的组件,在多 Tab 的 App 中,一般都会将 TabBarView 作为 Scaffold 的 Body。
undefined
undefined
undefined
undefined
undefined
undefined
undefined
undefined
undefined
undefined
undefined
undefined
undefined
undefined