Skip to content

Flutter 布局组件

ListView 列表组件

列表布局是我们项目开发中最常用的一种布局方式。Flutter 中我们可以通过 ListView 来定义列表项,支持垂直和水平方向展示。通过一个属性就可以控制列表的显示方向。列表有以下分类

  • 垂直列表
  • 垂直图文列表
  • 水平列表
  • 动态列表

列表组件常用参数

名称类型说明
scrollDirectionAxisAxis.horizontal 水平列表 Axis.vertical 垂直列表
paddingEdgeInsetsGeometry内边距
resolvebool组件反向排序
childrenList列表元素

Divider 横线, ListTile 一行内容

垂直列表

dart
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  List<Widget> _initListData() {
    List<Widget> list = [];
    for (var i = 0; i < 20; i++) {
      list.add(
        ListTile(
          leading: Image.network("https://ronhai.com/media/images/article/homebrew.png"),
          title: Text('华北黄淮高温雨今起强势登场${i + 1}'),
          subtitle: const Text("中国天气网讯 21日开始,华北黄淮高温雨今起强势登场"),
          trailing:
              Image.network("https://ronhai.com/media/images/article/homebrew.png"),
        ),
      );
    }
    return list;
  }

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: _initListData(),
    );
  }
}

垂直图文列表

dart
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  List<Widget> _initListData() {
    List<Widget> list = [];
    for (var i = 0; i < 20; i++) {
      list.add(
        Image.network('https://ronhai.com/media/images/article/homebrew.png'),
      );
      list.add(Container(
        height: 40,
        padding: EdgeInsets.fromLTRB(0, 10, 0, 10),
        child: Text(
          '我是一个标题${i}',
          textAlign: TextAlign.center,
          style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
        ),
      ));
    }
    return list;
  }

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: _initListData(),
    );
  }
}

水平列表

可以左右滑动

dart
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 200,
      child: ListView(
        scrollDirection: Axis.horizontal,
        children: [
          Container(
            width: 160.0,
            color: Colors.red,
            padding: const EdgeInsets.all(8.0),
          ),
          const SizedBox(width: 10),
          SizedBox(
            width: 160.0,
            child: Column(
              children: [
                SizedBox(
                  width: double.infinity,
                  height: 160,
                  child: Image.network(
                    'https://ronhai.com/media/images/article/homebrew.png',
                    fit: BoxFit.cover,
                  ),
                ),
                const SizedBox(
                  height: 8,
                ),
                const Text('World'),
              ],
            ),
          ),
          const SizedBox(width: 10),
          Container(
            width: 160.0,
            color: Colors.blue,
          ),
          const SizedBox(width: 10),
          Container(
            width: 160.0,
            color: Colors.green,
          ),
          const SizedBox(width: 10),
          Container(
            width: 160.0,
            color: Colors.yellow,
          ),
          const SizedBox(width: 10),
          Container(
            width: 160.0,
            color: Colors.orange,
          ),
        ],
      ),
    );
  }
}
image-20241227142629469

ListView.builder

动态实现列表

dart
import 'package:flutter/material.dart';

void main(List<String> args) {
  runApp(
    MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
          appBar: AppBar(
            centerTitle: true,
            title: const Text(
              '动态列表',
            ),
          ),
          body: MyApp()),
    ),
  );
}

class MyApp extends StatelessWidget {
  final List list = [];

  MyApp({super.key}) {
    for (var i = 0; i < 10; i++) {
      list.add("我是一个列表--$i");
    }
  }

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: list.length,
      itemBuilder: (context, index) {
        return ListTile(
          title: Text(list[index]),
        );
      },
    );
  }
}
image-20241227143046575

GridView 网格布局组件

GridView 网格布局在实际项目中用的也是非常多的,当我们想让可以滚动的元素使用矩阵方式排列的时候。此时我们可以用网格列表组件 GridView 实现布局。

GridView 创建网格列表主要有下面三种方式

  • 可以通过 GridView.count 实现网格布局
  • 可以通过 GridView.extent 实现网格布局
  • 通过 GridView.builder 实现动态网格布局

常用属性

名称类型说明
scrollDirectionAxis滚动方法
paddingEdgeInsetsGeometry内边距
resolvebool组件反向排序
crossAxisSpacingdouble水平子 Widget 之间间距
mainAxisSpacingdouble垂直子 Widget 之间间距
crossAxisCountint 用在 GridView.count一行的 Widget 数量
maxCrossAxisExtentdouble 用在 GridView.extent横轴子元素的最大长度
childAspectRatiodouble子 Widget 宽高比例
children[ ]
gridDelegateSliverGridDelegateWithFixedCrossAxisCount SliverGridDelegateWithMaxCrossAxisExtent控制布局主要用在 GridView.builder 里面

