Padding
Padding
可以给子节点添加填充(留白),和边距的效果类似,定义如下:
Padding({
...
EdgeInsetsGeometry padding,
Widget child,
})
复制代码
EdgeInsetsGeometry 是一个抽象类,开发中,我们一般都使用 EdgeInsets 类,他是 EdgeInsetsGeometry 的子类,定义了一下设置填充的方法
EdgeInsets
- fromLTRB(double left, double top, double right, double bottom) :分别指定四个方向的填充
- all(double value):所有方向都使用相同的数值填充
- only({left, top, right ,bottom }):可以设置具体某个方向的填充,可以同时指定多个方向
- symmetric({ vertical, horizontal }):用于设置对称方向的填充
栗子
代码语言:javascript复制class PaddingTest extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("padding"),
),
body: Container(
color: Colors.red,
child: Padding(
padding: EdgeInsets.fromLTRB(5, 10, 15, 20),
child: Text("I am 345"),
),
),
);
}
}
复制代码
尺寸限制类容器
尺寸类限制容器用于限制容器的大小,Flutter 中提供了很多这样的属性,如 ConstrainedBox
,SizedBox
,UnconstrainedBox
,AspectRatio
等。
ConstrainedBox
ConstrainedBox
用于对子组件添加额外的约束。例如设置最小高度等
栗子
代码语言:javascript复制class BoxTest extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Box"),
),
body: getConstrainedBox(),
);
}
Widget getConstrainedBox() {
return ConstrainedBox(
constraints: BoxConstraints(minWidth: double.infinity, minHeight: 50),
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
),
);
}
}
复制代码
可以看到,虽然将 Container 的高度设置为了 10,但是最终显示的却是 50像素。这正是 ConstrainedBox 的最小高度生效了
BoxConstraints
代码语言:javascript复制const BoxConstraints({
this.minWidth = 0.0, //最小宽度
this.maxWidth = double.infinity, //最大宽度
this.minHeight = 0.0, //最小高度
this.maxHeight = double.infinity //最大高度
})
复制代码
BoxConstraints 定义了一些便捷的构造函数,用于快速的生成 BoxConstraints
- tigth(size) : 生成给定大小的限制
- expand() : 可以生成一个尽可能大的 BoxConstraints
- 还有一些其他的,如图所示:
SizedBox
用于给子元素固定的宽高,例如:
代码语言:javascript复制Widget getSizedBox() {
return SizedBox(width: 50, height: 50, child: getRedBackground());
}
Widget getRedBackground() {
return DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
);
}
复制代码
实际上,SizedBox 只是 ConstrainedBox 的一个定制
代码语言:javascript复制@override
RenderConstrainedBox createRenderObject(BuildContext context) {
return RenderConstrainedBox(
additionalConstraints: _additionalConstraints,
);
}
BoxConstraints get _additionalConstraints {
return BoxConstraints.tightFor(width: width, height: height);
}
复制代码
看上面源码可以知道, Sized 和 ConstrainedBox 都是通过 RenderConstrainedBox,他们的 createRenderObject 方法都是返回一个 RenderConstrainedBox
多重限制
如果一个组件有多个父级 ConstrainedBox
,那么最终是哪个生效,示例:
Widget getConstrainedBoxS() {
return ConstrainedBox(
constraints: BoxConstraints(minWidth: 90, minHeight: 50),
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: 50, minHeight: 90),
child: getRedBackground(),
),
);
}
复制代码
由上面知道的宽高可知,对于 minWidth
和 minHeight
来说,是取父子中相应数值较大的。实际上,只有这样才能保证 父限制与子限制不冲突
UnconstrainedBox
该组件不会对子组件产生任何限制,它允许子组件按照本身大小绘制,一般情况下,我们很少使用此组件,但在 去除 多重限制的时候也许会有帮助,如下:
代码语言:javascript复制Widget getUnConstrainedBox() {
return ConstrainedBox(
constraints: BoxConstraints(minWidth: 90, minHeight: 50),
//去除父级限制
child: UnconstrainedBox(
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: 50, minHeight: 90),
child: getRedBackground(),
),
),
);
}
复制代码
可以看到上面的父级的限制已经被取消了,最终显示的是 50x50。
但是,需要注意的是,这个限制并发真正的去除,看图可知左右还有留白,也就是说父限制是存在的,只不过它不影响子元素 getRedBackground() 的大小,但是仍然还占有相应的空间,这一点必须要注意。
那么有什么办法可以彻底去除限制吗,答案是否定的!所以在开发中如果要对子组件进行限制,那么就一点要注意,因为一旦限制指定条件,子组件如果要进行相关自定义大小时将可能非常困难!
在实际开发中,当我们发现已经使用了 SizedBox
或者 ConstrainedBox
给定子元素宽高,但是仍然没有效果时,几乎可以断定:已经有父元素设置了限制!
例如:Material 组件中的 AppBar 的右侧菜单中,我们使用 SizedBox 指定 loading 按钮的大小,代码如下:
代码语言:javascript复制AppBar(
title: Text("Box"),
actions: [
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation(Colors.white70),
),
)
],
),
复制代码
可以看到 loading 并没有因为设置的大小发生变化,这是应为 Appbar 中已经指定了 action 的限制条件,所以我们要按定义 laoding 的大小 就需要去除限制,如下:
代码语言:javascript复制actions: [
UnconstrainedBox(
child: Padding(
padding: EdgeInsets.all(10),
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation(Colors.white70),
),
),
),
)
],
复制代码
上面使用了 Padding 走了一个内边距,目的是防止贴屏幕右侧的边
其他的容器限制类
除了上面介绍的容器外,还有一些其他的尺寸限制类容器,例如:
AspectRatio
:可以知道子组件的长宽比LimitedBox
:用于指定最大宽高FractionallySizedBox
可以根据父容器宽高比来设置子组件宽高等,
由于这些都使用比较简单,使用的时候可自行了解
装饰容器
DecoratedBox
DecoratedBox
可以在其子组件绘制前(或后),绘制一些装饰(Decoration),如背景,边框,渐变等,定义如下:
const DecoratedBox({
Decoration decoration,
DecorationPosition position = DecorationPosition.background,
Widget child
})
复制代码
- decoration:代表要绘制的装饰,他的类型为 Decoration 是一个抽象类,定义了一个接口 createBoxPainter() ,子类的主要职责是通过实现它来创建一个画笔,该笔用于绘制装饰。
- position:此属性决定在哪里绘制 Decoration,它接受 DecorationPostition 的枚举类型,该枚举有两个类型:
- background:在子组件之后绘制
- foreground:在子组件之上绘制,即前景
BoxDecoration
我们通常会直接使用 BoxDecoration 类,他是 Decoration 的子类,实现了常用装饰元素的绘制
代码语言:javascript复制BoxDecoration({
Color color, //颜色
DecorationImage image,//图片
BoxBorder border, //边框
BorderRadiusGeometry borderRadius, //圆角
List<BoxShadow> boxShadow, //阴影,可以指定多个
Gradient gradient, //渐变
BlendMode backgroundBlendMode, //背景混合模式
BoxShape shape = BoxShape.rectangle, //形状
})
复制代码
下面实现一个带阴影的背景色渐变的按钮
代码语言:javascript复制class DecoratedTest extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("DecoratedTest"),
),
body: Container(
child: DecoratedBox(
//background:背景,foreground:前景
position: DecorationPosition.background,
decoration: BoxDecoration(
//背景渐变
gradient:
LinearGradient(colors: [Colors.red, Colors.orange[700]]),
//圆角
borderRadius: BorderRadius.circular(10),
//阴影
boxShadow: [
BoxShadow(color: Colors.black26, offset: Offset(2, 2))
]),
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 80, vertical: 20),
child: Text(
"Login",
style: TextStyle(color: Colors.white),
),
),
),
),
);
}
}
复制代码
通过 DecoratedBox 可以对子元素进行装饰,上面这个例子中使用了渐变,圆角,阴影等进行装饰,效果如下:
其实装饰类 DecoratedBox 的功能类似于 android 中的 shap ,都是给控件添加各种样式。
变换 Transform
Transform
可以在其子组件绘制时对其应用一些矩阵变换来实现一些特效。
Matrix4
是一个 4D 矩阵,通过它我们可以实现各种矩形操作, 例子:
class TransformTest extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Transform"),
),
body: Container(
color: Colors.black,
child: getSkewTransform(),
),
);
}
Widget getSkewTransform() {
return Transform(
alignment: Alignment(-1, -1), //
transform: Matrix4.skewY(0.3), //沿 Y 轴倾斜0.3弧度
child: Container(
padding: const EdgeInsets.all(8),
color: Colors.deepOrange,
child: const Text('Apartment for rent!'),
),
);
}
}
复制代码
平移
Transform.translate
接受一个 offset 参数,可以在绘制时沿 x,y 轴对子组件平移指定的距离
//平移
Widget getTranslate() {
return DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
child: Transform.translate(
offset: Offset(10, 10),
child: Text("hello world"),
),
);
}
复制代码
旋转
Transform.rotate
可以对子组件进行旋转变化
Widget getRotate() {
return DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
child: Transform.rotate(
angle: math.pi / 2, //旋转90度
child: Text("hello world"),
),
);
}
复制代码
注意,math 需要导包
代码语言:javascript复制import 'dart:math' as math;
复制代码
缩放
Transform.scale
可以对子组件进行缩小或者放大
Widget getScale() {
return DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
child: Transform.scale(
scale: 1.5,
child: Text("hello world"),
),
);
}
复制代码
注意
Transform
的变化是在绘制阶段,而并不是在 layout 阶段,所以无论对 子组件做何种变化,其占用的空间的大小和在屏幕上的位置都是不变的,因为这些都是在布局阶段就确定的,例如:
Widget getTest() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
getScale(),
Text(
"你好",
style: TextStyle(color: Colors.green, fontSize: 18),
)
],
);
}
复制代码
由于 getScale 中的 Text 被放大后,占用的空间依然是红色的部分,所以第二个 Text 就会挨着红色的部分,最终就会出现重合
由于矩阵变换只会作用在绘制阶段,所以在某些场景下,在 UI 需要变化是,可以通过矩阵变换来达到视觉上的 UI 变化,而不是重新 build 流程,这样会节省 layout 的开销,所以性能会比较好,例如 Flow 组件,内部就是使用矩阵变换来更新 UI ,除此之外,Flutter 的动画组件中也大量的使用了 Transform 以提高性能
RotatedBox
RotatedBox
和 Transform.rotate
功能相似,但是有一点不同:RotatedBox 的变化是在 layout 阶段,会影响在子组件的位置和大小
将上面的栗子修改一下:
代码语言:javascript复制Widget getTest() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
getRotate(),
Text(
"你好",
style: TextStyle(color: Colors.green, fontSize: 18),
)
],
);
}
Widget getRotate() {
return DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
child: RotatedBox(
quarterTurns: 1, //旋转90度(1/4)
child: Text("hello world"),
),
);
}
复制代码
由于 RotatedBox 是作用于 layout 阶段,所以子组件会旋转 90 度(而不是绘制内容),decoration 会作用到子组件所占的实际空间上,所以最终的效果如上图
Container
在前面已经使用过很多次 Container
组件,Container
是一个组合类容器,它本身不对应具体的RenderObject
,它是DecoratedBox
、ConstrainedBox、Transform
、Padding
、Align
等组件组合的一个多功能容器
所以我们只需要通过一个 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,
})
复制代码
Container 的大多数属性都已经介绍过了,但是有两点需要说一下:
- 容器的大小可以通过
width
,height
属性来指定,也可以通过constraints
来限定;如果同时存在,则width
和height
优先。实际上 Container 内部会根据 width 和 height 来生成一个 constraints - color 和 decoration 是互斥的,如果同时指定就会报错! 实际上,当指定 color 时,Container 内会自动创建一个 decoration
栗子
代码语言:javascript复制class ContainerTest extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Container"),
),
body: Container(
margin: EdgeInsets.only(top: 50, left: 120),
constraints: BoxConstraints.tightFor(width: 200, height: 150),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
//背景径向渐变
gradient: RadialGradient(
colors: [Colors.red, Colors.orange],
center: Alignment.topLeft,
radius: 58),
//卡片阴影
boxShadow: [
BoxShadow(
color: Colors.black54, offset: Offset(2, 2), blurRadius: 12),
]),
child: Text("521", style: TextStyle(color: Colors.white, fontSize: 40)),
transform: Matrix4.rotationZ(.2),
alignment: Alignment(0,0),
),
);
}
}
复制代码
可以看到 ,Contrainer 具备多种组件的功能,通过源码可以看到,它正是前面我们介绍过得多种组件组合而成,在 Flutter 中,Container
组件也是组合优先于继承的实例
Padding 和 Margin
代码语言:javascript复制Container(
margin: EdgeInsets.all(20.0), //容器外补白
color: Colors.orange,
child: Text("Hello world!"),
),
Container(
padding: EdgeInsets.all(20.0), //容器内补白
color: Colors.orange,
child: Text("Hello world!"),
),
复制代码
效果和 Android 中 padding/margin 中的差不多,padding 是内边距,margin 是外边距
事实上,Container 内 margin 和 padding 都是通过 Padding 组件来实现的。上面的代码实际等价于下面的代码:
代码语言:javascript复制Padding(
padding: EdgeInsets.all(20.0),
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.orange),
child: Text("Hello world!"),
),
),
DecoratedBox(
decoration: BoxDecoration(color: Colors.orange),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Text("Hello world!"),
),
),
复制代码
实际上就是给最外层套了一个Padding
Scaffold,TabBar,底部导航
一个完整的路由页面可能会包含导航栏,抽屉菜单(Drawer) 以及底部 Tab 导航栏菜单等,如果每个路由页面都要开发者手动去完成,这会是一个无聊且麻烦的事情。
幸运的是 Flutter Material 组件库中提供了一些现成的组件来减少我们的开发任务
Scaffold
Scaffold 是一个路由页的骨架,使用它可以很容易的拼装出一个完整的页面
我们实现一个页面,他包含
1,导航栏,导航栏的按钮
2,抽屉菜单
3,底部导航
4,右下角悬浮按钮
实现代码如下:
代码语言:javascript复制class ScaffoldRoute extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _ScaffoldRouteState();
}
}
class _ScaffoldRouteState extends State<ScaffoldRoute> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("App"),
actions: [
IconButton(
icon: Icon(Icons.share),
onPressed: () => {},
)
],
),
drawer: Drawer(
child: Text("抽屉",style: TextStyle(fontSize: 40,color:Colors.blue),),
),
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: "首页"),
BottomNavigationBarItem(icon: Icon(Icons.list_alt), label: "列表"),
BottomNavigationBarItem(icon: Icon(Icons.person), label: "用户")
],
currentIndex: _selectedIndex,
fixedColor: Colors.blue,
onTap: _onItemTapped,
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: _add,
),
);
}
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
}
void _add() {}
}
复制代码
上面代码中我们用到的组件有:
- AppBar:一个导航栏骨架
- MyDrawer:抽屉菜单
- BottomNavigationBar:底部导航栏
- FloatingActionButton:漂浮按钮
AppBar
Appbar 是一个 Material 风格的导航栏,通过他可以设置标题,导航栏菜单,导航底部tab等
代码语言:javascript复制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 设置为菜单按钮(如上面截图所示),点击它可以打开抽屉菜单。
如果想要自定义菜单图标,可以手动设置 leading。如:
代码语言:javascript复制AppBar(
title: Text("App"),
leading: Builder(builder: (context) {
return IconButton(
icon: Icon(
Icons.dashboard,
color: Colors.white,
),
onPressed: () => {Scaffold.of(context).openDrawer()},
);
}),
...........
)
复制代码
可以看到左侧的菜单已经替换成功
打开抽屉的方法在 ScaffoldState 中,通过 Scaffold.of() 可以获取腹肌最近的 Scaffold 组件的 State 对象
ToolBar
下面,在 AppBar 中通过 Bottom 属性创建一个 TabBar 组件,他可以快速的生成 Tab 菜单,
代码语言:javascript复制class _ScaffoldRouteState extends State<ScaffoldRoute> with SingleTickerProviderStateMixin{
int _selectedIndex = 0;
TabController _tabController;
List tabs = ["新闻", "历史", "图片"];
@override
void initState() {
super.initState();
_tabController = TabController(length: tabs.length, vsync: this);
}
AppBar(
title: Text("App"),
bottom: TabBar(
controller: _tabController,
tabs: tabs.map((e) => Tab(text: e)).toList(),
),
........
)
}
复制代码
上面代码创建了一个 TabController,它是用于 监听 Tab 菜单切换的,然后通过 tabBar 生成了一个菜单栏。
TabBar 的 tabs 属性接受一个 Widget 数组,表示每一个 Tab 子菜单,我们可以自定义组件样式,也可以像例子中一样直接使用 Tab 组件
Tab 组件有三个可选参数,除了可以知道文字外,还可以指定 Tab 菜单图标,或者自定义 组件样式,定义如下:
代码语言:javascript复制Tab({
Key key,
this.text, // 菜单文本
this.icon, // 菜单图标
this.child, // 自定义组件样式
})
复制代码
开发者可根据实际的需求定制
TabBarView
通过 TabBar 我们只能生成一个静态菜单,真正的Tab页面还没有实现。由于 Tab
菜单和 Tab
页面的切换需要通过,我们需要通过 TabController 去监听 Tab菜单的切换,然后在去切换 Tab 页面, 代码如:
_tabController.addListener((){
switch(_tabController.index){
case 1: ...;
case 2: ... ;
}
});
复制代码
如果 Tab 页面可以滑动切换的话,还需要在滑动过程中更新 TabBar 指示器的偏移,显然,这样是非常麻烦的!
为此,Material 库提供了一个 TabBarView 组件,通过它不仅可以轻松实现 Tab 页,而且可以非常容易配合 TabBar 来实现通过切换和滑动状态的同步,如下:
代码语言:javascript复制body: TabBarView(
controller: _tabController,
children: tabs.map((e) {
return Container(
alignment: Alignment.center,
child: Text(e),
);
}).toList(),
),
复制代码
现在,无论是点击 Tab 还是 滑动页面, Tab 呀页面都会切换,
在上面的例子中,TabBar 和 TabBarView 的 controller 都是同一个,正是如此, TabBar 和 TabBarView 正是通过一个 controller 来实现菜单切换和滑动状态同步的,效果如下:
另外,Material 组件库也提供了一个 PageView 组件,它和 TabBarView 功能类似,下面将上面的例子重新整理一下,使用 pageView ,让 下面的 导航栏也动起来
代码语言:javascript复制class ScaffoldRoute extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _ScaffoldRouteState();
}
}
class _ScaffoldRouteState extends State<ScaffoldRoute>
with SingleTickerProviderStateMixin {
int _selectedIndex = 0;
PageController _pageController;
TabController _tabController;
List tabs = ["新闻", "历史", "图片"];
@override
void initState() {
super.initState();
_tabController = TabController(length: tabs.length, vsync: this);
_pageController = PageController();
_tabController.addListener(() {
print(_tabController.index);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: PageView(
controller: _pageController,
children: [
home(),
Container(
alignment: Alignment.center,
child: Text("列表"),
),
Container(
alignment: Alignment.center,
child: Text("用户"),
)
],
),
bottomNavigationBar: BottomNavigationBar(
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: "首页"),
BottomNavigationBarItem(icon: Icon(Icons.list_alt), label: "列表"),
BottomNavigationBarItem(icon: Icon(Icons.person), label: "用户")
],
currentIndex: _selectedIndex,
fixedColor: Colors.blue,
onTap: _onItemTapped,
),
);
}
Widget home() {
return Scaffold(
drawer: Drawer(
child: Text(
"抽屉",
style: TextStyle(fontSize: 40, color: Colors.blue),
),
),
appBar: AppBar(
title: Text("App"),
leading: Builder(builder: (context) {
return IconButton(
icon: Icon(
Icons.dashboard,
color: Colors.white,
),
onPressed: () => {Scaffold.of(context).openDrawer()},
);
}),
actions: [
IconButton(
icon: Icon(Icons.share),
onPressed: () => {},
)
],
bottom: TabBar(
controller: _tabController,
tabs: tabs.map((e) => Text(e)).toList(),
),
),
body: TabBarView(
controller: _tabController,
children: tabs.map((e) {
return Container(
alignment: Alignment.center,
child: Text(e),
);
}).toList(),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: _add,
),
);
}
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
_pageController.jumpToPage(index);
}
void _add() {}
}
复制代码
效果如下:
抽屉菜单 Drawer
Scaffold 的 drawer 和 endDrawer 属性分别可以接受一个 Widget 来作为 左,右抽屉菜单。上面的实例中也使用了左抽屉菜单,下面修改一下:
代码语言:javascript复制class DrawerTest extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Drawer(
child: MediaQuery.removePadding(
context: context,
//移除抽屉菜单顶部,默认留白
removeTop: true,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(top: 38),
child: Row(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: ClipOval(
child: Image.asset(
"images/icon.png",
width: 80,
),
),
),
Text(
"文档",
style: TextStyle(fontWeight: FontWeight.bold),
)
],
),
),
Expanded(
child: ListView(
children: [
ListTile(
leading: const Icon(Icons.add),
title: const Text('Add account'),
),
ListTile(
leading: const Icon(Icons.settings),
title: const Text('Manage accounts'),
),
],
),
)
],
),
),
);
}
}
复制代码
代码语言:javascript复制drawer: Drawer(
child: DrawerTest(),
),
复制代码
最终效果如下:
抽屉菜单通常将 Drawer 组件作为根节点,他是限额了 Materal 风格的菜单面板,MediaQuery.removePadding 可以移除 Drawer 默认的一些留白
底部 Tab 导航栏
我们可以通过 Scaffold 的 BottomNavigationBar 属性来设置底部导航,如上面的示例,我们通过 Material 组件提供的 BottomNavigationBar
和 BottomNavigationBarItem
来实现底部导航栏,代码也非常简单
但是如果要实现一些特殊的效果要怎么做呢,示例:
代码语言:javascript复制bottomNavigationBar: BottomAppBar(
color: Colors.white,
shape: CircularNotchedRectangle(),
child: Row(
children: [
IconButton(
icon: Icon(Icons.home),
onPressed: () => _pageController.jumpToPage(0),
),
SizedBox(), //中间位置空出
IconButton(
icon: Icon(Icons.business),
onPressed: () => _pageController.jumpToPage(2),
)
],
//均分底部导航栏的空间
mainAxisAlignment: MainAxisAlignment.spaceAround,
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () => _pageController.jumpToPage(1),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
复制代码
效果如下:
可以看到,上面的代码中没有打孔位置的属性,实际上,打孔位置取决于 FloatingActionButton 的位置,上面的位置为 FloatingActionButtonLocation.centerDocked
,所以打孔的位置在底部导航栏的正中间
BottomAppBar 的 shape 属性决定洞的外形,CircularNotchedRectangle
实现了一个圆形的外形,我们也可以进行自定义;
剪裁
Flutter 中提供了一些剪裁函数,用于对组件进行剪裁。
裁剪 Widget | 作用 |
---|---|
ClipOval | 子组件为正方形时剪裁为内贴圆形,为矩形时,裁切Wie内贴椭圆 |
ClipRRect | 将子组件剪裁为圆角矩形 |
ClipRect | 剪裁子组件到实际占用的矩形大小(溢出部分裁切) |
栗子
代码语言:javascript复制Widget avatar = Image.asset("images/avatar.jpg", width: 80,);
复制代码
代码语言:javascript复制Scaffold(
appBar: AppBar(
title: Text("剪裁"),
),
body: Center(
child: Column(
children: [
//原图
avatar,
//剪裁为圆形
ClipOval(
child: avatar,
),
//剪裁为圆角矩形
ClipRRect(
borderRadius: BorderRadius.circular(5),
child: avatar,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
//Align:调整子组件的位置,
Align(
alignment: Alignment.topLeft,
widthFactor: .5,//自身的 = 子组件 x widthFactor
child: avatar,
),
Text(
"Hello World",
style: TextStyle(color: Colors.green),
)
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 将溢出的部分剪裁
ClipRect(
child: Align(
alignment: Alignment.topLeft,
widthFactor: .5,
child: avatar,
),
),
Text(
"Hello World",
style: TextStyle(color: Colors.green),
)
],
)
],
),
),
);
复制代码
需要注意的是 Align 的 widthFactor
为 0.5 之后,图片的实际宽度等于 0.5 *80 ,即宽度的一半
CustomClippear
如果我们想要剪裁子组件的特定区域,比如,在上面示例的图片中,如果只想截取图片中 40 * 30 向上范围应该怎么做?
这个时候可以使用 CustomClipper 来自定义剪裁的区域,如下
1,自定义 CustomClipper:
代码语言:javascript复制class MyClipper extends CustomClipper<Rect> {
@override
Rect getClip(Size size) {
return Rect.fromLTWH(10, 15, 40, 30);
}
@override
bool shouldReclip(covariant CustomClipper<dynamic> oldClipper) => false;
}
复制代码
- getClip
获取剪裁区域的方法, 图片大小为 80*80,我们返回的区域为
Rect.fromLTWH(10, 15, 40, 30)
, 即图片中 40 * 30 像素的范围 - shouldReclip 是否重新剪裁,如果在应用中,剪裁区域始终不会发生变化时应该返回 false,这样就不会触发重新裁切,避免不必要的开销。如果会发生变化(比如在剪裁区域执行一个动画),那么就变化后应该返回 true 来重新执行裁切
2,通过 ClipRect 来执行剪裁
代码语言:javascript复制DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
child: ClipRect(
child: avatar,
clipper: MyClipper(),
),
)
复制代码
效果如上所示,可以看到是剪裁成功了,但是图片所占用的大小任然是 80 * 80 的,这是因为剪裁是在 layout 完成后的绘制阶段进行的,所以不会影响 组件的大小,这个 Transform 原理是相似的。
参考自 Flutter 实战