Flutter 学习 容器类Widget

编程入门 行业动态 更新时间:2024-10-07 06:50:01

Flutter 学习 <a href=https://www.elefans.com/category/jswz/34/1771431.html style=容器类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,})

来看看几个重要的

  • widthheight 可以指定容器的大小,同时 constraints 也可以指定,如果同时存在,则优先使用 witdhheight, 实际上,constraints 也是由 width、height 来生成的
  • colordecoration 是互斥的,同时使用会报错, 而 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的原理:

  1. FittedBox 在布局子组件时,会忽略父组件传递的约束,可以允许子组件无限大
  2. FittedBox 对子组件布局结束后获得子组件的真实大小
  3. 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

ScaffolddrawerendDrawer 属性分别可以接受一个 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导航栏

我们通过 bottomNavigationBarBottomNavigationBarItem 两个组件来来设置底部导航的按钮,比较简单。

除此之外,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

本文发布于:2024-02-13 15:38:24,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1759238.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:容器   Flutter   Widget

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!