Flutter | 布局组件(上)

简介: Flutter | 布局组件(上)

布局类组件都会包含一个或多个组件,不同的布局类组件对子组件(layout)方式不同。在 Flutter 中 Element 树才是最终的绘制树,Element 树是通过 Widget 树来创建的 (通 Widget.createElement()) ,Widget 其实就是 Element 的配置数据。


在 Fluter 中,根据 Widget 是否需要包含子节点将 Widget 分为了三类,分别对应三种 Element,如下表:

image.png

Flutter 中很多 widget 都是继承自 StatelessWidget 或者 StatefulWidget ,然后再 build 方法中构建真正的 RenderObjectWidget。如 Text 是继承自 StatelessWidget ,然后在 build 方法中通过 RichText 构建子树,而 RichText 才是继承自 MultiChildRenderObjectWidget。


所以说 Text 属于 MultiChildRenderWidget(其他 Widget 也可以这样描述),其实 StatelessWidget 和 StatefulWidget 就是两个用于组合的 Widget 的基类,他们本身最终并不关联最终的渲染对象(RenderObjectWidget)


MultiChildRenderObjectWidget 是继承自 RenderObjectWidget 的,在 RenderObjectWidget 中定义了创建,更新 RenderObject 的方法,子类必须实现他们,其实 RenderObject 就是最终布局,渲染 UI 界面的对象,也就是说,对于布局类组件来说,其布局算法都是通过对应的 RenderObject 对象来实现的。


所以在 RichText 中就实现了 创建,更新 RenderObject 的方法


布局组件就是直接或间接继承(包含)MultiChildRenderObjectWidget 的 Widget,他们一般都会有一个 children 属性用于接收子 Widget 。


一个普通的 Widget 继承路线为:


继承 (Stateless/Stateful)Widget ,然后实现 build 方法


在 build 方法中通过创建继承自 (Leaf/SingleChild/MultiChild)RenderObjectWidget 的类,然后实现对应的方法来构建最终的渲染UI界面的对象(RenderObject)


而 (Leaf/SingleChild/MultiChild)RenderObjectWidget 则是继承自 RenderObjectWidget ,最终继承自 Widget。


在 RenderObjectWidget 和 Widget 中定义着 创建,更新RenderObject 的方法。以及 createElement 方法。


其实 createElement 方法是在 (Leaf/SingleChild/MultiChild)RenderObjectWidget 类中实现的,而创建,更新 ObjectRender 则是在 (Leaf/SingleChild/MultiChild)RenderObjectWidget 的实现类中完成的


线性布局(Row 和 Column)


线性布局指的是沿着水平或者垂直方向排布子组件。在 Flutter 中通过 Row 和 Column 来实现线性布局,类似于 Android 中的 LinearLayout 控件


Row 和 Column 都继承子 Flex,至于 Fiex 暂不多说


主轴和纵轴


在线性布局中,如果布局是水平方向,主轴就是指水平方向,纵轴即垂直方向;如果布局是垂直方向,主轴就是垂直方向,那么纵轴就是水平方向。


在线性布局中,有两个定义对齐方式的枚举类 MainAxisAlignment 和 CrossAxisAlignment ,分别代表主轴对齐和纵轴对齐


Row


Row 可以在水平方向排列子 Widget。其定义如下:


Row({
  //......
  MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
  MainAxisSize mainAxisSize = MainAxisSize.max,
  CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
  TextDirection? textDirection,
  VerticalDirection verticalDirection = VerticalDirection.down,
  TextBaseline textBaseline = TextBaseline.alphabetic,
  List<Widget> children = const <Widget>[],
})


textDirection :水平方向组件的布局顺序,默认为系统当前 Locale 环境的文本方向(中文,英语都是左往右,而阿拉伯是右往左)


mainAxisSize:表示 Row 在主轴(水平)占用的空间,如 MainAxisSize.max 表示尽可能多的占用水平方向的空间,此时无论子 Widget 占用多少空间,Row 的宽度始终等于水平方向的最大宽度; MainAxisSize.min 表示尽可能的少占用水平空间,当子 Widget 没有占满水平剩余空间,则 Row 的实际宽度等于所有的子组件占用的水平空间。


其实就相当于 Android 中的 match_parent 和 warp_parent


mainAxisAlignment:表示子组件在 Row 所占水平空间的对齐方式,如果 mainAxisSize 值为 min,则此属性毫无意义,对应的值有 start ,center,end 等。


需要注意的是,textDirection 是 mainAxisAlignment 的参考系。例如 textDirection 是textDirection.ltr 时,则 MainAxisAlignment.start 表示左对齐,如果为 rtl 则,start 表示右对齐


crossAxisAlignment:表示子组件在纵轴的对齐方式,他的值也是 start,center,end 。只不过参考系是 verticalDirection 的值,具体的和上面的差不多,只是方向变了


children:子组件数组


栗子


class RowTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("线性布局 Row,Column"),
      ),
      body:  Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [Text("Hello word"), Text("345")],
          ),
          Row(
            mainAxisSize: MainAxisSize.min,
            mainAxisAlignment: MainAxisAlignment.center,
            children: [Text("Hello word"), Text("345")],
          ),
          Row(
            mainAxisSize: MainAxisSize.max,
            mainAxisAlignment: MainAxisAlignment.end,
            children: [Text("Hello word"), Text("345")],
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.end,
            textDirection: TextDirection.rtl,
            children: [Text("Hello word"), Text("345")],
          ),
          Row(
            crossAxisAlignment: CrossAxisAlignment.start,
            verticalDirection: VerticalDirection.up,
            children: [
              Text(
                "Hello word",
                style: TextStyle(fontSize: 30),
              ),
              Text("345")
            ],
          )
        ],
      )
    );
  }
}


Column


Column 可以在垂直方向排列其子组件,参数和 Row 一样,只不过排列的方式是垂直的,主轴和纵轴相反。


栗子


class ColumnTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("线性布局 Row,Column"),
      ),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [Text("Hi"), Text("World")],
      ),
    );
  }
}


由于有指定 主轴的 size,所以默认为 max。则这个 Column 会占用尽可能多的空间,这个栗子中为屏幕的高度


crossAxisAlignment 为 center,表示在纵轴上居中对齐。Colum 的宽度取决于其子 Widget 中宽度最大的 Widget,所以 hi 会被显示在 world 的中间部分


Row 和 Column 都只会在主轴上占用尽可能的最大空间,而纵轴的长度取决于他们最大子 Widget 的长度


如何让 hi 和 world 在屏幕中间对齐呢,有如下两种办法:


class ColumnTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("线性布局 Row,Column"),
      ),
      body: ConstrainedBox(
        constraints: BoxConstraints(minWidth: double.infinity),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.center,
          children: [Text("Hi"), Text("World")],
        ),
      ),
    );
  }
}


特殊情况


如果 Row 嵌套 Row ,或者 Column 嵌套 Column,那么之后最外面的 Row/Column 会占用尽可能大的空间,


class ColumnTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("线性布局 Row,Column"),
        ),
        body: Container(
          color: Colors.green,
          child: Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisSize: MainAxisSize.max, //有效,外层Colum高度为整个屏幕
              children: <Widget>[
                Container(
                  color: Colors.red,
                  child: Column(
                    mainAxisSize: MainAxisSize.max, //无效,内层Colum高度为实际高度
                    children: <Widget>[
                      Text("hello world "),
                      Text("I am Jack "),
                    ],
                  ),
                )
              ],
            ),
          ),
        ));
  }
}


这种情况可以使用 Expanded 组件


children: <Widget>[
 Expanded(
   child:  Container(
     color: Colors.red,
     child: Column(
       mainAxisSize: MainAxisSize.max, //无效,内层Colum高度为实际高度
       children: <Widget>[
         Text("hello world "),
         Text("I am Jack "),
       ],
     ),
   ),
 )
]


弹性布局 Flex


弹性布局允许子组件按照一定比例来分配父容器空间。Flutter 中弹性布局主要通过 Flex 和 Expanded 来配合实现


