import 'package:flutter/material.dart'; import 'package:patbond_flutter/core/theme/app_theme.dart'; /// Feed 骨架屏单元(05 号规范 §3.7):模拟单图卡——头部行(32 圆 + /// 两条横条)→ 4:3 通栏块 → 两条正文横条。块色 `surfaceTint` /// (1.18:1,装饰性占位不受对比度约束)。 /// /// 动效:整体不透明度 0.6 ↔ 1.0 呼吸循环 1200ms;系统「减弱动态效果」 /// 开启时静止在 1.0。首载用法为连排 3 张(P1/P4)。 class FeedSkeleton extends StatefulWidget { const FeedSkeleton({super.key}); @override State createState() => _FeedSkeletonState(); } class _FeedSkeletonState extends State with SingleTickerProviderStateMixin { late final AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 1200), lowerBound: 0.6, value: 1, ); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final reduceMotion = MediaQuery.of(context).disableAnimations; if (reduceMotion) { _controller.stop(); _controller.value = 1; } else if (!_controller.isAnimating) { _controller.repeat(reverse: true); } return FadeTransition( opacity: _controller, child: Card( clipBehavior: Clip.antiAlias, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.all(14), child: Row( children: [ const _SkeletonBlock(width: 32, height: 32, circle: true), const SizedBox(width: 10), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _SkeletonBlock( width: _relative(context, 0.40), height: 12, ), const SizedBox(height: 6), _SkeletonBlock( width: _relative(context, 0.24), height: 8, ), ], ), ], ), ), const AspectRatio( aspectRatio: 4 / 3, child: ColoredBox(color: AppColors.surfaceTint), ), Padding( padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _SkeletonBlock(width: _relative(context, 0.90), height: 12), const SizedBox(height: 8), _SkeletonBlock(width: _relative(context, 0.60), height: 12), ], ), ), ], ), ), ); } /// 横条宽度按屏宽比例取(规范 40%/24%/90%/60%),免依赖父约束测量。 double _relative(BuildContext context, double fraction) => MediaQuery.of(context).size.width * fraction; } class _SkeletonBlock extends StatelessWidget { const _SkeletonBlock({ required this.width, required this.height, this.circle = false, }); final double width; final double height; final bool circle; @override Widget build(BuildContext context) { return Container( width: width, height: height, decoration: BoxDecoration( color: AppColors.surfaceTint, borderRadius: BorderRadius.circular(circle ? width / 2 : 6), ), ); } }