Files
App.K12.Student.Miniapp_Flu…/lib/pages/demo/stack_demo.dart
T
2026-09-04 15:22:20 +08:00

190 lines
7.6 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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),
),
),
);
}
}