基本页面布局
Scaffold 中有个 ListView,ListView 中有 100 个高 50 的 Container 用作辅助观看,ListView 中第三个元素是一个 GridView,GridView 的滑动效果被禁止。
class GiveView extends GetView<GiveController> {
const GiveView({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('GiveView'),
centerTitle: true,
),
body: ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
if (index == 3) {
return SizedBox(
height: 220,
child: GridView.builder(
itemCount: 10,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 5,
mainAxisExtent: 100,
crossAxisSpacing: 20,
mainAxisSpacing: 20,
),
itemBuilder: (BuildContext context, int index) {
return Image.network(
'https://img.xjh.me/random_img.php?return=302&time=${DateTime.now().microsecond}');
},
),
);
}
return Container(
margin: const EdgeInsets.symmetric(vertical: 5),
height: 50,
decoration: BoxDecoration(
border: Border.all(),
color: Colors.amber,
),
);
},
),
);
}
}

如上图所示,要探究的问题就以此为基础。
与 AppBar 的关系
目前为止,页面布局看着都很正常,没有问题。
去掉 AppBar
有时候是不需要 AppBar 的,比如:

去掉之后变成了如下摸样,明显不符合预期,上边的状态栏、刘海都空白了,没有完全占满。就好像 ListView 与最上边有了一个状态栏高度的距离,至于是 Margin,还是 Padding,又或者是一个空的 Container,继续向下查看。

通过查看 ListView 源码发现,在 packages/flutter/lib/src/widgets/scroll_view.dart:807 处发现会获取当前上下文的 mediaQuery 对象,并使用其中的 Padding。

ScrollView(ListView、GridView、BoxScrollView 的抽象类),在 build 时,会调用 buildSlivers,先看自身有没有设置 padding,如果设置了,那就用自己的。没设置就看 Widget 树中,最近的一个 MediaQuery 它的 padding 信息,以其为主。
找到了对应的代码逻辑,可以通过 Debug 和 DevTools 进行确认一下。
Debug 查看:

DevTools 查看:

由此也能看出,离 ListView 最近的一个 MediaQuery,其 Padding top 是 59,也就是状态栏的高度。
加上 AppBar
上面看到了去掉 AppBar 的情况,为了相互印证,把 AppBar 再加上进行查看。


Debug 查看:

DevTools 查看:

解决去掉 AppBar 的问题
方案 1
通过刚才的分析,现在已经很明白了,只需要给自己的 ScrollView 设置上 padding 即可,他就不会进入下面的代码:

最终代码:

效果:

完美,咦,不对,怎么中间的 GridView 的布局不对劲了?看着应该是离上边有个状态栏高度的 padding,真是“拆了东墙补西墙”。细细想想,把 ListView 的 padding 设置上了,它用自己的 padding 了,可是 GridView 也是 ScrollView 的实现,所以它也会走这块代码:

方案 2
GridView 可没有给他设置 padding,所以它还是用 Scaffold 下、GridView 上(Widget 树中)的那个 MediaQuery 中的 padding 信息,所以还是有个 top: 59 的距离,简单做法就是给 GridView 也设置上他自己的 padding,可是我已经烦了,难道每多一个 ScrollView 就要给他设置个 padding?
既然知道是 ScrollView 最近的 MediaQuery 导致的,那可以不可以一劳永逸,把这个最近的 MediaQuery 换了?当然可以,这里在 ListView 外边加一个自己的 MediaQuery:
Widget build(BuildContext context) {
return Scaffold(
// 手动增加一层 MediaQuery,默认的 padding 为 0
body: MediaQuery(
data: const MediaQueryData(),
child: ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
if (index == 3) {
return SizedBox(
height: 220,
child: GridView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: 10,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 5,
mainAxisExtent: 100,
crossAxisSpacing: 20,
mainAxisSpacing: 20,
),
itemBuilder: (BuildContext context, int index) {
return Image.network(
'https://img.xjh.me/random_img.php?return=302&time=${DateTime.now().microsecond}');
},
),
);
}
return Container(
margin: const EdgeInsets.symmetric(vertical: 5),
height: 50,
decoration: BoxDecoration(
border: Border.all(),
color: Colors.amber,
),
);
},
),
),
);
}
这样在 Scaffold 下、ListView 上就是自己创建的这个 MediaQuery 了。

还有另一种方式是使用 MediaQuery.removePadding 是一样的道理。
外部 ScrollView 使用 padding 与不使用的区别
恢复到自己插入 MediaQuery 之前的代码:
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
if (index == 3) {
return SizedBox(
height: 220,
child: GridView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: 10,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 5,
mainAxisExtent: 100,
crossAxisSpacing: 20,
mainAxisSpacing: 20,
),
itemBuilder: (BuildContext context, int index) {
return Image.network(
'https://img.xjh.me/random_img.php?return=302&time=${DateTime.now().microsecond}');
},
),
);
}
return Container(
margin: const EdgeInsets.symmetric(vertical: 5),
height: 50,
decoration: BoxDecoration(
border: Border.all(),
color: Colors.amber,
),
);
},
),
);
}

给 ListView 加上 padding:


这两个的区别就在于 ListView 加不加 padding。
- 没加:内部的 ScrollView 布局正常。
- 加了:内部的 ScrollView 布局异常,有一个高度为状态栏高度的 top padding。
那来分析分析原因,还得回到 ScrollView 的 buildSlivers 中(packages/flutter/lib/src/widgets/scroll_view.dart:803)。
前者

先说前者,ListView 没有设置 padding,所以会执行红框范围的代码,其中 819 行包裹了一层 MediaQuery,这就和上边自行加一层 MediaQuery 的效果是一样的了,后续在 ListView 中的 ScrollView 都将默认使用此 MediaQuery,所以布局没有问题。
后者
后者,由于 ListView 设置有 padding,就不会走红框范围的代码,那也就不会在 Widget 树中增加一个 MediaQuery,所以在 ListView 内部的 ScrollView 如果自己不设置 padding,还是会使用那个 padding top = 59 的 MediaQuery。



















