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(),
                ),
              )
            ],
          ),
        ));
  }
}
相关文章
|
4天前
|
编解码 前端开发 开发者
【Flutter前端技术开发专栏】Flutter中的响应式设计与自适应布局
【4月更文挑战第30天】Flutter框架助力移动应用实现响应式设计与自适应布局,通过层次化布局系统和`Widget`树管理,结合`BoxConstraints`定义尺寸范围,实现自适应。利用`MediaQuery`获取设备信息,调整布局以适应不同屏幕。`FractionallySizedBox`按比例设定尺寸,`LayoutBuilder`动态计算布局。借助这些工具,开发者能创建跨屏幕尺寸、方向兼容的应用,提升用户体验。
【Flutter前端技术开发专栏】Flutter中的响应式设计与自适应布局
|
4月前
Flutter 组件(二)文本 与 输入框组件
Flutter 组件(二)文本 与 输入框组件
74 0
|
4月前
|
容器
Flutter 组件(一)组件概述
Flutter 组件(一)组件概述
58 0
|
4天前
|
开发框架 前端开发 数据安全/隐私保护
【Flutter 前端技术开发专栏】Flutter 中的布局与样式设计
【4月更文挑战第30天】本文探讨了Flutter的布局和样式设计,关键点包括:1) 布局基础如Column、Row和Stack用于创建复杂结构;2) Container、Center和Expanded等常用组件的作用;3) Theme和Decoration实现全局样式和组件装饰;4) 实战应用如登录界面和列表页面的构建;5) 响应式布局利用MediaQuery和弹性组件适应不同屏幕;6) 性能优化,避免过度复杂设计。了解并掌握这些,有助于开发者创建高效美观的Flutter应用。
【Flutter 前端技术开发专栏】Flutter 中的布局与样式设计
|
4天前
|
前端开发 搜索推荐 UED
【Flutter前端技术开发专栏】Flutter中的高级UI组件应用
【4月更文挑战第30天】探索Flutter的高级UI组件,如`TabBar`、`Drawer`、`BottomSheet`,提升应用体验和美观度。使用高级组件能节省开发时间,提供内置交互逻辑和优秀视觉效果。示例代码展示了如何实现底部导航栏、侧边导航和底部弹出菜单。同时,自定义组件允许个性化设计和功能扩展,但也带来性能优化和维护挑战。参考Flutter官方文档和教程,深入学习并有效利用这些组件。
【Flutter前端技术开发专栏】Flutter中的高级UI组件应用
|
9天前
|
编解码 算法 开发者
Flutter的布局系统:深入探索布局Widget与布局原则
【4月更文挑战第26天】Flutter布局系统详解,涵盖布局Widget(Row/Column、Stack、GridView/ListView、CustomSingleChildLayout)和布局原则(弹性布局、约束优先、流式布局、简洁明了)。文章旨在帮助开发者理解并运用Flutter的布局系统,创建适应性强、用户体验佳的界面。通过选择合适的布局Widget和遵循原则,可实现复杂且高效的UI设计。
|
10天前
|
Dart
Flutter 中优雅切换应用主题的组件
【Flutter】使用adaptive_theme组件优雅切换应用主题:封装MaterialApp,支持light/dark/system模式,自定义色彩,保存选择,含调试按钮。安装配置、设置主题、监听切换、自定义颜色、读取配置步骤详细。代码示例与学习路径推荐。[查看完整教程](https://flutter.ducafecat.com/blog/flutter-app-theme-switch)
Flutter 中优雅切换应用主题的组件
|
8月前
|
人机交互
Flutter笔记 - ListTile组件及其应用
Flutter笔记 - ListTile组件及其应用
97 0
|
4月前
|
容器
Flutter笔记:Box协议的布局约束原理与应用
Flutter笔记:Box协议的布局约束原理与应用
48 0
|
4月前
|
开发者 索引 容器
Flutter开发笔记:Flutter 布局相关组件
Flutter开发笔记:Flutter 布局相关组件
125 0