Flex 组件可以沿着水平或者垂直方向排列子组件,如果知道主轴方向,使用 Row 或者 Column 会更方便一些。Row 和 Column 都继承子 Flex,参数也都基本相同,所以能使用 Flex 的地方基本上都可以使用 Row 或者 Column。


Flex 可以和 Expanded 组件配合实现弹性布局,大多数参数基本都和线性布局一样,这里不做介绍,定义如下


Flex({
  Key? key,
  required this.direction,
  List<Widget> children = const <Widget>[],
})


direction:弹性布局的方向,Row 默认为 水平方向,Column 默认为垂直方向

Flex 继承自 MultiChildRenderObjectWidget ,对应的 RenderObject 为 RenderFlex,RenderFlex 中实现了其布局算法


Expanded


可以按比例 扩伸 Row,Column 和 Flex 子组件所占用的空间


const Expanded({
  int flex = 1, 
  @required Widget child,
})


flex:弹性系数,如果为 0 或者 null,则没有弹性,既不会扩展占用的空间。如果大于 0,所有的 Expanded 按照 flex 的比例来分隔主轴的全部空闲空间


栗子


class FlexTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("弹性布局Flex"),
      ),
      body: Column(
        children: [
          Flex(
            direction: Axis.horizontal,
            children: [
              Expanded(
                flex: 1,
                child: Container(
                  height: 30,
                  color: Colors.red,
                ),
              ),
              Expanded(
                flex: 2,
                child: Container(
                  height: 30,
                  color: Colors.blue,
                ),
              )
            ],
          ),
          Padding(
            padding: const EdgeInsets.only(top: 20),
            child: SizedBox(
              height: 100,
              child: Flex(
                direction: Axis.vertical,
                children: [
                  Expanded(
                    flex: 2,
                    child: Container(
                      height: 30,
                      color: Colors.yellow,
                    ),
                  ),
                  Spacer(
                    flex: 1,
                  ),
                  Expanded(
                    flex: 1,
                    child: Container(
                      height: 30,
                      color: Colors.green,
                    ),
                  )
                ],
              ),
            ),
          )
        ],
      ),
    );
  }
}


栗子中的 Spacer 的功能是占用指定比例的空间,实际上它只是 Expanded 的一个包装类


流式布局 Wrap ,Flow


在使用 Row 和 Column 时,如果子 Widget 超出 屏幕范围,则会报溢出错误,如:


class WrapAndFlowTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("WarpAndFlow"),
      ),
      body: Container(
        height: 100,
        child: Row(
          mainAxisAlignment: MainAxisAlignment.start,
          mainAxisSize: MainAxisSize.max,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [Text("345" * 100)],
        ),
      ),
    );
  }
}


可以看到,右边部分报出溢出错误。这是因为 Row 默认只有一行,如果超出屏幕,不会折行,并且会报错


我们把超出自动折行的布局称为流式布局。Flutter 中通过 Wrap 和 Flow 来支持流式布局。


Wrap 定义如下


Wrap({
  ...
  this.direction = Axis.horizontal,
  this.alignment = WrapAlignment.start,
  this.spacing = 0.0,
  this.runAlignment = WrapAlignment.start,
  this.runSpacing = 0.0,
  this.crossAxisAlignment = WrapCrossAlignment.start,
  this.textDirection,
  this.verticalDirection = VerticalDirection.down,
  List<Widget> children = const <Widget>[],
})


可以看到有很多属性在 Row ,Colum 中都有,如 direction,textDirection等,这些参数意义都相同,这里不过多介绍


spacing:主轴方向子 Widget 的间距

runSpacing:纵轴方向的间距

runAlignment:纵轴方向的对齐方式


例子