实现网格布局

GridView.count 构造函数内部使用了 SliverGridDelegateWithFixedCrossAxisCount,我们通过它可以快速的创建横轴固定数量子元素的 GridView

dart
import 'package:flutter/material.dart';

void main(List<String> args) {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
          appBar: AppBar(
            leading: const Icon(Icons.more_horiz),
            centerTitle: true,
            title: const Text('网格布局'),
            actions: const [
              Icon(Icons.add),
            ],
          ),
          body: MyHomePage()),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return GridView.count(
      crossAxisCount: 5, //一行的Widget数量
      childAspectRatio: 2, //子Widget宽高比例
      crossAxisSpacing: 10,
      mainAxisSpacing: 10,
      children: const [
        Icon(Icons.home),
        Icon(Icons.ac_unit),
        Icon(Icons.search),
        Icon(Icons.settings),
        Icon(Icons.airport_shuttle),
        Icon(Icons.all_inclusive),
        Icon(Icons.beach_access),
        Icon(Icons.cake),
        Icon(Icons.circle),
        Icon(Icons.dashboard),
      ],
    );
  }
}
image-20241227150501199

实现网格布局

GridView.extent 构造函数内部使用了 SliverGridDelegateWithMaxCrossAxisExtent,我们通过它可以快速的创建横轴子元素为固定最大长度的的 GridView。

dart
class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return GridView.extent(
      maxCrossAxisExtent: 50, //横轴子元素的最大宽度
      childAspectRatio: 1, //子Widget高比例
      children: const [
        Icon(Icons.home),
        Icon(Icons.ac_unit),
        Icon(Icons.search),
        Icon(Icons.settings),
        Icon(Icons.airport_shuttle),
        Icon(Icons.all_inclusive),
        Icon(Icons.beach_access),
        Icon(Icons.cake),
        Icon(Icons.circle),
        Icon(Icons.dashboard),
      ],
    );
  }
}

GridView.builder

  • SliverGridDelegateWithFixedCrossAxisCount 实现 count 功能

  • SliverGridDelegateWithMaxCrossAxisExtent 实现 extent 功能

    设置同 count 和 extent 一样,gridDelegate 的类型选择哪种类型渲染

dart
import 'package:flutter/material.dart';

void main(List<String> args) {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
          appBar: AppBar(
            leading: const Icon(Icons.more_horiz),
            centerTitle: true,
            title: const Text('网格布局'),
            actions: const [
              Icon(Icons.add),
            ],
          ),
          body: MyHomePage()),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: GridView.builder(
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 3,
          crossAxisSpacing: 10,
          mainAxisSpacing: 10,
        ),
        itemBuilder: (context, index) {
          return Container(
            color: Colors.blue,
            child: Center(
              child: Text(
                'Item ${index + 1}',
                style: const TextStyle(color: Colors.white),
              ),
            ),
          );
        },
        itemCount: 15, //数量
      ),
    );
  }
}
image-20241227160703148

Flex Expanded 弹性布局

Flex 组件可以沿着水平或垂直方向排列子组件,如果你知道主轴方向,使用 Row 或 Column 会方便一些,因为 Row Column 都继承自 Flex ,参数基本相同,所以能使用 Flex 的地方基本上都可以使用 Row 或 Column 。 Flex 本身功能是很强大的,它也可以和 Expanded 组件配合实现弹性布局 。Row 和 Column 是继承 flex 布局,使用 Expanded 必需在这三个组件中,具有 flex 的容器中

水平弹性布局

dart
class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Flex(
      direction: Axis.horizontal,
      children: [
        Expanded(
            flex: 1,
            child: Image.network(
                'https://ronhai.com/media/images/article/flutter.png')),
        Expanded(
            flex: 1,
            child: Image.network(
                'https://ronhai.com/media/images/article/flutter.png')),
        const Expanded(
          flex: 2,
          child: Text('Hello World'),
        )
      ],
    );
  }
}

垂直弹性布局

dart
class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Flex(
      direction: Axis.vertical,
      children: [
        Expanded(
            flex: 1,
            child: Container(
              color: Colors.red,
              child: const Center(child: Text('1')),
            )),
        Expanded(
            flex: 2,
            child: Container(
              color: Colors.pink,
              child: const Center(child: Text('2')),
            )),
        Expanded(
            flex: 1,
            child: Container(
                alignment: Alignment.center,
                width: double.infinity,
                color: Colors.blue,
                child: const Text(
                  '222',
                  style: TextStyle(color: Colors.white),
                )))
      ],
    );
  }
}
image-20241229100235460

Row 和 Column 线性布局

Row 水平布局组件

属性说明
mainAxisAlignment主轴的排序方式
crossAxisAlignment次轴的排序方式
children组件子元素

Column 垂直布局组件

属性说明
mainAxisAlignment主轴的排序方式
crossAxisAlignment次轴的排序方式
children组件子元素

double

double.infinity 和 double.maxFinite 可以让当前元素的 width 或者 height 达到父元素的尺寸

元素铺满

dart
class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      height: double.infinity,
      color: Colors.grey[200],
      child: const Column(
        crossAxisAlignment: CrossAxisAlignment.stretch, //交叉轴对齐方式
        mainAxisAlignment: MainAxisAlignment.spaceEvenly, //主轴对齐方式
        children: [
          Icon(Icons.volunteer_activism),
          Icon(Icons.volunteer_activism),
          Icon(Icons.volunteer_activism),
          Icon(Icons.volunteer_activism),
          Icon(Icons.volunteer_activism),
          Icon(Icons.volunteer_activism),
          Icon(Icons.volunteer_activism),
          Icon(Icons.volunteer_activism),
          Icon(Icons.volunteer_activism),
          Icon(Icons.volunteer_activism),
        ],
      ),
    );
  }
}

Paddiing 组件

在 html 中常见的布局标签都有 padding 属性,但是 Flutter 中很多 Widget 是没有 padding 属性。这个时候我们可以用 Padding 组件处理容器与子元素之间的间距。

属性说明
paddingpadding 值, EdgeInsetss 设置填充的值
child子组件

Stack、Align、Positioned 层叠布局

Alignment 的参数

dart
  /// The top left corner.
  static const Alignment topLeft = Alignment(-1.0, -1.0);

  /// The center point along the top edge.
  static const Alignment topCenter = Alignment(0.0, -1.0);

  /// The top right corner.
  static const Alignment topRight = Alignment(1.0, -1.0);

  /// The center point along the left edge.
  static const Alignment centerLeft = Alignment(-1.0, 0.0);

  /// The center point, both horizontally and vertically.
  static const Alignment center = Alignment(0.0, 0.0);

  /// The center point along the right edge.
  static const Alignment centerRight = Alignment(1.0, 0.0);

  /// The bottom left corner.
  static const Alignment bottomLeft = Alignment(-1.0, 1.0);

  /// The center point along the bottom edge.
  static const Alignment bottomCenter = Alignment(0.0, 1.0);

  /// The bottom right corner.
  static const Alignment bottomRight = Alignment(1.0, 1.0);

Stack 组件

Stack 表示堆的意思,我们可以用 Stack 或者 Stack 结合 Align 或者 Stack 结合 Positiond 来实现页面的定位布局

属性说明
alignment配置所有子元素的显示位置
children子组件

Stack Align

Align 组件可以调整子组件的位置 , Stack 组件中结合 Align 组件也可以控制每个子元素的显示位置

属性说明
alignment配置所有子元素的显示位置
child子组件
dart
class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
          width: 300,
          height: 400,
          color: Colors.green,
          child: const Stack(
            // alignment: Alignment.center,
            children: [
              Align(
                alignment: Alignment(1, 1),
                child: Icon(Icons.home, size: 40, color: Colors.white),
              ),
              Align(
                alignment: Alignment.center,
                child: Icon(Icons.search, size: 30, color: Colors.white),
              ),
              Align(
                alignment: Alignment.bottomRight,
                child: Icon(Icons.settings_applications,
                    size: 30, color: Colors.white),
              )
            ],
          )),
    );
  }
}

Stack Positioned

Stack 组件中结合 Positioned 组件也可以控制每个子元素的显示位置,Positione 必须是 Stack 的直接组件才有效果

属性说明
top子元素距离顶部的距离
bottom子元素距离底部的距离
left子元素距离左侧距离
right子元素距离右侧距离
child子组件
width组件的高度 (注意:宽度和高度必须是固定值,没法使用 double.infinity)
height子组件的高度

获取屏幕

FlutterMediaQuery 获取屏幕宽度和高度

dart
final size =MediaQuery.of(context).size;

组件的 build 方法中可以通过,=MediaQuery.of(context).size;

dart
Widget build(BuildContext context) {
  final size =MediaQuery.of(context).size;
  final width =size.width;
  final height =size.height;
}

实战定位布局

dart
import 'package:flutter/material.dart';

void main(List<String> args) {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});
  List<Widget> _TitleList() {
    List<Widget> list = [];
    for (int i = 0; i < 30; i++) {
      list.add(ListTile(
        title: Text("这是一个标题${i + 1} "),
      ));
    }
    return list;
  }

  @override
  Widget build(BuildContext context) {
    final size = MediaQuery.of(context).size;
    return Scaffold(
        appBar: AppBar(
          centerTitle: true,
          title: const Text('页面布局'),
        ),
        body: Stack(
          children: [
            ListView(
              children: _TitleList(),
            ),
            Positioned(
                left: 0,
                top: 0,
                width: size.width,
                height: 50,
                child: Container(
                    alignment: Alignment.center,
                    color: Colors.red,
                    child: const Text(
                      '我是标题头',
                      style: TextStyle(
                        color: Colors.white,
                      ),
                    )))
          ],
        ));
  }
}
image-20241229100235460

AspectRatio

AspectRatio 的作用是根据设置调整子元素 child 的宽高比。AspectRatio 首先会在布局限制条件允许的范围内尽可能的扩展,widget 的高度是由宽度和比率决定的,类似于 BoxFit 中的 contain,按照固定比率去尽量占满区域。如果在满足所有限制条件过后无法找到一个可行的尺寸,AspectRatio 最终将会去优先适应布局限制条件,而忽略所设置的比率。

属性说明
aspectRatio宽高比,最终可能不会根据这个值去布局,具体则要看综合因素,外层是否允许按照这种比率进行布局,这只是一个参考值
child子组件
dart
class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Center(
        child: Container(
            width: 300,
            // height: 300,
            color: Colors.green,
            child: AspectRatio(
              aspectRatio: 1.0 / 2.5,
              child: Container(
                color: Colors.red,
              ),
            )));
  }
}

Card 组件

Card 是卡片组件块,内容可以由大多数类型的 Widget 构成,Card 具有圆角和阴影,这让它看起来有立体感。

属性说明
margin外边距
child子组件
elevation阴影值的深度
color背景颜色
shadowColor阴影颜色
margin外边距
clipBehaviorclipBehavior 内容溢出的剪切方式 Clip.none 不剪切 Clip.hardEdge 裁剪但不应用抗锯齿 Clip.antiAlias 裁剪而且抗锯齿 Clip.antiAliasWithSaveLayer 带有抗锯齿的剪辑,并在剪辑之后立即保存 saveLayer
ShapeCard 的阴影效果,默认的阴影效果为圆角的长方形边。 shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(10)) ),
dart
class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: const [
        Card(
          child: ListTile(
            leading: Icon(Icons.account_circle),
            title: Text('我是隆海'),
            subtitle: Text('官网地址:ronhai.com'),
          ),
        ),
        Card(
          child: ListTile(
            leading: Icon(Icons.account_circle),
            title: Text('我是隆海'),
            subtitle: Text('官网地址:ronhai.com'),
          ),
        ),
        Card(
          child: ListTile(
            leading: Icon(Icons.account_circle),
            title: Text('我是隆海'),
            subtitle: Text('官网地址:ronhai.com'),
          ),
        ),
      ],
    );
  }
}

image-20241230090943178

dart
class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: const [
        Card(
          shape: const RoundedRectangleBorder(
              borderRadius: BorderRadius.all(Radius.circular(10))),
          // color: Colors.red,
          elevation: 20,
          margin: const EdgeInsets.all(10),
          child: Column(
            children: const <Widget>[
              ListTile(
                title: Text("小帮", style: TextStyle(fontSize: 28)),
                subtitle: Text("高级软件工程师"),
              ),
              Divider(),
              ListTile(
                title: Text("1234567890123"),
              ),
              ListTile(title: Text("四川成都"))
            ],
          ),
        ),
        Card(
          shape: const RoundedRectangleBorder(
              borderRadius: BorderRadius.all(Radius.circular(10))),
          // color: Colors.red,
          elevation: 20,
          margin: const EdgeInsets.all(10),
          child: Column(
            children: const <Widget>[
              ListTile(
                title: Text("小帮", style: TextStyle(fontSize: 28)),
                subtitle: Text("高级软件工程师"),
              ),
              Divider(),
              ListTile(
                title: Text("1234567890123"),
              ),
              ListTile(title: Text("四川成都"))
            ],
          ),
        )
      ],
    );
  }
}

image-20241230091658240

dart

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: [
        Card(
          elevation: 10,
          margin: const EdgeInsets.all(10),
          child: Column(
            children: [
              AspectRatio(
                aspectRatio: 20 / 9,
                child: Image.asset(
                  'images/bannar.jpg',
                  fit: BoxFit.cover,
                ),
              ),
              const ListTile(
                leading: Icon(Icons.home),
                title: Text('Ronhai'),
                subtitle: Text('ronhai.com'),
              ),
            ],
          ),
        ),
        Card(
          elevation: 10,
          margin: const EdgeInsets.all(10),
          child: Column(
            children: [
              AspectRatio(
                aspectRatio: 20 / 9,
                child: Image.asset(
                  'images/bannar.jpg',
                  fit: BoxFit.cover,
                ),
              ),
              const ListTile(
                leading: Icon(Icons.home),
                title: Text('Ronhai'),
                subtitle: Text('ronhai.com'),
              ),
            ],
          ),
        ),
      ],
    );
  }
}

image-20241230095911024

Wrap 组件

Wrap 可以实现流布局,单行的 Wrap 跟 Row 表现几乎一致,单列的 Wrap 则跟 Column 表现几乎一致。但 Row 与 Column 都是单行单列的,Wrap 则突破了这个限制,mainAxis 上空间不足时,则向 crossAxis 上去扩展显示

属性说明
direction主轴的方向,默认水平
alignment主轴的对其方式
spacing主轴方向上的间距
textDirection文本方向
verticalDirection定义了 children 摆放顺序,默认是 down,见 Flex 相关属性介绍。
runAlignmentrun 的对齐方式。run 可以理解为新的行或者列,如果是水平方向布局的话, run 可以理解为新的一行
runSpacing垂直间距

自定义 Button 按钮

dart
class MyButton extends StatelessWidget {
  // 自定义按钮
  final text;
  final void Function()? onPressed;
  const MyButton(this.text, {super.key, required this.onPressed});

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
        onPressed: onPressed,
        style: ButtonStyle(
            backgroundColor: WidgetStateProperty.all(Colors.black26),
            foregroundColor: WidgetStateProperty.all(Colors.white)),
        child: Text(text));
  }
}

用 Wrap 使用 Button

dart
class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});
  List<Widget> _initButton() {
    List<Widget> list = [];
    for (var i = 0; i < 30; i++) {
      list.add(MyButton('第${i}集', onPressed: () {}));
    }
    return list;
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
        padding: EdgeInsets.all(10),
        child: Wrap(
          spacing: 10, // 水平间距
          runSpacing: 20, // 垂直间距
          // textDirection: TextDirection.ltr, //文本方向
          // direction: Axis.vertical, //主轴的对其方式
          alignment: WrapAlignment.start,
          children: _initButton(),
        ));
  }
}

image-20241230112455733

做一个搜索页面

dart
import 'package:flutter/material.dart';

void main(List<String> args) {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(primarySwatch: Colors.blue),
      home: Scaffold(
          appBar: AppBar(
            leading: const Icon(Icons.chevron_left_outlined),
            centerTitle: true,
            title: const Text('搜索页面'),
            actions: const [
              const Icon(Icons.more_horiz),
            ],
          ),
          body: const LayoutDemo()),
    );
  }
}

class Button extends StatelessWidget {
  // 自定义按钮
  final text;
  final void Function()? onPressed;
  const Button(this.text, {super.key, required this.onPressed});

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
        onPressed: onPressed,
        style: ButtonStyle(
            backgroundColor: WidgetStateProperty.all(Colors.black26),
            foregroundColor: WidgetStateProperty.all(Colors.white)),
        child: Text(text));
  }
}

class LayoutDemo extends StatelessWidget {
  const LayoutDemo({super.key});

  @override
  Widget build(BuildContext context) {
    return Padding(
        padding: const EdgeInsets.all(10),
        child: ListView(
          children: [
            Text(
              '热搜',
              style: Theme.of(context).textTheme.headlineSmall,
            ),
            const Divider(),
            Wrap(
              spacing: 8.0, // 主轴(水平)方向间距
              runSpacing: 4.0, // 纵轴(垂直)方向间距
              children: [
                Button("女装", onPressed: () {}),
                Button("笔记本", onPressed: () {}),
                Button("玩具", onPressed: () {}),
                Button("文学", onPressed: () {}),
                Button("女装", onPressed: () {}),
                Button("时尚", onPressed: () {}),
                Button("女装", onPressed: () {}),
                Button("女装", onPressed: () {}),
              ],
            ),
            const SizedBox(
              height: 10,
            ),
            const Row(
              children: [
                Text('历史记录',
                    style:
                        TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
              ],
            ),
            const Divider(),
            const SizedBox(
              height: 10,
            ),
            const Column(
              children: [
                ListTile(
                  title: Text('女装'),
                ),
                Divider(),
                ListTile(
                  title: Text('笔记本'),
                ),
                Divider(),
              ],
            ),
            const SizedBox(
              height: 10,
            ),
            Padding(
              padding: const EdgeInsets.all(10.0),
              child: OutlinedButton.icon(
                  onPressed: () {},
                  label: const Text('清空历史记录'),
                  icon: const Icon(Icons.delete)),
            )
          ],
        ));
  }
}

image-20241230115002983

Scaffold

BottomNavigationBar

BottomNavigationBar 是底部导航条,可以让我们定义底部 Tab 切换,bottomNavigationBar 是 Scaffold 组件的参数。

属性名说明
itemsList 底部导航条按钮集合
iconSizeicon
currentIndex默认选中第几个
onTap选中变化回调函数
fixedColor选中的颜色
typeBottomNavigationBarType.fixed BottomNavigationBarType.shifting
dart
import 'package:flutter/material.dart';

void main(List<String> args) {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Flutter Demo'),
        ),
        body: const MyHomePage(),
        bottomNavigationBar: BottomNavigationBar(
          items: const [
            BottomNavigationBarItem(
              icon: Icon(Icons.home),
              label: 'Home',
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.business),
              label: 'Business',
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.school),
              label: 'School',
            ),
          ],
        ),
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  List<String> list = [];
  @override
  Widget build(BuildContext context) {
    return ListView(children: [
      Column(
        children: list.map((value) {
          return ListTile(
            title: Text('$value'),
          );
        }).toList(),
      ),
      const SizedBox(height: 20),
      Padding(
        padding: const EdgeInsets.all(40),
        child: ElevatedButton(
            onPressed: () {
              setState(() {
                list.add('Hello World');
              });
            },
            child: const Text('增加')),
      )
    ]);
  }
}

image-20241230151119798

实现页面切换

在 lib 目录下新建 pages 和 pages/tabs,tabs 下新建 home.dart,search.dart,profile.dart。pages 下新建 tabs.dart

main.dart

dart
import 'package:flutter/material.dart';
import './pages/tabs.dart';

void main(List<String> args) {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Tabs',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const Tabs(),
    );
  }
}

tabs.dart

dart
import 'package:flutter/material.dart';
import './tabs/home.dart';
import './tabs/search.dart';
import './tabs/profile.dart';
import './tabs/user.dart';

class Tabs extends StatefulWidget {
  const Tabs({super.key});

  @override
  State<Tabs> createState() => _TabsState();
}

class _TabsState extends State<Tabs> {
  int _currentIndex = 0;
  final List<Widget> _tabs = [
    const HomePage(),
    const SearchPage(),
    const ProfilePage(),
    const UserPage(),
  ];
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Tabs'),
      ),
      body: _tabs[_currentIndex],
      bottomNavigationBar: BottomNavigationBar(
          items: const [
            BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
            BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Search'),
            BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
            BottomNavigationBarItem(icon: Icon(Icons.people), label: 'User'),
          ],
          selectedItemColor: Colors.red,
          currentIndex: _currentIndex,
          type: BottomNavigationBarType.fixed, //超过三个tab时,需要设置type为fixed
          onTap: (index) {
            setState(() {
              _currentIndex = index;
            });
          }),
    );
  }
}

home.dart

dart
import 'package:flutter/material.dart';

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return const Center(
      child: Text('HomePage'),
    );
  }
}

search.dart

dart
import 'package:flutter/material.dart';

class SearchPage extends StatefulWidget {
  const SearchPage({super.key});

  @override
  State<SearchPage> createState() => _SearchPageState();
}

class _SearchPageState extends State<SearchPage> {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text('SearchPage'),
    );
  }
}

profile.dart

dart
import 'package:flutter/material.dart';

