Flutter 学习 容器类Widget

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.3
            child: 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
    按照自定义的路径剪裁

来看下例子:

  @override
  Widget 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> {
    
    
  @override
  Rect getClip(Size size) =>const Rect.fromLTWH(15, 15, 30, 30);
  
  @override
  bool 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 示例:单行缩放布局

我们有三个数据,都需要在一行展示,换行是不能接受的。 如果数据过多,就会出现数据太长或屏幕太窄,无法显示在一行的情况,因此,我们希望如果无法一行显示时,要对组件进行适当的缩放,以保证一行能够显示的下。

  @override
  Widget 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;

  @override
  Widget 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;

  @override
  Widget 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;

  @override
  Widget 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);

  @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: 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 示例中实现了一个 “砖石” 形状的示例。

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/rikkatheworld/article/details/121739453