class WrapAndFlowTest extends StatelessWidget {
  final List<String> _list = const [
    "爱是你我",
    "一壶老酒",
    "最炫民族风",
    "怒放的生命",
    "再见青春",
    "北京,北京"
  ];
  List<Widget> getMusicList() {
    /* List<Widget> widgets = new List();
    _list.forEach((element) {
      widgets.add(RaisedButton(
        child: Text(element),
        onPressed: () => print(element),
      ));
    });*/
    return _list
        .map((e) => RaisedButton(
              child: Text(e),
              onPressed: () => print(e),
            ))
        .toList();
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("WarpAndFlow"),
        ),
        body: Padding(
          padding: EdgeInsets.all(10),
          child: Flex(
            direction: Axis.horizontal,
            children: [
              Expanded(
                flex: 1,
                child: Wrap(
                  spacing: 25,
                  runSpacing: 4,
                  alignment: WrapAlignment.center,
                  children: getMusicList(),
                ),
              )
            ],
          ),
        ));
  }
}
相关文章
|
2月前
|
传感器 缓存 监控
Stream 组件在 Flutter 中的应用场景有哪些?
Stream 组件在 Flutter 中的应用场景有哪些?
173 58
|
2月前
|
UED 开发者
Flutter|常用数据通信组件
Flutter|常用数据通信组件
101 49
|
5天前
|
开发者 容器
Flutter&鸿蒙next 布局架构原理详解
本文详细介绍了 Flutter 中的主要布局方式,包括 Row、Column、Stack、Container、ListView 和 GridView 等布局组件的架构原理及使用场景。通过了解这些布局 Widget 的基本概念、关键属性和布局原理,开发者可以更高效地构建复杂的用户界面。此外,文章还提供了布局优化技巧,帮助提升应用性能。
64 4
|
5天前
|
容器
深入理解 Flutter 鸿蒙版的 Stack 布局:适配屏幕与层叠样式布局
Flutter 的 Stack 布局组件允许你将多个子组件层叠在一起,实现复杂的界面效果。本文介绍了 Stack 的基本用法、核心概念(如子组件层叠、Positioned 组件和对齐属性),以及如何使用 MediaQuery 和 LayoutBuilder 实现响应式设计。通过示例展示了照片展示与文字描述、动态调整层叠布局等高级用法,帮助你构建更加精美和实用的 Flutter 应用。
76 2
|
5天前
Flutter 自定义组件继承与调用的高级使用方式
本文深入探讨了 Flutter 中自定义组件的高级使用方式,包括创建基本自定义组件、继承现有组件、使用 Mixins 和组合模式等。通过这些方法,您可以构建灵活、可重用且易于维护的 UI 组件,从而提升开发效率和代码质量。
105 1
|
5天前
|
开发工具 UED
Flutter&鸿蒙next中封装一个输入框组件
本文介绍了如何创建一个简单的Flutter播客应用。首先,通过`flutter create`命令创建项目;接着,在`lib`目录下封装一个自定义输入框组件`CustomInput`;然后,在主应用文件`main.dart`中使用该输入框组件,实现简单的UI布局和功能;最后,通过`flutter run`启动应用。本文还提供了后续扩展建议,如状态管理、网络请求和UI优化。
82 1
|
8天前
|
Dart UED
Flutter用户交互组件
Flutter用户交互组件
12 2
|
17天前
|
容器
Flutter&鸿蒙next 布局架构原理详解
Flutter&鸿蒙next 布局架构原理详解
|
23天前
|
存储 开发框架 开发者
flutter:代码存储&基本组件 (五)
本文档介绍了Flutter中的一些基本组件和代码示例,包括代码存储、基本组件如AppBar的简单使用、可滑动切换的标签栏、TextField的多种用法(如简单使用、登录页面、文本控制器的监听与使用、修饰等),以及如何实现点击空白区域隐藏键盘等功能。通过这些示例,开发者可以快速掌握在Flutter应用中实现常见UI元素的方法。
|
23天前
|
Android开发 开发者 容器
flutter:&UI布局 (六)
本文档介绍了Flutter中的UI布局方式,包括线性布局(如Column和Row)、非线性布局(如Stack、Flex、Positioned)以及Wrap布局等。通过具体示例代码展示了如何使用这些布局组件来构建灵活多变的用户界面,例如使用Column垂直排列文本、使用Stack叠加组件、以及利用Wrap实现自动换行的按钮布局等。