class ProfilePage extends StatefulWidget {
  const ProfilePage({super.key});

  @override
  State<ProfilePage> createState() => _ProfilePageState();
}

class _ProfilePageState extends State<ProfilePage> {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: const Text('ProfilePage'),
    );
  }
}

user.dart

dart
import 'package:flutter/material.dart';

class UserPage extends StatefulWidget {
  const UserPage({super.key});

  @override
  State<UserPage> createState() => _UserPageState();
}

class _UserPageState extends State<UserPage> {
  @override
  Widget build(BuildContext context) {
    return const Center(
      child: Text('UserPage'),
    );
  }
}

FloatingActionButton

FloatingActionButton 简称 FAB ,可以实现浮动按钮,也可以实现类似闲鱼 app 的底部凸起导航

常用属性

属性名称属性值
child子视图,一般为Icon,不推荐使用文字
tooltipFAB被长按时显示,也是无障碍功能
backgroundColor背景颜色
elevation未点击的时候的阴影
hignlightElevation点击时阴影值,默认12.0
onPressed点击事件回调
shape可以定义FAB的形状等
mini是否是mini类型默认false

咸鱼按钮效果

dart
import 'package:flutter/material.dart';
import './tabs/home.dart';
import './tabs/search.dart';
import './tabs/profile.dart';
import './tabs/user.dart';
import './tabs/settings.dart';

class Tabs extends StatefulWidget {
  const Tabs({super.key});

  @override
  State<Tabs> createState() => _TabsState();
}

class _TabsState extends State<Tabs> {
  int _currentIndex = 0;
  final List<Widget> _tabs = [
    const HomePage(),
    const SearchPage(),
    const SettingsPage(),
    const ProfilePage(),
    const UserPage(),
  ];
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Tabs'),
      ),
      body: _tabs[_currentIndex],
      bottomNavigationBar: BottomNavigationBar(
          items: const [
            BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
            BottomNavigationBarItem(icon: Icon(Icons.search), label: '搜索'),
            BottomNavigationBarItem(icon: Icon(Icons.settings), label: '设置'),
            BottomNavigationBarItem(icon: Icon(Icons.person), label: '轮廓'),
            BottomNavigationBarItem(icon: Icon(Icons.people), label: '用户'),
          ],
          selectedItemColor: Colors.red,
          currentIndex: _currentIndex,
          type: BottomNavigationBarType.fixed, //超过三个tab时,需要设置type为fixed
          onTap: (index) {
            setState(() {
              _currentIndex = index;
            });
          }),
      floatingActionButton: Container(
        height: 60,
        width: 60,
        padding: const EdgeInsets.all(4),
        margin: const EdgeInsets.only(top: 4),
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(30),
          color: Colors.white,
        ),
        child: FloatingActionButton(
          onPressed: () {
            setState(() {
              _currentIndex = 2;
            });
          },
          backgroundColor: _currentIndex == 2 ? Colors.red : Colors.white,
          child: Icon(Icons.add,
              color: _currentIndex == 2 ? Colors.white : Colors.black38),
        ),
      ),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
    );
  }
}
image-20241230155104179

Drawer

在 Scaffold 组件里面传入 drawer 参数可以定义左侧边栏,传入 endDrawer 可以定义右侧边栏。侧边栏默 认是隐藏的,我们可以通过手指滑动显示侧边栏,也可以通过点击按钮显示侧边栏。

DrawerHeader

常见属性

属性描述
decoration设置顶部背景颜色
child配置子元素
padding内边距
margin外边距

UserAccountsDrawerHeader

常见属性

属性描述
decoration设置顶部背景颜色
accountName账户名称
accountEmail账户邮箱
currentAccountPicture用户头像
otherAccountsPictures用来设置当前账户其他账户头像

AppBar

AppBar 自定义顶部按钮图标、颜色

属性描述
leading在标题前面显示的一个控件,在首页通常显示应用的 logo;在其他界面通常显示为返回按钮
title标题,通常显示为当前界面的标题文字,可以放组件
actions通常使用 IconButton 来表示,可以放按钮组
bottom通常放tabBar,标题下面显示一个 Tab 导航栏
backgroundColor导航背景颜色
iconTheme图标样式
centerTitle标题是否居中显示

TabBar

TabBar 常见属性

