Flutter 学习 容器类Widget

Posted RikkaTheWorld

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 S

以上是关于Flutter 学习 容器类Widget的主要内容,如果未能解决你的问题,请参考以下文章

Flutter 学习 布局类Widget

Flutter,访问父容器widget的padding值

Flutter学习-多子布局Widget

Flutter Widget - Container 容器

Flutter 从另一个 Widget 更改容器宽度

Flutter Stateful Widget 重新创建 State