import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; /// ============================================================ /// Stack + Positioned 层叠布局演示页 /// /// 一、两种使用方式: /// 1. 直接运行本页面:runApp(const MaterialApp(home: StackDemoPage())) /// 2. 复制某个 Demo 的 Stack 代码到任意 Scaffold 的 body 中。 /// /// 二、作用: /// Stack 实现 UI 重叠,类似 CSS 的 position:absolute。 /// Stack = 图层容器(后写的子组件盖在上面) /// Positioned = 给子组件做绝对定位(只能直接放在 Stack 的 children 里!) /// /// 三、核心属性: /// Stack.alignment : 对【没有被 Positioned 包裹】的子组件做默认对齐 /// Stack.clipBehavior: Clip.none 允许子组件绘制超出 Stack 边界 /// (默认 Clip.hardEdge,超出会被裁剪看不见) /// Positioned.top/bottom/left/right : 距离 Stack 四边的距离 /// Positioned.fill : 填满整个 Stack 容器(相当于做蒙层) /// /// 四、三大高频坑: /// 1. 全部子组件都是 Positioned 时 → Stack 尺寸异常! /// 真实行为:size = 父约束最大值(constraints.biggest),不按内容算。 /// 放在无界约束(如 Column 裸放)→ debug 断言崩溃 "A Stack requires /// bounded constraints",整个 body 都画不出来; /// 放在有界约束 → Stack 占满整个可用区域。 /// 修复A:给 Stack 套 SizedBox 固定宽高; /// 修复B:放一个非 Positioned 的底层组件撑起大小。 /// 2. Positioned 只能直接作为 Stack 的 children,套其他 Widget 中间层会失效。 /// 3. 做悬浮角标务必 clipBehavior: Clip.none,否则负偏移部分被裁剪掉。 /// ============================================================ class StackDemoPage extends StatelessWidget { const StackDemoPage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Stack 层叠布局 Demo')), body: SingleChildScrollView( // AlwaysScrollableScrollPhysics() 永远可以滚动,内容很短也能拽动回弹 // BouncingScrollPhysics() 越界回弹(iOS 风格,拉出来空白再弹回) // NeverScrollableScrollPhysics() ❌完全禁止滚动,就算内容溢出也不能滑动 physics: BouncingScrollPhysics(), padding: EdgeInsets.all(26.w), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const _SectionTitle('Demo1:基础重叠 + 右上角角标'), // 注意:Flutter 3.44 的 Container 已不是 const 构造,外层不能再写 const Center Center( child: Stack( // 未被 Positioned 的子组件默认居中 alignment: Alignment.topLeft, // 允许内容超出 Stack,角标才能"溢出"到盒子外 clipBehavior: Clip.none, // 允许超出 Stack 边界 (默认 Clip.hardEdge,超出会被裁剪看不见) children: [ // 底层盒子 Container( width: 250.w, height: 250.h, color: Colors.blue, ), // 绝对定位:右上角,负偏移让它一半悬在盒子外 Positioned( top: -10, right: -10, child: Container( padding: EdgeInsets.all(6.w), color: Colors.red, child: Text( "角标", style: TextStyle(color: Colors.white), ), ), ), // 普通子组件,受 Stack.alignment 控制居中 Text( "中间文字", style: TextStyle(color: Colors.white, fontSize: 20.sp), ), ], ), ), SizedBox(height: 32.h), const _SectionTitle('Demo2:Positioned.fill 蒙层'), Center( child: Stack( children: [ // 底层组件撑起 Stack 的尺寸(250x250) Container(width: 250.w, height: 250.h, color: Colors.grey), // Positioned.fill 填满整个 Stack,做半透明蒙层 Positioned.fill( child: Container( color: Colors.black54, child: Center( child: Text( "蒙层", style: TextStyle( color: Colors.white, fontSize: 20.sp, fontWeight: FontWeight.bold, ), ), ), ), ), ], ), ), SizedBox(height: 32.h), const _SectionTitle('坑①:全部 Positioned → Stack 尺寸异常'), Text( '全部子组件都是 Positioned 时,Stack 尺寸不由内容决定,' '而是直接取父约束最大值。套上 SizedBox 有界约束后,' 'Stack 会占满 300x60(而非收缩到内容大小);' '若裸放在 Column 这种无界高度里,debug 模式会直接断言崩溃,' '整个 body 都画不出来——这就是之前页面只剩标题的原因。', style: TextStyle(fontSize: 13.sp, color: Color(0xFF888888)), ), SizedBox(height: 8.h), // 有界约束 + 全 Positioned:Stack 占满 SizedBox 给的 300x60 SizedBox( width: 300.w, height: 60.h, child: ColoredBox( color: Color(0xFFF5F7FA), child: Stack( children: [ Positioned(top: 10, left: 10, child: Text("A")), ], ), ), ), SizedBox(height: 32.h), const _SectionTitle('坑①修复方案1:SizedBox 固定宽高'), SizedBox( width: 300.w, height: 80.h, child: ColoredBox( color: Color(0xFFF5F7FA), child: Stack( children: [ Positioned(top: 10, left: 10, child: Text("A")), ], ), ), ), SizedBox(height: 32.h), const _SectionTitle('坑①修复方案2:非 Positioned 底层组件撑起尺寸'), Stack( children: [ // 非 Positioned 底层组件:由它决定 Stack 尺寸 ColoredBox( color: Color(0xFFE8E8E8), child: SizedBox(width: 300.w, height: 80.h), ), Positioned(top: 10, left: 10, child: Text("A")), ], ), ], ), ), ); } } /// 区块标题 class _SectionTitle extends StatelessWidget { const _SectionTitle(this.title); final String title; @override Widget build(BuildContext context) { return Padding( padding: EdgeInsets.only(bottom: 12.h), child: Text( title, style: TextStyle( fontSize: 20.sp, fontWeight: FontWeight.bold, color: Color(0xFF333333), ), ), ); } }