Day 2。前几天开始认真过某系统的 Flutter 跨端布局,今天正好复习到 Stack 和 Positioned,顺手把笔记整理成一篇。说个很常见的问题:头像右下角要挂一个"未读红点",Row 和 Column 能做到吗?做不到,因为这两个组件只会让子元素排队,一个往左排一个往下排,真正要"叠"上去的时候,必须用层叠布局。这篇就把 Stack 的尺寸逻辑、Positioned 的坐标规则、fit/alignment/clipBehavior 三个容易出鬼的参数,以及三个能直接抄的实战布局一次讲清楚。
1. 先从"叠在一起"说起:Stack 的图层思维和尺寸规则
1.1 你真正需要层叠布局的三种场景
先说结论:只要界面上有"A 压在 B 上面"的需求,第一反应就应该是 Stack。我归纳下来,业务里最常见的有三种。
第一种是角标类。头像右下角加红点、商品图上角落加折扣标签、等级徽章叠在头像左上角。第二种是覆盖层类。视频播放器上面叠控制栏、图片上面叠半透明遮罩和说明文字、地图上叠悬浮按钮。第三种是浮动操作类。列表页右下角的悬浮按钮、直播间的礼物入口、客服气泡,这些组件需要"钉"在某个固定位置,而且还要盖住滚动内容。
这三种场景的本质是同一个:多个组件不再线性排列,而是在同一块画布上互相覆盖。Stack 在 Flutter 里干的就是这件事,它把每一个直接子组件当作一个图层,子组件的先后顺序就是图层的前后顺序——准确地说,children 列表里越靠后的组件,画出来的时候越靠上。你可以把它理解成 PS 里的图层,也可以理解成前端里的绝对定位,坐标原点在左上角,靠左是 x 正方向,靠下是 y 正方向。
1.2 Stack 和 Row/Column 的本质差异:排队的尽头是叠放
Row 和 Column 的布局思路是"排队":一个往水平方向排,一个往垂直方向排,子组件会彼此错开,不会重叠。Stack 的思路完全不同,它给所有子组件提供同一个"画布",不排队,默认往左上角堆叠。如果你的孩子列表里放了几个普通子组件又没指定位置,它们会整整齐齐地叠在左上角,这是新手第一个会懵的地方。
我说一个比较容易理解的角度:Row/Column 解决的是"多个兄弟组件怎么排列",Stack 解决的是"多个兄弟组件怎么叠起来"。当你把两个有一定尺寸的 Container 放进 Stack,不写 Positioned,看到的会是后一个挡着前一个、都出现在左上角;但如果你在身体里给它们各自加 Positioned,就能把其中一个挪到右上角、另一个留在左下角。
还有一个特性值得记牢:Stack 的 z-order 完全由 children 顺序决定,没有类似 "zIndex" 的属性。想置顶就把那个组件放到 children 的最后面,想垫底就放最前面。后面讲手势穿透时还会再回到这个顺序。
1.3 Stack 的尺寸到底由谁决定:这是被问得最多的一个点
这是我想重点展开的。很多人写 Positioned 的时候定位不准,往往不是坐标算错,而是没搞懂 Stack 本身多大。
Stack 的尺寸分两种情况。一种情况是 Stack 被放进了有 tight 约束的地方,比如直接作为 Scaffold 的 body。这时候父级强制要求 Stack 填满屏幕,Stack 的大小就是整个可用区域,Positioned 里随便写 left/right/top/bottom 都相对这块大画布计算,行为很直观。另一种情况是 Stack 被放在宽松环境里,比如放在 Row 里面或者被 Center 包着,这时 Stack 的尺寸默认由"非定位子组件中尺寸最大的那一个"决定,Positioned 子组件不会参与撑大 Stack。
这个规则会带来什么坑?假设你写了 Stack(children:[Positioned(left:0, top:0, child: Text('x'))]),里面只有一个定位组件,而没有普通子组件。在宽松环境下,这个 Stack 的尺寸可能趋近于零,因为没有任何非定位子组件给它一个"基准大小",Positioned 孩子本身不贡献尺寸。结果坐标可能看起来"没生效"或者被裁掉。反直觉的地方在于,同样这段代码放进 Scaffold 的 body,又能正常定位,因为 body 约束又把 Stack 撑满了。
所以我的建议是:在你能确定 Stack 需要多大时,要么给它外层包一个 SizedBox 指定宽高,要么给它一个会撑起尺寸的非定位背景子组件,要么让它处在 tight 约束下。别让 Stack 处在一个"自身尺寸不确定"的状态里,这是层叠布局一系列怪问题的最常见来源。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Positioned:四个锚点、两种拉伸和一种"填满"魔法
2.1 top/left/right/bottom 到底是相对谁的偏移
Positioned 是 Stack 里专门负责"钉位置"的子组件,它必须在 Stack 中使用,写在别的地方会直接报错。它接收的对象是 left、top、right、bottom 这四边的偏移,本质是相对 Stack 四条内边界的距离。
举个例子:Stack 尺寸是 200x200,里面放一个 40x40 的白盒子,Positioned(left: 20, top: 20) 后,这个白盒子的左上角落在 (20, 20) 这个坐标上。这个例子看起来简单,但很多人会把 right 理解成"组件右边缘的 x 坐标",其实不对。right: 20 表示组件右边距 Stack 右边 20 像素,也就是说组件会被往左推,最终它的右边缘在 Stack 宽减 20 的位置。同理 bottom: 20 表示组件底边距 Stack 底边 20。
还有一点容易忽略:这些偏移都是相对 Stack,不是相对屏幕。如果 Stack 只占屏幕左半部分,那 Positioned(right: 0) 出来的"右边",也只是 Stack 的右边界,不是屏幕边缘。这点在做跨端适配时特别重要,后面第 5 章会单独说。
2.2 左右都写、上下都写的拉伸效果
Positioned 另一个好玩的特性是,"对边同时给"会产生拉伸。只给 left 不给 width 和 right,组件会保持自己的宽度;一旦 left 和 right 同时给了,宽度就被强行拉伸成"Stack 宽度减去左右偏移之和"。上下同理,top 和 bottom 同时给的时候高度被拉伸。
这个特性非常实用。比如你想在 Stack 顶部画一条横贯的分割线,线宽只留左右各 16 像素边距,就可以写 Positioned(left: 16, right: 16, top: 10, child: Container(height: 1, color: Colors.grey))。系统会自动把这条线从左边距拉到右边距,比手动算宽度省事很多。
这里有个和新手易踩的坑:有些人习惯在 Stack 里用 Expanded 或 Flexible 来控制某个孩子的尺寸,报错之后才发现 Stack 不是 Flex 家族,不支持这两个组件。Stack 里想"拉伸充满"的正确手段,就是用 Positioned 同时给定对边,或者直接用下面要说的 Positioned.fill。
2.3 一招搞定通栏遮罩:Positioned.fill
Positioned.fill 本质上就是"四边全部归零"的快捷写法,子组件会被强制填满整个 Stack。源码内部就是 Positioned(left: 0, top: 0, right: 0, bottom: 0, child: ...),所以它也可以接受带边距的参数,比如 Positioned.fill(left: 24) 表示四个方向基本填满,但左侧留 24 像素。
它最常见的用途是给底层内容加遮罩。比如视频卡片上盖一层从透明到黑色的渐变,让底部的文字更清晰,这样的遮罩组件就可以直接放在 Stack 里,配合 Container 的 gradient 或半透明色。相比手动写 Positioned(left: 0, right: 0, top: 0, bottom: 0),fill 可读性更高,强烈推荐日常使用。
2.4 扩展:PositionedDirectional 和百分比定位
最后提两个进阶手段。一个是 PositionedDirectional,它和 Positioned 的区别在于,它接收的是 start/end 而不是 left/right,会跟随语言方向(从左到右还是从右到左)。如果你的应用要适配阿拉伯语文案之类的 RTL 场景,用它更稳妥;只做中文和英文界面的话,Positioned 完全够用。
另一个是百分比定位。Positioned 本身不支持百分比,但你可以用 LayoutBuilder 拿到父级尺寸,再乘比例去计算偏移,或者更省事的是用 Align + Alignment 坐标。Stack 里也可以用 Align 做子组件,比如 Align(alignment: Alignment(0.5, 0.5)) 实现居中偏右下。不过凡是要求左右边距精确的场景,直接用 Positioned 还是最不容易出错的。
3. Stack 三个容易"出鬼"的参数:fit、alignment、clipBehavior
3.1 StackFit.expand 让背景色铺满,loose 和 passthrough 又是什么
Stack 的 fit 参数我一开始没认真看,后来在项目里频繁遇到"背景没铺满"才发现它的重要性。它有三个值:StackFit.loose、StackFit.expand、StackFit.passthrough。默认是 loose。
三者区别我整理成了表格:
| 取值 | 约束行为 | 典型场景 |
|---|---|---|
| StackFit.loose | 给非定位子组件宽松的约束,允许组件保持自身尺寸,但不能超过 Stack 范围 | 普通堆叠、角标场景 |
| StackFit.expand | 强制所有非定位子组件填满 Stack,忽略子组件自身尺寸 | 背景层、全屏遮罩 |
| StackFit.passthrough | 把 Stack 收到的约束原样透传给非定位子组件,由子组件自行决定尺寸 | 少用,特殊自定义布局 |
现在可以串起前面 1.3 节的知识点了。如果 Stack 在 tight 约束下,fit 影响的主要是"非定位子组件"能拿到多大的约束;如果 Stack 在宽松环境里,expand 会让非定位子组件尽量撑满 Stack 的可用区域。
我在代码里最常用的是 StackFit.expand 加一个不带 Positioned 的 Container 做背景层。这样只需一行 Container(color: Colors.black) 就能铺满整个卡片,完全不用再写 Positioned.fill。注意,expand 只影响非定位子组件,对已经包了 Positioned 的组件是没有作用的。
3.2 alignment 是"所有非定位孩子的默认落位点"
Stack 的 alignment 参数决定的是"所有没有用 Positioned 包裹的子组件"的默认对齐方式。它的默认值是 AlignmentDirectional.topStart,也就是在从左到右的阅读方向下靠左上角对齐,这个默认值坑了很多人——放三个普通 Container 进去,全挤在左上角。
alignment 是一个"群体默认值",不是"每个子组件独立对齐"。你要是想让其中一个普通子组件独自分到右下角,单改 Stack.alignment 是做不到的,因为它是统一作用给所有非定位子组件的。哪个组件想单独偏移,正确的做法是给它包一个 Align,或者直接用 Positioned。
Positioned 的子组件不受 alignment 影响。这一点务必记牢,因为它很容易让人产生误解:Positioned(left:10, top:10) 明明指定了坐标,为什么还要受 Stack.alignment 控制?答案是不受,有了 Positioned 之后坐标完全由自己的参数决定。
3.3 clipBehavior 默认会裁掉溢出内容,"溢出"不等于"屏幕外"
Stack 默认的 clipBehavior 是 Clip.hardEdge,意思是所有超出 Stack 边界的绘制内容都会被裁剪。听到"裁剪"别紧张,大多数情况下这是合理的,尤其是你想保证内容严格控制在圆角卡片内部的时候。
但它会给一类场景造成困扰:元素故意要"画出边界"的时候。典型的例子就是第 1 章提到的角标红点,我希望红点有一半探出头像右上角,但如果 Stack 默认裁剪,探出的那部分就会消失。解决方式是在 Stack 上显式设置 clipBehavior: Clip.none,让它别裁。
另一个容易被裁的是阴影。某些场景下子组件开启了 BoxShadow,阴影本来会溢出一段距离,默认裁剪就看不到了,当时我在排查"怎么阴影不见了"时花了半天,最后才发现是 Stack 把阴影裁了。所以记住:需要溢出阴影、光晕、角标突出、旋转动画位移的时候,把 clipBehavior 改成 none。
这里有句提醒:打开 Clip.none 之后,超出部分确实不再裁剪,但也意味着绘制范围变大了,对性能有一点点影响,代价通常可以接受。而如果你确实需要裁剪,比如圆角头像的 ClipRRect 里包 Stack,那就保留默认裁剪。
4. 三个实战布局:照着敲就能跑
4.1 头像角标:红点、等级徽章和"溢出边缘"的处理
先来一个步骤化的小例子,完整实现"头像 + 右上角红点 + 左下角等级标签"。红点我特意做了探出边缘的样式,这是大多数社交 App 的小设计,能明显区分"有新动态"和"只是普通挂件"。
dart复制import 'package:flutter/material.dart';
class AvatarWithBadge extends StatelessWidget {
const AvatarWithBadge({super.key});
@override
Widget build(BuildContext context) {
return Stack(
// 关键:红点要探出头像边缘,必须关掉裁剪
clipBehavior: Clip.none,
children: [
const CircleAvatar(
radius: 36,
backgroundColor: Colors.blueGrey,
child: Icon(Icons.person, size: 40, color: Colors.white),
),
// 未读红点:故意覆盖到头像边界外
Positioned(
right: -2,
top: -2,
child: Container(
width: 18,
height: 18,
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
),
),
// 等级徽章
Positioned(
left: 0,
bottom: 0,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.orange,
borderRadius: BorderRadius.circular(10),
),
child: const Text(
'Lv.3',
style: TextStyle(color: Colors.white, fontSize: 10),
),
),
),
],
);
}
}
这个例子里有三个细节重点。第一,红点的 right 和 top 传了负值,让 Red Dot 往右上角外探出;如果不设置 clipBehavior: Clip.none,那多出来的部分会被 Stack 裁掉。第二,红点外层的白边是 Border.all,它是用来模拟"头像边缘被描边"的效果,视觉上更像原生角标。第三,等级徽章用了左对齐加 bottom: 0,如果你想放在右下角,改成 right: 0, bottom: 0 就行,位置调整非常灵活。
4.2 视频卡片控制层:渐变遮罩、返回按钮、标题和播放键
第二个例子做一个视频卡片的控制层。需求是:背景是一张封面图,图上覆盖一层从透明到黑色的渐变,左上角是返回按钮,左下角是标题和播放量,右下角是一个小播放按钮。
dart复制class VideoCard extends StatelessWidget {
const VideoCard({super.key});
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(12),
child: SizedBox(
width: double.infinity,
height: 220,
child: Stack(
// 铺满模式,让下面所有非定位子组件尽量占满整个卡片
fit: StackFit.expand,
children: [
// 封面层,真实项目里会用 Image.network
Container(color: Colors.blueGrey),
// 渐变遮罩层
const DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.transparent, Colors.black54],
),
),
),
// 返回按钮
Positioned(
left: 12,
top: 12,
child: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () {},
),
),
// 底部信息
Positioned(
left: 16,
bottom: 16,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: const [
Text(
'一个视频标题',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 4),
Text(
'1.2万次播放',
style: TextStyle(color: Colors.white70, fontSize: 12),
),
],
),
),
// 右下角播放按钮
Positioned(
right: 16,
bottom: 16,
child: FloatingActionButton.small(
onPressed: () {},
child: const Icon(Icons.play_arrow),
),
),
],
),
),
);
}
}
你注意我这里用了 StackFit.expand,然后渐变遮罩层直接是 DecoratedBox 作为非定位子组件。expand 会强制它填满整个卡片,省掉了 Positioned.fill 的写法。返回按钮、标题、播放按钮全部用 Positioned 钉在四个方向,互不影响,图层顺序上后写的播放按钮盖在最上面。这个结构也是很多真实视频卡片的标准骨架,理解了它之后,直播间的礼物列表、电商商品的促销角标都是同一个套路。
4.3 列表页右下角悬浮按钮:Stack + ListView 的写法
很多人觉得悬浮按钮只能靠 Scaffold 的 floatingActionButton,其实用 Stack 也能做,而且自由度更高。我写过一个需求是按钮要悬浮在"商品详情页右下角,但离底部还有一段固定距离",用系统 FAB 还得额外调整。Stack 方案非常直接:Stack 包住 ListView,再用 Positioned 钉按钮。
dart复制class ListWithFab extends StatelessWidget {
const ListWithFab({super.key});
final List<String> items = List.generate(20, (i) => '条目 $i');
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
ListView.builder(
// 关键:底部 padding 要留出按钮的空间,否则最后一项会被挡住
padding: const EdgeInsets.fromLTRB(16, 16, 16, 80),
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(title: Text(items[index]));
},
),
// 悬浮按钮
Positioned(
right: 16,
bottom: 16,
child: FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),
),
],
),
);
}
}
相比 Scaffold 自带的 FAB,这个写法的好处有两个:第一个是任何地方都能用,不依赖 Scaffold,比如在自定义弹窗或小组件的内部;第二个是位置控制特别自由,你可以把按钮放在列表中部右侧、或者离底部更远,还能在 Stack 里塞多个按钮做组合。但代价是,你需要手动给 ListView 留出底部 padding,不然最后一条内容会被按钮盖住,这里我设置了底部 80 像素,一般足够放一个标准 FAB 加边距。
5. 图层多了之后:手势、滚动和绘制性能的进阶排雷
5.1 透明遮罩为什么会"吃掉"点击事件
关于 Stack 的手势命中,有一个非常经典的问题:为什么我加了一层透明的遮罩,下面的按钮就点不动了?这要理解 Flutter 的 hit test 顺序:Stack 会从 children 列表的最后一个组件(也就是最上层)开始,反向寻找第一个"能命中"的组件。如果上层是一个 Container,哪怕它是透明的、没有任何点击处理,它依然可能参与命中测试,把下层按钮的事件挡住。
解决方式其实有讲究,IgnorePointer 和 AbsorbPointer 就能派上用场,两者区别在于:IgnorePointer 直接让子树忽略指针事件,事件会穿过它去命中下层;AbsorbPointer 则会把事件吸收掉,但不会向下传递,适合做"拦截层"。常见的需求是"遮罩必须能显示,但不能拦截点击",首选 IgnorePointer。
dart复制Stack(
children: [
GestureDetector(
onTap: () => debugPrint('底层可点'),
child: Container(color: Colors.grey, width: 200, height: 200),
),
// 这层只是视觉遮罩,不挡下层交互
IgnorePointer(
child: Positioned.fill(
child: Container(color: Colors.black12),
),
),
],
)
这里有个小坑要提醒:IgnorePointer 本身要包在遮罩组件外面,而不是反过来。如果你把 IgnorePointer 放在了 Container 里面,可能只有 Container 的内容区域不响应,而 Container 本身仍会占用命中测试区域,达不到穿透效果。
5.2 没有 Positioned 的孩子全部堆在一起?先检查 alignment
自从引入了 Stack,偶尔会遇到"为什么一堆组件全叠在左上角"的问题。大多数情况是因为 children 里夹杂了多个普通组件,又没有给它们单独包 Positioned,Stack.alignment 又是默认的 topStart,自然就堆起来了。
在这个场景里,补救措施有三条,优先级从高到低:第一,给需要偏移的组件包 Positioned,最清晰;第二,如果只是想让某个组件居中或靠右下,用 Align 解决;第三,如果多个普通组件本身就按 Stack 排列且都能接受统一对齐,可以修改 Stack.alignment。核心思想是——Stack 的 children 不是"每个组件自动各占一行的列表",它们是"同一块画布上的多个图层",不主动指定位置,就一定会重合。
5.3 什么情况下要给 Stack 包 RepaintBoundary
Stack 里的组件多了之后,我还会关注一个问题:重绘的性能。Flutter 的绘制以组件树为单位,如果某个子组件频繁重绘,比如一个动画进度条、一个实时变化的计时器,而它没有做隔离,那它重绘时可能带动整个 Stack 层级一起重绘。
解决办法是给频繁变化的子组件包一层 RepaintBoundary,它会形成独立的绘制图层,重绘时不影响周围组件。要注意 RepaintBoundary 不是越多越好。每多一个独立图层,系统合成时就要多考虑一层,数量太多反而增加合成成本。我的经验是:一对复杂界面,把"动态层"包起来即可,不要让静态文字、静态图标也跟着反复重绘。
5.4 跨端小屏真机上的三个适配细节
既然这套学习路线是冲着某系统跨端来的,我顺带把设备适配的通用经验也放进来了,这些都是 Flutter 层就能解决的问题。
第一,安全区域。真机上有状态栏、导航栏、刘海等系统元素,Positioned(top: 0) 的组件的确可能直接顶到状态栏里,看起来很突兀。通常最外层包一个 SafeArea,或者在计算边距时加上 MediaQuery.of(context).padding.top,就能让悬浮按钮、返回按钮避开这些区域。
第二,屏幕尺寸差异。Positioned 的 left/right 是固定像素值,在平板和手机上视觉效果差距可能很大。忠于需求的前提下,可以采用 LayoutBuilder 计算比例,或者用 Align 搭配 Alignment 做相对定位,适合做"永远贴着边"的组件。
第三,文字缩放。系统设置了超大字体后,Positioned 固定的容器尺寸可能被文字撑爆。角标里的数字、标签上的文案,我习惯用固定尺寸的 Text 包裹 FittedBox,或者给角标容器设置明确的宽高,而不是仅仅靠 padding 撑着,避免文本变大后位移。
5.5 图层顺序就是 z-index,别去硬找不存在的 zIndex 属性
Stack 没有 zIndex 属性,很多从前端转过来的同学会到处找它。实际上,children 的顺序天然就是层级顺序:写在后面的组件显示在上层。想要动态调整某个组件的层级时,通常做法是把它在 children 列表里的索引往后挪。
如果代码里大量使用这种动态调整层级的需求,我会建议你在业务层维护一个"有序组件列表",而不是在 build 方法里手写一长串 children,这样做可读性会好很多。遇到嵌套 Stack 的时候同理,每层各自维护自己的顺序,别试图跨 Stack 控制层级,编程的复杂度先降下来再说。
6. 学完这一课的确认清单和我的几条手记
6.1 三道自测题,检验自己是不是真懂了
每次学习笔记我都习惯留几道自测题,这次也放三道,建议你先自己想答案,再看我的解法。
问题一:一个 Stack 单独放在 Center 里,里面只有一个 Positioned 子组件,为什么坐标看起来"失效"了?答案:Stack 的尺寸在宽松约束下由非定位子组件决定,没有任何非定位子组件时,Stack 可能趋近于零宽零高,Positioned 也就无从定位。解决方法是给 Stack 给 SizedBox 固定尺寸,或者放一个撑尺寸的背景组件。
问题二:怎样让一个 40x40 的按钮永远位于 Stack 的右下角,并且距离右边缘和下边缘各 20 像素?答案:Positioned(right: 20, bottom: 20, child: SizedBox(width: 40, height: 40, child: button))。只要 Stack 本身尺寸确定,这个按钮就会保持在这个相对位置,不随屏幕内容滚动而改变。
问题三:一个从透明到半透明的遮罩层盖住了整个页面,但我不希望它拦截下层按钮的点击,应该怎么处理?答案:在遮罩组件外层包上 IgnorePointer,或者把遮罩整体放进 IgnorePointer 子树。如果只是想让点击事件无法穿透,则用 AbsorbPointer。
6.2 这几条经验是写代码时回头复盘才记住的
第一,别再拿 Row/Column 的思路去套 Stack。在 Stack 里写子组件,先问一句"这个位置我到底想让它固定在哪"。固定不了的地方,往往是布局设计本身有问题。第二,图层顺序就是 z-index 这个观念要尽早建立,很多"为什么点不到""为什么没盖住"的问题,追根溯源都是顺序问题。第三,先跑出完整 UI,再想 RepaintBoundary 之类的性能优化。新手阶段优先保证布局逻辑清晰,性能优化过度反而容易让代码变得难维护。
另外再分享一个实用小技巧:当你在 Stack 里调试时,可以临时给每个子组件加不同的背景色或边框,一眼就能确认它们各自的边界范围。我在学习叠加布局的那几天全靠这个办法排查坐标错位,比盯着代码猜快得多。
学完这几块内容,我对"层叠"这两个字有了更完整的理解。它不只是一个组件,更是一种建模思维:界面不是一行行文字,而是很多图层的组合。下一篇大概会去看布局中更偏弹性伸缩的部分,不过这之前,建议你先把今天这个头像角标和视频卡片亲手敲一遍。跑起来之后,你就知道 Stack 和 Positioned 到底解决的是什么问题了。