属性描述
tabs显示的标签内容,一般使用Tab对象,也可以是其他的Widget
controllerTabController对象
isScrollable是否可滚动
indicatorColor指示器颜色
indicatorWeight指示器高度
indicatorPadding底部指示器的Padding
indicator指示器decoration,例如边框等
indicatorSize指示器大小计算方式,TabBarIndicatorSize.label跟文字等宽,TabBarIndicatorSize.tab跟每个tab等宽
labelColor选中label颜色
labelStyle选中label的Style
labelPadding每个label的padding值
unselectedLabelColor未选中label颜色
unselectedLabelStyle未选中label的Style

Tabbar TabBarView

实现类似头条顶部导航

  1. 混入SingleTickerProviderStateMixin
dart
class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin{
  @override
  Widget build(BuildContext context) {
   return Scaffold(
      appBar: AppBar(
        title: const Text('TabBar TabBarView'),
      ),
      body: const Text('头条滑动导航'),
    );
  }
}
  1. 定义TabController
dart
class _HomePageState extends State<HomePage>
    with SingleTickerProviderStateMixin {
  late TabController _tabController;
  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 3, vsync: this);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('TabBar TabBarView'),
      ),
      body: const Text('头条滑动导航'),
    );
  }
}
  1. 配置TabBar和TabBarView
dart
class _HomePageState extends State<HomePage>
    with SingleTickerProviderStateMixin {
  late TabController _tabController;
  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 3, vsync: this);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('TabBar TabBarView'),
        bottom: TabBar(controller: _tabController, tabs: const [
          Tab(
            child: Text('关注'),
          ),
          Tab(
            child: Text('热门'),
          ),
          Tab(
            child: Text('视频'),
          ),
        ]),
      ),
      body: TabBarView(controller: _tabController, children: [
        ListView(
          children: [
            ListTile(
              leading: Icon(Icons.person),
              title: Text('我是关注列表'),
              onTap: () {},
            ),
          ],
        ),
        ListView(
          children: [
            ListTile(
              leading: Icon(Icons.person),
              title: Text('我是热门列表 1'),
              onTap: () {},
            ),
          ],
        ),
        ListView(
          children: [
            ListTile(
              leading: Icon(Icons.person),
              title: Text('我是视频列表 1'),
              onTap: () {},
            ),
          ],
        )
      ]),
    );
  }
}

image-20250101231149277

preferredSize 组件

PreferredSize 可以改变 appBar 的高度

dart
      appBar: PreferredSize(
        preferredSize: const Size.fromHeight(60),////设置tabbar的高度
        child: AppBar(
          title: const Text('TabBar TabBarView'),
          bottom: TabBar(controller: _tabController, tabs: const [
            Tab(
              child: Text('关注'),
            ),
            Tab(
              child: Text('热门'),
            ),
            Tab(
              child: Text('视频'),
            ),
            Tab(
              child: Text('推荐'),
            ),
          ]),
        ),
      ),

image-20250101231923169

KeepAliveWrapper 缓存页面

AutomaticKeepAliveClientMixin 可以快速的实现页面缓存功能,但是通过混入的方式实现不是很优 雅, 所以我们有必要对 AutomaticKeepAliveClientMixin 混入进行封装

keepAliveWrapper.dart

dart
import 'package:flutter/material.dart';

class KeepAliveWrapper extends StatefulWidget {
  const KeepAliveWrapper(
      {Key? key, @required this.child, this.keepAlive = true})
      : super(key: key);
  final Widget? child;
  final bool keepAlive;
  @override
  State<KeepAliveWrapper> createState() => _KeepAliveWrapperState();
}

class _KeepAliveWrapperState extends State<KeepAliveWrapper>
    with AutomaticKeepAliveClientMixin {
  @override
  Widget build(BuildContext context) {
    return widget.child!;
  }

  @override
  bool get wantKeepAlive => widget.keepAlive;
  @override
  void didUpdateWidget(covariant KeepAliveWrapper oldWidget) {
    if (oldWidget.keepAlive != widget.keepAlive) {
      // keepAlive 状态需要更新,实现在 AutomaticKeepAliveClientMixin 中
      updateKeepAlive();
    }
    super.didUpdateWidget(oldWidget);
  }
}

监听TabController改变事件

dart
  late TabController _tabController;
  @override
  void initState() {
    super.initState();
    _tabController = TabController(length: 4, vsync: this);
    //监听_tabController的改变事件
    _tabController.addListener(() {
      if (_tabController.animation!.value == _tabController.index) {
        print(_tabController.index); //获取点击或滑动页面的索引值
      }
    });
  }


  @override
  void dispose() {
    super.dispose();
    _tabController.dispose();
  }