容器类Widget"/>
Flutter 学习 容器类Widget
文章目录
- 1. 概述
- 2. 填充 Padding
- 3. 装饰容器 DecoratedBox
- 3.1 BoxDecration
- 4. 变换 Transform
- 4.1 平移
- 4.2 旋转
- 4.3 缩放
- 4.4 RotatedBox
- 5. Container容器
- 5.1 Padding 和Margin
- 6. Clip
- 6.1 CustomClipper
- 7 FittedBox
- 7.1 示例:单行缩放布局
- 8. 脚手架 Scaffold
- 8.1 Scaffold
- 8.1.1 示例代码
- 8.2 AppBar
- 8.3 抽屉菜单 Drawer
- 8.4 FloatingActionButton
- 8.5 底部Tab导航栏
1. 概述
容器类和布局类都是接收子Widget展示,他们有很多相同点,而它们的不同点是:
- 布局Widget一般接收一个 Widget 数组,它们直接或间接继承自
MultiChildRenderObjectWidget
,而容器类 Widget 一般只需要接收一个 子Wdiget,它们直接或间接继承SingleChildRenderObjectWidget
- 布局类 Widget 是按照一定的配列方式来对其子 Widget 进行排列,而 容器类Widget 一般只包装其 子Widget,对齐添加一些修饰、变化或限制
2. 填充 Padding
Padding定义:
class Padding extends SingleChildRenderObjectWidget {const Padding({...required this.padding,Widget? child,})...
padding 是 EdgeInsetsGeometry
,我们一般使用 EdgeInsets
,它是子类,用于指定留白的大小,定义了一些便捷的方法:
formLTRB(...)
分别指定四个方向的填充all(...)
所有方向均使用相同数组的填充only(...)
仅指定某几个方向的填充symmetric(...)
用于设置对称方向的填充,例如vertical
指上(top)下(bottom)
例子:
Padding(padding: EdgeInsets.all(20.0), // 所有方向留白20像素child: Column(children: const [Padding(padding: EdgeInsets.only(right: 10),child: Text("右边留白"),),Padding(padding: EdgeInsets.symmetric(vertical: 20),child: Text("上下留白"),),Padding(padding: EdgeInsets.fromLTRB(5, 20, 20, 8),child: Text("四周留白")),],)),
3. 装饰容器 DecoratedBox
DecoratedBox
可以在其子组件绘制前(或后)绘制一些装饰(Decoration),如背景、边框或者渐变,定义如下:
class DecoratedBox extends SingleChildRenderObjectWidget {const DecoratedBox({required this.decoration,this.position = DecorationPosition.background,Widget? child,})
- decoration
代表要绘制的装饰,类型是Decoration
,它是一个抽象类,定义了一个接口createBoxPainter()
,子类用其实现一个画笔,用于绘制装饰。 position
此属性决定在哪里绘制Decoration
,它接收DecorationPosition
的枚举类型,该枚举类型的值为:
①background
:在子组件之后绘制,用于画背景
②foreground
: 在子组件之上绘制,用于画前景
decoration 一些默认的实现类有:
来学习下 BoxDecoration
3.1 BoxDecration
我们通常会直接使用 BoxDecration
类,它是一个 Decration 的子类,用于实现常用的装饰元素的绘制。
const BoxDecoration({this.color,this.image,this.border, // 边框this.borderRadius, //圆角this.boxShadow, // 阴影this.gradient, // 渐变this.backgroundBlendMode, // 背景混合模式this.shape = BoxShape.rectangle, // 形状})
下面来实现一个带阴影和渐变背景的按钮:
DecoratedBox(decoration: BoxDecoration(gradient:const LinearGradient(colors: [Colors.blue, Colors.purple]), // 匀速渐变borderRadius: BorderRadius.circular(5.0), //圆角boxShadow: const [BoxShadow(color: Colors.black54, //阴影颜色offset: Offset(2.0, 2.0), // 阴影深度blurRadius: 6.0 //阴影圆角)]),child: const Padding(padding: EdgeInsets.symmetric(horizontal: 80, vertical: 18.0),child: Text("Rikka", style: TextStyle(color: Colors.white))),)
效果如下:
4. 变换 Transform
Transform
可以在其子组件绘制时对其应用一些矩阵变换实现动画效果, Matrix4
是一个4D矩阵,通过它们可以实现不同的操作:
Container(color: Colors.yellow,child: Transform(alignment: Alignment.topRight, // 相对坐标系原点的对齐方式transform: Matrix4.skewY(0.3), // 沿着 Y 轴倾斜 0.3child: Container(padding: EdgeInsets.all(8.0),color: Colors.deepOrange,child: Text("This is Funker"),),),)]),
效果为:
4.1 平移
使用 Transform.translate
进行平移,它接收 Offset
参数,用于指定在 x、y轴对子组件的平移距离:
DecoratedBox(decoration: BoxDecoration(color: Colors.red),child: Transform.translate(offset: Offset(20.0, 10.0), child: Text("Hello rikka")))
指定该Text的x轴向右平移20,y轴向下平移10,效果如下:
4.2 旋转
使用 Transform.rotate
进行旋转,使用 angle
来指定旋转角度 :
import 'dart:math' as math;DecoratedBox(decoration: BoxDecoration(color: Colors.red),child: Transform.rotate(angle: math.pi/3, child: Text("Hello rikka")))
效果如下:
4.3 缩放
使用 Transform.scale
进行缩放,使用 angle
来指定旋转角度 :
DecoratedBox(decoration: BoxDecoration(color: Colors.red),child: Transform.scale(scale: 2.0, child: Text("Hello rikka"))),
4.4 RotatedBox
Transform
的变换阶段是在绘制阶段,这是在布局阶段之后,所以无论对子组件应用何种变化,其占用空间的大小和屏幕上的位置都是固定不变的。
用官方的例子来说,看下面代码:
效果是这样的:
这是因为第一个 Text 实际占据的部分就是红色区域,即使其子组件缩放,也不会改变组件的实际位置,而后面的Text是紧跟红色区域的,就产生了文字重叠。
为了解决这个问题,Flutter 封装了一些可以在布局阶段之后变换的Widget,比如 RotatedBox
,它是用于旋转的,代码示例如下:
Row(mainAxisAlignment: MainAxisAlignment.center, children: const [DecoratedBox(decoration: BoxDecoration(color: Colors.red),// 旋转90度child: RotatedBox(quarterTurns: 1, child: Text("Hello rikka"))),Text("Hello",style: TextStyle(color: Colors.blue),)])
5. Container容器
Container
本身没有具体的 RenderObject,因为它继承的是 StatelessWidget
,它是用来组合 DecoratedBox、ConstrainedBox、Transform、Padding等组件的一个容器,它定义如下:
class Container extends StatelessWidget {Container({...this.alignment,this.padding,this.color,this.decoration,this.foregroundDecoration,double? width,double? height,BoxConstraints? constraints,this.margin,this.transform,this.transformAlignment,this.child,this.clipBehavior = Clip.none,})
来看看几个重要的
width
、height
可以指定容器的大小,同时constraints
也可以指定,如果同时存在,则优先使用witdh
和height
, 实际上,constraints 也是由 width、height 来生成的color
和decoration
是互斥的,同时使用会报错, 而 decoration 是由 color 创建的
5.1 Padding 和Margin
这两个属性对 Android开发来已经是老朋友了, padding 用来留白、 margin 用来补白。 而 Container
中使用 Padding
组件来实现的,例如下面代码:
Container(margin: EdgeInsets.all(20.0),color: Colors.blue,child: Text("Hello Rikka"),),Container(padding: EdgeInsets.all(20.0),color: Colors.blue,child: Text("Hello Rikka"),)
和下面代码等价:
Padding(padding: EdgeInsets.all(20.0),child: DecoratedBox(decoration: BoxDecoration(color: Colors.blue),child: Text("Hello Rikka"))),DecoratedBox(decoration: BoxDecoration(color: Colors.blue),child: Padding(padding: EdgeInsets.all(20.0), child: Text("Hello Rikka")))
6. Clip
来看下剪裁相关的 Widget:
ClipOval
子组件为正方形时剪裁成内贴圆形,为矩形时,剪裁成内贴椭圆ClipRRect
将子组件剪裁为圆角矩形ClipRect
默认剪裁子组件布局空间之外的绘制内容ClipPath
按照自定义的路径剪裁
来看下例子:
@overrideWidget build(BuildContext context) {Widget avatar = Image.asset("images/bobo.jpg", width: 60.0);return Scaffold(appBar: AppBar(title: const Text("Basics Demo"),),body: Center(child: Column(children: [avatar, //不剪裁ClipOval(child: avatar), //剪成圆形ClipRRect(//剪裁为圆角矩形borderRadius: BorderRadius.circular(5.0),child: avatar,),Row(mainAxisAlignment: MainAxisAlignment.center,children: [Align(alignment: Alignment.topLeft,widthFactor: .5, //宽度设为原来宽度的一半,另一半则溢出,child: avatar,),const Text("Hello Rikka",style: TextStyle(color: Colors.blue))],),Row(mainAxisAlignment: MainAxisAlignment.center,children: [ClipRect(//将溢出部分裁剪child: Align(alignment: Alignment.topLeft,widthFactor: .5, //宽度设置为原来的一半child: avatar,),),const Text("Hello Rikka",style: TextStyle(color: Colors.blue),)],)],),));}
值得一提的是最后的两个 Row, 他们都设置了 widthFactor
为0.5,即将图片设置为原来的一半
- 第一个
Row
图片溢出部分仍然会显示 - 第二个
Row
剪裁掉了溢出的部分
6.1 CustomClipper
如果我们只想剪裁子组件的特定区域,例如图片中间的 60*60 像素范围,可以使用 CustomClipper
来自定义剪裁区域,使用如下:
class CenterClipper extends CustomClipper<Rect> {@overrideRect getClip(Size size) =>const Rect.fromLTWH(15, 15, 30, 30);@overridebool shouldReclip(covariant CustomClipper<Rect> oldClipper) => false;
}
getClip
用于获取剪裁区域的接口,由于图片是 1000*1000像素,所以中间区域就是 (250, 250, 500, 500)shouldReclip
决定是否剪裁,如果剪裁区域是不中部发生变化,应该返回false,这样就不会触发重新剪裁,避免不必要的性能开销
接着我们来使用这个 Clip:
DecoratedBox(decoration: const BoxDecoration(color: Colors.blue),child: ClipRect(clipper: CenterClipper(),child: avatar,),)
这里就剪裁成功了,但是图片所占用控件大小依然是60*60,这是因为组件大小是在 layout 阶段确定的,而剪裁是在之后绘制进行的,这和 Transform
的原理类似。
7 FittedBox
我们开发中经常会遇到子元素超过父容器大小的情况。
比如将一张大图片显示在一个较小的区域,父组件会将自身最大的显示空间做为约束传递给子组件,子组件如果超出了这个约束,就要做一些缩小、裁剪。
例如 Text 组件如果其他父组件宽度固定,高度不限的话,则默认情况下 Text 会在文本达到父组件宽度时换行,那如果我们想让 Text 文本在超过父组件的宽度时不要换行而是字体缩小,或者在父组件宽高固定时,而Text文本比较小,此时想让文本放大以填充整个父组件空间该这么做呢?
这个问题的本质是 子组件如何适配父组件空间, Flutter提供了一 FittedBox
组件来解决这个问题,定义如下:
const FittedBox({Key? key,this.fit = BoxFit.contain,this.alignment = Alignment.center,this.clipBehavior = Clip.none,Widget? child,})
FittedBox的原理:
FittedBox
在布局子组件时,会忽略父组件传递的约束,可以允许子组件无限大FittedBox
对子组件布局结束后获得子组件的真实大小FittedBox
知道子组件的真实大小,也知道它父组件的约束,那么 FittedBox 就可以通过指定的适配方式(由 BoxFit 的枚举值),让子组件在 FittedBox 父组件的约束范围内按照指定的方式显示
下面是一个示例:
return Center(child: Column(children: [wContainer(BoxFit.none),Text("Rikka"),wContainer(BoxFit.contain),Text("The World"),],),);
...Widget wContainer(BoxFit boxFit) {return Container(width: 50,height: 50,color: Colors.red,child: FittedBox(fit: boxFit,// 子容器超过父容器大小child: Container(width: 80, height: 90, color: Colors.blue),),);}
BoxFit.container
就是按照子组件的比例进行缩放,尽可能多的占据父组件空间
7.1 示例:单行缩放布局
我们有三个数据,都需要在一行展示,换行是不能接受的。 如果数据过多,就会出现数据太长或屏幕太窄,无法显示在一行的情况,因此,我们希望如果无法一行显示时,要对组件进行适当的缩放,以保证一行能够显示的下。
@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text("Basics Demo"),),body: Center(child: Column(children: [_wRow(" 90000000000000000000000000 "),FittedBox(child: _wRow(" 90000000000000000000000000 ")),_wRow(" 800 "),FittedBox(child: _wRow(" 800 ")),].map((e) => Padding(padding: const EdgeInsets.symmetric(vertical: 20),child: e,)).toList(),),),);}
... Widget _wRow(String text) {Widget result = Text(text);result = Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly,children: [result, result, result],);return result;}
我们给 Row 在主轴的对齐方式是 MainAxisAlignment.spaceEvenly
,这会将水平方向的剩余显示空间均分成多份穿插在每一个 child 之间,也就是平等划分区域。
可以看到,当数字为 “90…” 时,三个数字的长度之和已经超出了屏幕的宽度,所以 Row 会有溢出,让给 Row 添加了 FittedBox
时,就可以按比例缩放至一行显示,实现了我们的效果。
但是当数字没有那么大时,如 “800”,直接使用 Row 是符合预期的,但是使用了 FittedBox
却挤在了一起,不符合我们的预期。之所以会这样,是因为指定主轴对齐方式为 sapceEvenly
: Row在布局时会拿到父组件的约束,如果约束的 maxWidth 不是无限大, Row 就会依据子组件的数量和它们的大小在主轴方向来根据 spaceEvenly
填充算法来分割水平的长度,最终 Row 的宽度为 maxWidth,但如果 maxWidth 为无限大,就无法进行分割了,所以此时 Row 就会将子组件宽度之和作为自己的宽度,导致出现这样的结果。
所以此时的解决方法,就是让 FittedBox 子元素接收到的约束的 maxWidth 为宽度屏幕即可,我们分装一个 SingleLineFittedBox 来替换 FittedBox 以达到预期效果,实现代码如下:
class SingleLineFittedBox extends StatelessWidget {const SingleLineFittedBox({Key? key, this.child}) : super(key: key);final Widget? child;@overrideWidget build(BuildContext context) {return LayoutBuilder(builder: (_, constraints) {return FittedBox(child: ConstrainedBox(constraints: constraints.copyWith(// 让 maxWidth 使用屏幕宽度maxWidth: constraints.maxWidth),child: child,));});}
}
然后使用它:
children: [_wRow(" 90000000000000000000000000 "),SingleLineFittedBox(child: _wRow(" 90000000000000000000000000 ")),_wRow(" 800 "),SingleLineFittedBox(child: _wRow(" 800 ")),]
这下下面修复了,但是上面却溢出了, 这是要因为:我们在 SIngleLineFittedBox
中将 Row 的maxWidth 设置为屏幕宽度后,效果和不加 SingleLineFittedBox 的效果是一样的,Row 收到父组件约束的 maxWidth 都是屏幕的宽度,这个是时候需要少加修改就可以实现:
class SingleLineFittedBox extends StatelessWidget {const SingleLineFittedBox({Key? key, this.child}) : super(key: key);final Widget? child;@overrideWidget build(BuildContext context) {return LayoutBuilder(builder: (_, constraints) {return FittedBox(child: ConstrainedBox(constraints: constraints.copyWith(minWidth: constraints.maxWidth, maxWidth: double.infinity),child: child,));});}
}
我们将最小宽度(minWidth)约束指定为屏幕宽度,因为 Row 必须得遵守父组件的约束,所以 Row 的宽度至少等于屏幕宽度,所以就不会出现所在一起的情况,同时将 maxWidth 设置为无限大,就可以处理数字总长度超出屏幕宽度的情况。运行后如下:
8. 脚手架 Scaffold
Scaffold
是 Material 组件库中最常用的组件, 除此之外还有其他丰富多样的组件,可以自行查看 Flutter Gallery 中的 Material 组件部分示例。 它继承自 StatefulWidget
,组合了一些常用的属性。
8.1 Scaffold
Scaffold 一般作为一个路由页的骨架,我们可以使用它来简单地拼装出一个完整的页面。
8.1.1 示例代码
我们实现一个页面,它的包含有:
- 一个导航栏 (AppBar)
- 导航栏右边有一个分享按钮
- 有一个抽屉菜单 (Drawer)
- 有一个底部导航 (BottomNavigation)
- 右下角有一个悬浮的动作按钮 (FloatingActionButton)
代码示例如下:
class _ScaffoldWidgetRouteState extends State<ScaffoldWidgetRoute> {int _selectIndex = 1;@overrideWidget build(BuildContext context) {return Scaffold(// 导航栏appBar: AppBar(title: const Text("Scaffold Demo"),actions: [// 导航栏右侧菜单IconButton(onPressed: () {}, icon: Icon(Icons.share))],),// 抽屉菜单drawer: Drawer(),// 底部导航bottomNavigationBar: BottomNavigationBar(items: const [BottomNavigationBarItem(icon: Icon(Icons.home), label: "Home"),BottomNavigationBarItem(icon: Icon(Icons.ac_unit), label: "Unit"),BottomNavigationBarItem(icon: Icon(Icons.school), label: "School")],currentIndex: _selectIndex,fixedColor: Colors.blue,onTap: (index) => {},),// 悬浮按钮floatingActionButton: FloatingActionButton(child: Icon(Icons.add),onPressed: () => {},),);}
}
效果如下:
可以看到很轻松就实现了这样的页面,这就是 Scaffold 的魅力,接下来看下各个部件
8.2 AppBar
AppBar
是 Material风格的导航栏,通过它可以设置导航栏标题、导航栏菜单、导航栏底部的Tab标题等。
我们来看下 AppBar 的定义:
AppBar({Key? key,this.leading, // 导航栏最左侧的 Widget ,一般是抽屉,或者导航按钮this.automaticallyImplyLeading = true, // 如果leading为null,是否自动实现默认leading的按钮this.title, // 页面标题this.actions, // 导航栏右侧菜单this.flexibleSpace, this.bottom, // 导航栏底部菜单,通常为 Tab按钮组this.elevation, // 导航栏阴影this.centerTitle, // 标题是否居中...})
如果给 Scaffold
实现了 drawer
也就是抽屉菜单, 默认情况下 Scaffold 会自动将 AppBar 的 leading
设置为菜单按钮,点击它就可以打开抽屉。 如果想自定义这个菜单图标,可以手动实现leading,如下代码:
appBar: AppBar(title: const Text("Scaffold Demo"),leading: Builder(builder: (context) {return IconButton(onPressed: () => {Scaffold.of(context).openDrawer()},icon: Icon(Icons.dashboard,color: Colors.white,));}),...
使用 Scaffold.of(context)
可以取得父级最近的 Scaffold 组件的 State 对象, 打开抽屉的方法已经被实现在了 ScaffoldState
中了。
效果为:
8.3 抽屉菜单 Drawer
Scaffold
有 drawer
和 endDrawer
属性分别可以接受一个 Widget 来作为页面的左、右抽屉菜单。 如果开发者提供了抽屉菜单,那么当用户手指从屏幕左或右向里滑动时就可以打开抽屉菜单。
我们来实现一个 MyDrawer:
class MyDrawer extends StatelessWidget {const MyDrawer({Key? key}) : super(key: key);@overrideWidget build(BuildContext context) {return Drawer(child: MediaQuery.removePadding(context: context,// 移除抽屉菜单顶部默认留白removeTop: true,child: Column(crossAxisAlignment: CrossAxisAlignment.start,children: [Padding(padding: const EdgeInsets.only(top: 40),child: Row(children: [Padding(padding: const EdgeInsets.symmetric(horizontal: 16.0),child: ClipOval(child: Image.asset("images/bobo.jpg", width: 80.0),)),const Text("Rika",style: TextStyle(fontWeight: FontWeight.bold))])),Expanded (child: ListView(children: const [ListTile(leading: const Icon(Icons.add),title: Text("Add account"),),ListTile(leading: const Icon(Icons.settings),title: Text("Settings"),),],),)],)));}
}
并在 Scaffold 中使用 drawer : MyDrawer()
, 效果如下:
上面代码中的 MediaQuery.removerPadding
可以移除 Drawer 默认的留白(比如 Drawer 默认顶部会留和手机状态栏等高的空白)。 ListView 将在后面滚动组件中介绍。
8.4 FloatingActionButton
FloatingActionButton
是一种特殊Button,通常悬浮在页面中的某一个位置,通过 floatingActionButton
来设置,不过国内的UI一般都不会看到有这样的按钮就是了
8.5 底部Tab导航栏
我们通过 bottomNavigationBar
、BottomNavigationBarItem
两个组件来来设置底部导航的按钮,比较简单。
除此之外,Material 组件还提供了一个 BottomAppBar
,它可以和 FloatingActionButton
, 效果比较有趣,代码如下:
bottomNavigationBar: BottomAppBar(color: Colors.white,shape: CircularNotchedRectangle(), // 底部导航栏打一个圆形的洞child: Row(children: [IconButton(icon: Icon(Icons.home),onPressed: () {},),SizedBox(), //中间位置空出IconButton(onPressed: () {}, icon: Icon(Icons.ac_unit))],// 均分底部导航栏横向空间mainAxisAlignment: MainAxisAlignment.spaceAround,),),// 悬浮按钮floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,floatingActionButton: FloatingActionButton(child: Icon(Icons.add),onPressed: () => {},),);
其中 BottomAppBar 的shape 属性决定了洞的外形, CircularNotchedRectangle
是Flutter 默认帮我们实现的一个圆形外形,也可以自定义外形, Flutter Gallery 示例中实现了一个 “砖石” 形状的示例。
更多推荐
Flutter 学习 容器类Widget
发布评论