190 lines
7.6 KiB
Dart
190 lines
7.6 KiB
Dart
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),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|