Flutter笔记:使用Flutter构建响应式PC客户端/Web页面-案例

Flutter笔记 使用Flutter构建响应式PC客户端/Web页面-案例


作者李俊才 (jcLee95)blog.csdn.net/qq_28550263
邮箱 : 291148484@163.com
本文地址blog.csdn.net/qq_28550263...


【简介】也不需要什么简介,使用 Flutter 框架完成下面的响应式PC/Web端效果,你会怎么做呢:

本文就是介绍相关响应式上的一些工具和实践。

目 录* * *


1. Flutter框架中的尺寸工具

Flutter 中,你可以使用 MediaQueryLayoutBuilderAspectRatio 等Widget实现 尺寸测量与约束 ,进而创建响应式UI。例如,你可以使用 MediaQuery 来获取屏幕的尺寸和方向,然后根据这些信息来动态调整组件的布局和样式。你也可以使用 LayoutBuilder 来根据父组件的尺寸来调整子组件的布局。

1.1 MediaQuery

MediaQuery 组件可以获取当前媒体(例如屏幕)的一些属性,如尺寸、方向、亮度等。你可以使用 MediaQuery.of(context) 来获取一个 MediaQueryData 对象,然后通过这个对象来获取媒体的属性。

例如,你可以使用以下代码来获取屏幕的宽度和高度:

arduino 复制代码
double screenWidth = MediaQuery.of(context).size.width;
double screenHeight = MediaQuery.of(context).size.height;

你也可以使用以下代码来判断当前是否为横屏:

ini 复制代码
bool isLandscape = MediaQuery.of(context).orientation == Orientation.landscape;

1.2 LayoutBuilder

LayoutBuilder 组件可以根据父组件的约束来生成不同的布局。

LayoutBuilder 的构造函数接受一个回调函数,这个回调函数有两个参数:BuildContextBoxConstraints 。 其中 BoxConstraints 对象描述了父组件对子组件的约束,如最大/最小宽度和高度。

例如,你可以使用以下代码来创建一个宽度为父Widget一半的子组件:

php 复制代码
LayoutBuilder(
  builder: (BuildContext context, BoxConstraints constraints) {
    return Container(
      width: constraints.maxWidth / 2,
      child: ...,
    );
  },
)

LayoutBuilderMediaQuery 都是 Flutter 中用于获取和处理布局信息的工具,但它们的用途和工作方式有所不同:

  • MediaQuery 主要用于获取媒体(通常是屏幕)的信息,如尺寸、方向、亮度等。你可以使用 MediaQuery 来创建响应式布局,即根据屏幕的尺寸和方向来调整布局。例如,你可以使用 MediaQuery 来获取屏幕的宽度,然后根据宽度来决定显示一个列布局还是行布局;
  • LayoutBuilder 主要用于获取父组件对子组件的约束,如最大/最小宽度和高度。你可以使用 LayoutBuilder 来创建自适应布局,即根据父组件的约束来调整子组件的布局。例如,你可以使用 LayoutBuilder来获取父组件的最大宽度,然后根据最大宽度来决定子组件的宽度。
  • MediaQuery 获取的是一个 静态的快照 ,它获取的屏幕信息在获取时就已经固定,不会随着屏幕尺寸的变化而变化。如果你需要响应屏幕尺寸的变化,你需要在屏幕尺寸变化时重新获取MediaQuery的信息。相比之下, LayoutBuilder 则是动态的,它会在父组件的约束变化时重新构建。这意味着如果父 组件 的尺寸变化了,LayoutBuilder 会自动重新获取约束并重新构建子组件。

可见,如果你需要创建一个能够响应屏幕尺寸变化的布局,你可能需要使用 LayoutBuilder 。如果你只需要获取屏幕的信息,并不需要响应屏幕尺寸的变化,那么 MediaQuery 可能更适合你;反之,则需要使用 LayoutBuilder

1.3 AspectRatio

AspectRatio 用于强制子组件具有特定的宽高比。AspectRatio 的构造函数接受一个 aspectRatio参数,这个参数表示宽度和高度的比例。比如:

less 复制代码
AspectRatio(
  aspectRatio: 16 / 9,
  child: ...,
)

2. 实践:构建响应式Header

我们实现响应式Header的基本思路是,根据屏幕的宽度来选择显示不同的顶部导航栏。整体上,依据这个

scala 复制代码
class ResponsiveHeaderNavbar extends StatelessWidget {
  const ResponsiveHeaderNavbar({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        // 使用LayoutBuilder来获取父Widget的尺寸约束
        // 如果最大宽度小于800像素,返回垂直布局的顶部导航栏
        if (constraints.maxWidth < 800) {
          return const VerticalTopNav();
        } 
        // 否则,返回原始的顶部导航栏
        else {
          return const OriginalTopNav();
        }
      },
    );
  }
}

在这个组件中,LayoutBuilder被用来获取父Widget的尺寸约束,然后根据最大宽度来选择显示哪种顶部导航栏。如果最大宽度小于800像素,就显示垂直布局的顶部导航栏;否则,就显示原始的顶部导航栏。这样,无论屏幕的尺寸如何变化,都能保证顶部导航栏的布局适应屏幕尺寸。

接下来,我们该具体实现 VerticalTopNavOriginalTopNav了。

先看 OriginalTopNav 部分:

scss 复制代码
class OriginalTopNav extends StatelessWidget {
  const OriginalTopNav({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.black,
      child: const Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          HoverTextWidget(66),
          HoverTextWidget(66),
          HoverTextWidget(66),
          HoverTextWidget(66),
          Spacer(),
          HoverTextWidget(66),
        ],
      ),
    );
  }
}

其中的文本组件HoverTextWidget代码为:

scala 复制代码
class HoverTextWidget extends StatefulWidget {
  const HoverTextWidget(this.width, {Key? key}) : super(key: key);
  final double width;

  @override
  State<HoverTextWidget> createState() => _HoverTextWidgetState();
}

class _HoverTextWidgetState extends State<HoverTextWidget> {
  // 定义一个状态变量,用于记录鼠标是否悬停在该组件上
  bool isHovered = false;

  // 定义一个方法,用于更新isHovered状态
  void onHover(bool hover) {
    setState(() {
      isHovered = hover;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MouseRegion(
      // 当鼠标进入该组件时,调用onHover方法并传入true
      onEnter: (_) => onHover(true),
      // 当鼠标离开该组件时,调用onHover方法并传入false
      onExit: (_) => onHover(false),
      // 根据isHovered状态来改变鼠标光标,如果isHovered为true,光标为click,否则为basic
      cursor: isHovered ? SystemMouseCursors.click : SystemMouseCursors.basic,
      child: GestureDetector(
        child: Container(
          height: 40,
          alignment: Alignment.center,
          // 根据isHovered状态来改变背景颜色,如果isHovered为true,背景颜色为grey,否则为black
          color: isHovered ? Colors.grey : Colors.black,
          width: widget.width,
          child: Text(
            '链接',
            style: TextStyle(
              // 根据isHovered状态来改变文字颜色,如果isHovered为true,文字颜色为black,否则为white
              color: isHovered ? Colors.black : Colors.white,
            ),
          ),
        ),
      ),
    );
  }
}

似乎没什么难点需要说明的,那就接着 VelticalTopNav 部分:

less 复制代码
class VelticalTopNav extends StatelessWidget {
  const VelticalTopNav({super.key});

  @override
  Widget build(BuildContext context) {
    // return const LinkedTabGroup(Axis.vertical);
    return ExpansionTile(
      // title: Text(''),
      title: SvgPicture.asset(
        '/assets/svgs/jcstudio-v2-color.svg',
        width: 50, // 设置宽度
        height: 50,
      ),
      leading: const Icon(Icons.menu, color: Colors.amber),
      backgroundColor: Colors.black,
      collapsedBackgroundColor: Colors.black,
      collapsedIconColor: Colors.amber,
      collapsedTextColor: Colors.amber,
      children: [
        ListTile(
          title: const Text(
            '链接',
            style: TextStyle(color: Colors.white),
          ),
          onTap: () {
            // 处理子菜单项点击
          },
        ),
        ListTile(
          title: const Text(
            '链接',
            style: TextStyle(color: Colors.white),
          ),
          onTap: () {
            // 处理子菜单项点击
          },
        ),

        ListTile(
          title: const Text(
            '链接',
            style: TextStyle(color: Colors.white),
          ),
          onTap: () {
            // 处理子菜单项点击
          },
        ),
        ListTile(
          title: const Text(
            '链接',
            style: TextStyle(color: Colors.white),
          ),
          onTap: () {
            // 处理子菜单项点击
          },
        ),
      ],
    );
  }
}

这样就完成了整体效果中的Header效果,它将在后面被我们的响应式页面所调用。

4. 实践:完成一个响应式Web骨架

同样的思路,我们可以用以完成页面主体部分。

less 复制代码
class ExampleHomePage extends StatelessWidget {
  final String url = '/windows_page_view_2';
  const ExampleHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
         // 调用上一节写好的 Header(appbar)部分
          const ResponsiveHeaderNavbar(),
          Expanded(
            child: CustomScrollView(
              slivers: [
                SliverAppBar(
                  automaticallyImplyLeading: false,
                  pinned: true,
                  snap: false,
                  floating: false,
                  expandedHeight: 460.0,
                  flexibleSpace: FlexibleSpaceBar(
                    background: Image.asset(
                      'assets/images/it_curtoon_1.png',
                      fit: BoxFit.cover,
                    ),
                    title: const Text('flutter-online.top'),
                  ),
                ),
				// 同样的思路在这里再来一次
                SliverToBoxAdapter(
                  child: LayoutBuilder(
                    builder: (context, constraints) {
                      if (constraints.maxWidth < 800) {
                        // 当屏幕宽度小于800像素时,改为上下布局
                        return const Column(
                          children: [
                            Part1(),
                            Part2(),
                          ],
                        );
                      } else {
                        // 屏幕宽度大于等于800像素时,保持原有布局
                        return Column(
                          children: [
                            Container(
                              padding: const EdgeInsets.only(
                                  left: 100, right: 100, top: 30),
                              child: const Row(
                                children: [
                                  Expanded(
                                    child: Part1(),
                                  ),
                                  SizedBox(
                                    width: 260,
                                    child: Part2(),
                                  ),
                                ],
                              ),
                            ),
                          ],
                        );
                      }
                    },
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

效果如开头我所展示的那样:

相关推荐
火柴就是我15 小时前
flutter 之真手势冲突处理
android·flutter
Speed12315 小时前
`mockito` 的核心“打桩”规则
flutter·dart
法的空间15 小时前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
恋猫de小郭16 小时前
Android 将强制应用使用主题图标,你怎么看?
android·前端·flutter
玲珑Felone16 小时前
从flutter源码看其渲染机制
android·flutter
ALLIN2 天前
Flutter 三种方式实现页面切换后保持原页面状态
flutter
Dabei2 天前
Flutter 国际化
flutter
Dabei2 天前
Flutter MQTT 通信文档
flutter
Dabei2 天前
Flutter 中实现 TCP 通信
flutter
孤鸿玉2 天前
ios flutter_echarts 不在当前屏幕 白屏修复
flutter