// 共享骨架屏加载组件 — 替代 CircularProgressIndicator // // 使用: SkeletonBox(height: 80) 或 SkeletonBox(height: 48, shimmer: true) // 样式: 灰色圆角矩形 + 可选 shimmer 微光动画 import 'package:flutter/material.dart'; import '../core/theme/app_radius.dart'; /// 骨架屏加载占位块 class SkeletonBox extends StatelessWidget { const SkeletonBox({ super.key, required this.height, this.width, this.shimmer = false, this.borderRadius, }); /// 高度 final double height; /// 宽度(默认撑满父容器) final double? width; /// 是否启用 shimmer 微光动画 final bool shimmer; /// 自定义圆角(默认 AppRadius.md) final BorderRadius? borderRadius; @override Widget build(BuildContext context) { final theme = Theme.of(context); final bgColor = theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.3); final radius = borderRadius ?? AppRadius.mdBorder; if (shimmer) { return _ShimmerBox( height: height, width: width, bgColor: bgColor, borderRadius: radius, ); } return Container( height: height, width: width, decoration: BoxDecoration( color: bgColor, borderRadius: radius, ), ); } } /// 带微光动画的骨架屏 class _ShimmerBox extends StatefulWidget { const _ShimmerBox({ required this.height, this.width, required this.bgColor, required this.borderRadius, }); final double height; final double? width; final Color bgColor; final BorderRadius borderRadius; @override State<_ShimmerBox> createState() => _ShimmerBoxState(); } class _ShimmerBoxState extends State<_ShimmerBox> with SingleTickerProviderStateMixin { late final AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 1500), )..repeat(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final theme = Theme.of(context); return AnimatedBuilder( animation: _controller, builder: (context, child) { return Container( height: widget.height, width: widget.width, decoration: BoxDecoration( borderRadius: widget.borderRadius, gradient: LinearGradient( begin: Alignment(-1.0 + 2.0 * _controller.value, 0), end: Alignment(1.0 + 2.0 * _controller.value, 0), colors: [ widget.bgColor, theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.15), widget.bgColor, ], ), ), ); }, ); } } /// 通用列表骨架屏 — 显示 N 行占位 class SkeletonList extends StatelessWidget { const SkeletonList({ super.key, this.itemCount = 5, this.itemHeight = 72, this.spacing = 12, this.shimmer = false, }); /// 骨架行数 final int itemCount; /// 每行高度 final double itemHeight; /// 行间距 final double spacing; /// 是否 shimmer final bool shimmer; @override Widget build(BuildContext context) { return ListView.builder( physics: const NeverScrollableScrollPhysics(), shrinkWrap: true, itemCount: itemCount, itemBuilder: (_, _) => Padding( padding: EdgeInsets.only(bottom: spacing), child: SkeletonBox(height: itemHeight, shimmer: shimmer), ), ); } }