Flutter笔记使用Flutter构建响应式PC客户端/Web页面-案例
1. Flutter框架中的尺寸工具
在 Flutter 中,你可以使用 MediaQuery、LayoutBuilder、AspectRatio 等Widget实现 尺寸测量与约束,进而创建响应式UI。例如,你可以使用 MediaQuery 来获取屏幕的尺寸和方向,然后根据这些信息来动态调整组件的布局和样式。你也可以使用 LayoutBuilder 来根据父组件的尺寸来调整子组件的布局。
1.1 MediaQuery
MediaQuery 组件可以获取当前媒体(例如屏幕)的一些属性,如尺寸、方向、亮度等。你可以使用 MediaQuery.of(context) 来获取一个 MediaQueryData 对象,然后通过这个对象来获取媒体的属性。
例如,你可以使用以下代码来获取屏幕的宽度和高度:
double screenWidth = MediaQuery.of(context).size.width; double screenHeight = MediaQuery.of(context).size.height;
你也可以使用以下代码来判断当前是否为横屏:
bool isLandscape = MediaQuery.of(context).orientation == Orientation.landscape;
1.2 LayoutBuilder
LayoutBuilder 组件可以根据父组件的约束来生成不同的布局。
LayoutBuilder 的构造函数接受一个回调函数,这个回调函数有两个参数:BuildContext 和 BoxConstraints。 其中 BoxConstraints 对象描述了父组件对子组件的约束,如最大/最小宽度和高度。
例如,你可以使用以下代码来创建一个宽度为父Widget一半的子组件:
LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return Container( width: constraints.maxWidth / 2, child: ..., ); }, )
LayoutBuilder 和 MediaQuery 都是 Flutter 中用于获取和处理布局信息的工具,但它们的用途和工作方式有所不同:
- MediaQuery 主要用于获取媒体(通常是屏幕)的信息,如尺寸、方向、亮度等。你可以使用 MediaQuery 来创建响应式布局,即根据屏幕的尺寸和方向来调整布局。例如,你可以使用 MediaQuery 来获取屏幕的宽度,然后根据宽度来决定显示一个列布局还是行布局;
- LayoutBuilder主要用于获取父组件对子组件的约束,如最大/最小宽度和高度。你可以使用 LayoutBuilder 来创建自适应布局,即根据父组件的约束来调整子组件的布局。例如,你可以使用 LayoutBuilder来获取父组件的最大宽度,然后根据最大宽度来决定子组件的宽度。
- MediaQuery 获取的是一个 静态的快照,它获取的屏幕信息在获取时就已经固定,不会随着屏幕尺寸的变化而变化。如果你需要响应屏幕尺寸的变化,你需要在屏幕尺寸变化时重新获取MediaQuery的信息。相比之下, LayoutBuilder 则是动态的,它会在父组件的约束变化时重新构建。这意味着如果父 组件 的尺寸变化了,LayoutBuilder 会自动重新获取约束并重新构建子组件。
可见,如果你需要创建一个能够响应屏幕尺寸变化的布局,你可能需要使用 LayoutBuilder。如果你只需要获取屏幕的信息,并不需要响应屏幕尺寸的变化,那么 MediaQuery 可能更适合你;反之,则需要使用 LayoutBuilder。
1.3 AspectRatio
AspectRatio 用于强制子组件具有特定的宽高比。AspectRatio 的构造函数接受一个 aspectRatio
参数,这个参数表示宽度和高度的比例。比如:
AspectRatio( aspectRatio: 16 / 9, child: ..., )
2. 实践:构建响应式Header
我们实现响应式Header的基本思路是,根据屏幕的宽度来选择显示不同的顶部导航栏。整体上,依据这个
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像素,就显示垂直布局的顶部导航栏;否则,就显示原始的顶部导航栏。这样,无论屏幕的尺寸如何变化,都能保证顶部导航栏的布局适应屏幕尺寸。
接下来,我们该具体实现 VerticalTopNav 和 OriginalTopNav了。
先看 OriginalTopNav 部分:
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代码为:
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 部分:
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骨架
同样的思路,我们可以用以完成页面主体部分。
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(), ), ], ), ), ], ); } }, ), ), ], ), ), ], ), ); } }
效果如开头我所展示的那样: