作者:李俊才 (jcLee95):https://blog.csdn.net/qq_28550263
邮箱 :[email protected]
本文地址:https://blog.csdn.net/qq_28550263/article/details/134339945
【简介】也不需要什么简介,使用 Flutter 框架完成下面的响应式PC/Web端效果,你会怎么做呢:
本文就是介绍相关响应式上的一些工具和实践。
在 Flutter 中,你可以使用 MediaQuery、LayoutBuilder、AspectRatio 等Widget实现 尺寸测量与约束,进而创建响应式UI。例如,你可以使用 MediaQuery 来获取屏幕的尺寸和方向,然后根据这些信息来动态调整组件的布局和样式。你也可以使用 LayoutBuilder 来根据父组件的尺寸来调整子组件的布局。
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;
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。
AspectRatio 用于强制子组件具有特定的宽高比。AspectRatio 的构造函数接受一个 aspectRatio
参数,这个参数表示宽度和高度的比例。比如:
AspectRatio(
aspectRatio: 16 / 9,
child: ...,
)
我们实现响应式Header的基本思路是,根据屏幕的宽度来选择显示不同的顶部导航栏。整体上,依据这个
class ResponsiveHeaderNavbar extends StatelessWidget {
const ResponsiveHeaderNavbar({Key? key}) : super(key: key);
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});
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;
State<HoverTextWidget> createState() => _HoverTextWidgetState();
}
class _HoverTextWidgetState extends State<HoverTextWidget> {
// 定义一个状态变量,用于记录鼠标是否悬停在该组件上
bool isHovered = false;
// 定义一个方法,用于更新isHovered状态
void onHover(bool hover) {
setState(() {
isHovered = hover;
});
}
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});
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效果,它将在后面被我们的响应式页面所调用。
同样的思路,我们可以用以完成页面主体部分。
class ExampleHomePage extends StatelessWidget {
final String url = '/windows_page_view_2';
const ExampleHomePage({super.key});
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(),
),
],
),
),
],
);
}
},
),
),
],
),
),
],
),
);
}
}
效果如开头我所展示的那样: