前言
在 flutter 开发时,ListView 顶部偶尔会出现莫名其妙的空白区域,本文的目的主要是从以下几个方面来搞清楚其内部逻辑:
- 为什么在没有显式设置 padding 的情况下,ListView 仍然顶部留白?
MediaQuery.of(context).padding
到底做了什么?- 为什么Scaffold()设置了appBar()就正常?
ListView空白示例
-
在使用
ListView
开发页面时,顶部经常出现莫名其妙的空白区域。什么时候会有空白,什么时候又不会有空白,只有搞清楚了ListView内部发生了什么,才能避免类似的问题。lessreturn Scaffold( body: Column( children: [ SizedBox( height: MediaQuery.of(context).padding.top, ), Container( color: Colors.green, child: Text("ListView"), ), Expanded( child: ListView( children: List.generate( 1, (index) => LayoutBuilder(builder: (context, constraints) { print("constraints = $constraints"); return Container( height: 80, color: index.isEven ? Colors.blue : Colors.green, alignment: Alignment.center, child: Text('Item $index', style: const TextStyle(color: Colors.white)), ); }), ), )), ], ), );
Container()和ListView()没有设置间距,为什么 UI 展示出来有一段是空白的,为什么?
ListView 的buildSlivers代码
ListView
的父类是BoxScrollView
,在BoxScrollView
中有个buildSlivers
方法:
ini
@override
List<Widget> buildSlivers(BuildContext context) {
Widget sliver = buildChildLayout(context); // 构建核心的 SliverList/SliverChildBuilder
EdgeInsetsGeometry? effectivePadding = padding; // 用户是否手动传入了 padding?
if (padding == null) {
final MediaQueryData? mediaQuery = MediaQuery.maybeOf(context);
if (mediaQuery != null) {
// 👉 从 MediaQuery 中提取系统 padding(如状态栏、导航栏)
final EdgeInsets mediaQueryHorizontalPadding = mediaQuery.padding.copyWith(
top: 0.0,
bottom: 0.0,
);
//top还是取自mediaQuery
final EdgeInsets mediaQueryVerticalPadding = mediaQuery.padding.copyWith(
left: 0.0,
right: 0.0,
);
// 👉 根据滚动方向,决定主轴上需要应用的 padding
effectivePadding = scrollDirection == Axis.vertical
? mediaQueryVerticalPadding
: mediaQueryHorizontalPadding;
// 👉 剩下的 padding 留给子树继续使用(不影响主轴)
sliver = MediaQuery(
data: mediaQuery.copyWith(
padding: scrollDirection == Axis.vertical
? mediaQueryHorizontalPadding // 只保留横轴上的 padding
: mediaQueryVerticalPadding,
),
child: sliver,
);
}
}
// 👉 真正把 padding 应用到 Sliver 上(包一层 SliverPadding)
if (effectivePadding != null) {
sliver = SliverPadding(padding: effectivePadding, sliver: sliver);
}
return <Widget>[sliver];
}
- 如果未设置
padding
,ListView
会自动读取MediaQuery.of(context).padding
- 并通过内部的
SliverPadding
自动加到布局上
MediaQuery.of(context).padding
-
MediaQuery.of(context)
返回的是MediaQueryData
,其中包含.padding
、.viewInsets
、.viewPadding
。 -
.padding
/.viewPadding
/.viewInsets
的区别属性名 来源 包含内容 是否受键盘影响 常见用途 padding MediaQuery.of(context).padding viewPadding 减去 viewInsets(已被遮挡部分不计) ✅ 是 实际布局时要避让的系统 UI 区域 viewPadding MediaQuery.of(context).viewPadding 屏幕上永久性 UI 占用区域(状态栏、底部导航栏等) ❌ 否 获取物理安全区域,不受键盘弹出影响 viewInsets MediaQuery.of(context).viewInsets 临时被系统遮挡的区域(如软键盘) ✅ 是 避免被键盘遮挡的区域 -
padding 是实际需要避让的系统区域,设置 3 个值,主要是处理键盘的情况:
属性 状态栏高度 底部 Home 指示器 键盘弹出高度 最终值 viewPadding 44px 34px 无视键盘 top: 44, bottom: 34 viewInsets 0px 0px 300px(键盘) bottom: 300 padding viewPadding - viewInsets bottom: 0(因为被键盘挡住)
ListView的空白问题,主要是MediaQuery.of(context).padding
引起,而.padding
也取决手机刘海的高度。
如何调试 ListView 留白问题
- 只需要打印
MediaQuery.of(context).padding
,如果是 0 则是正常,否则就会留空白。
less
Builder(builder: (context) {
//top为0就是正常的
print("PaddingTop:${MediaQuery.of(context).padding.top}");
return ListView(
children: List.generate(
1,
(index) => LayoutBuilder(builder: (context, constraints) {
print("constraints = $constraints");
return Container(
height: 80,
color: index.isEven ? Colors.blue : Colors.green,
alignment: Alignment.center,
child: Text('Item $index',
style: const TextStyle(color: Colors.white)),
);
}),
),
);
}
开发时设置Scaffold() appBar()则正常,没设置就留白
原因是Scaffold()内部对appBar()进行判断,具体代码在ScaffoldState()的 buidl()方法:
php
_addIfNonNull(
children,
widget.body == null
? null
: _BodyBuilder(
extendBody: widget.extendBody,
extendBodyBehindAppBar: widget.extendBodyBehindAppBar,
body: KeyedSubtree(key: _bodyKey, child: widget.body!),
),
_ScaffoldSlot.body,
removeLeftPadding: false,
///如果widget.appBar != null,则removeTopPadding为 true
removeTopPadding: widget.appBar != null,
removeRightPadding: false,
removeBottomPadding:
widget.bottomNavigationBar != null || widget.persistentFooterButtons != null,
removeBottomInset: _resizeToAvoidBottomInset,
);
void _addIfNonNull(
List<LayoutId> children,
Widget? child,
Object childId, {
required bool removeLeftPadding,
required bool removeTopPadding,
required bool removeRightPadding,
required bool removeBottomPadding,
bool removeBottomInset = false,
bool maintainBottomViewPadding = false,
}) {
MediaQueryData data = MediaQuery.of(context).removePadding(
removeLeft: removeLeftPadding,
removeTop: removeTopPadding,
removeRight: removeRightPadding,
removeBottom: removeBottomPadding,
);
if (removeBottomInset) {
data = data.removeViewInsets(removeBottom: true);
}
if (maintainBottomViewPadding && data.viewInsets.bottom != 0.0) {
data = data.copyWith(padding: data.padding.copyWith(bottom: data.viewPadding.bottom));
}
if (child != null) {
children.add(LayoutId(id: childId, child: MediaQuery(data: data, child: child)));
}
}
所以很好的解释了设置appBar()就正常了。
解决方案:
- 配合
MediaQuery.removePadding()
控制 padding - Scaffold()设置 appBar()
总结
- ListView 顶部空白并非 bug,而是 Flutter 为了适配多种屏幕、安全区而做的"保护性设计"
- 真正掌握了其原理,能让我们开发出更稳定、适配性更强的 UI 页面
- 你还有遇到过哪些类似"看起来是 bug,实际上是设计"的 Flutter 问题?