import 'package:flutter/material.dart'; /// 简单的骨架屏动画组件 - 使用Flutter内置动画实现加载效果 class SimpleShimmer extends StatefulWidget { final Widget child; final Duration duration; final Color baseColor; final Color highlightColor; const SimpleShimmer({ super.key, required this.child, this.duration = const Duration(milliseconds: 1500), required this.baseColor, required this.highlightColor, }); @override State createState() => _SimpleShimmerState(); } class _SimpleShimmerState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _animation; @override void initState() { super.initState(); _controller = AnimationController( duration: widget.duration, vsync: this, )..repeat(); _animation = Tween(begin: -2, end: 2).animate( CurvedAnimation( parent: _controller, curve: Curves.easeInOutSine, ), ); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _animation, builder: (context, child) { return ShaderMask( shaderCallback: (bounds) { return LinearGradient( begin: Alignment(_animation.value, 0), end: Alignment(_animation.value + 1, 0), colors: [ widget.baseColor, widget.highlightColor, widget.baseColor, ], stops: const [0.0, 0.5, 1.0], ).createShader(bounds); }, child: widget.child, ); }, ); } } /// 加载状态组件库 - 提供多种样式的加载状态展示 /// 包括骨架屏、圆形加载器、进度条等多种加载效果 /// 基础骨架屏组件 - 用于内容加载时的占位显示 /// [width] 骨架屏宽度,null时根据父容器自适应 /// [height] 骨架屏高度,必填参数 /// [borderRadius] 圆角半径,null时使用默认值 /// [margin] 外边距,控制骨架屏与其他元素的间距 class Skeleton extends StatefulWidget { /// 骨架屏宽度,null时根据父容器自适应 final double? width; /// 骨架屏高度,必填参数 final double height; /// 圆角半径,null时使用默认值 final double? borderRadius; /// 外边距,控制骨架屏与其他元素的间距 final EdgeInsetsGeometry? margin; /// 背景颜色,null时使用主题色彩 final Color? backgroundColor; /// 骨架屏高亮颜色,null时使用主题色彩 final Color? highlightColor; const Skeleton({ super.key, this.width, required this.height, this.borderRadius, this.margin, this.backgroundColor, this.highlightColor, }); @override State createState() => _SkeletonState(); } class _SkeletonState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _animation; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(milliseconds: 1500), vsync: this, )..repeat(); _animation = Tween(begin: -2, end: 2).animate( CurvedAnimation( parent: _controller, curve: Curves.easeInOutSine, ), ); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final baseColor = widget.backgroundColor ?? theme.colorScheme.surfaceVariant.withOpacity(0.5); final highlightColor = widget.highlightColor ?? theme.colorScheme.surfaceVariant.withOpacity(0.8); return AnimatedBuilder( animation: _animation, builder: (context, child) { return Container( width: widget.width, height: widget.height, margin: widget.margin, decoration: BoxDecoration( borderRadius: BorderRadius.circular(widget.borderRadius ?? 8), gradient: LinearGradient( begin: Alignment(_animation.value, 0), end: Alignment(_animation.value + 1, 0), colors: [ baseColor, highlightColor, baseColor, ], stops: const [0.0, 0.5, 1.0], ), ), ); }, ); } } /// 圆形骨架屏组件 - 用于头像、图标等圆形元素的占位显示 /// [size] 圆形骨架屏直径,必填参数 /// [margin] 外边距,控制骨架屏与其他元素的间距 class CircleSkeleton extends StatefulWidget { /// 圆形骨架屏直径,必填参数 final double size; /// 外边距,控制骨架屏与其他元素的间距 final EdgeInsetsGeometry? margin; /// 背景颜色,null时使用主题色彩 final Color? backgroundColor; /// 骨架屏高亮颜色,null时使用主题色彩 final Color? highlightColor; const CircleSkeleton({ super.key, required this.size, this.margin, this.backgroundColor, this.highlightColor, }); @override State createState() => _CircleSkeletonState(); } class _CircleSkeletonState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _animation; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(milliseconds: 1500), vsync: this, )..repeat(); _animation = Tween(begin: -2, end: 2).animate( CurvedAnimation( parent: _controller, curve: Curves.easeInOutSine, ), ); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final baseColor = widget.backgroundColor ?? theme.colorScheme.surfaceVariant.withOpacity(0.5); final highlightColor = widget.highlightColor ?? theme.colorScheme.surfaceVariant.withOpacity(0.8); return AnimatedBuilder( animation: _animation, builder: (context, child) { return Container( width: widget.size, height: widget.size, margin: widget.margin, decoration: BoxDecoration( shape: BoxShape.circle, gradient: LinearGradient( begin: Alignment(_animation.value, 0), end: Alignment(_animation.value + 1, 0), colors: [ baseColor, highlightColor, baseColor, ], stops: const [0.0, 0.5, 1.0], ), ), ); }, ); } } /// 图片骨架屏组件 - 专门用于图片加载时的占位显示 /// 模拟真实的图片比例和加载效果 /// [width] 图片宽度,null时根据父容器自适应 /// [height] 图片高度,必填参数 /// [aspectRatio] 宽高比,null时使用固定高度 class ImageSkeleton extends StatelessWidget { /// 图片宽度,null时根据父容器自适应 final double? width; /// 图片高度,必填参数 final double height; /// 宽高比,null时使用固定高度 final double? aspectRatio; /// 外边距,控制骨架屏与其他元素的间距 final EdgeInsetsGeometry? margin; /// 背景颜色,null时使用主题色彩 final Color? backgroundColor; /// 骨架屏高亮颜色,null时使用主题色彩 final Color? highlightColor; const ImageSkeleton({ super.key, this.width, required this.height, this.aspectRatio, this.margin, this.backgroundColor, this.highlightColor, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); final baseColor = backgroundColor ?? theme.colorScheme.surfaceVariant.withOpacity(0.5); final highlightColor = this.highlightColor ?? theme.colorScheme.surfaceVariant.withOpacity(0.8); Widget skeleton = Container( width: width, height: height, margin: margin, decoration: BoxDecoration( color: baseColor, borderRadius: BorderRadius.circular(8), ), child: AnimatedContainer( duration: const Duration(milliseconds: 1500), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, colors: [ baseColor, highlightColor, baseColor, ], ), borderRadius: BorderRadius.circular(8), ), ), ); if (aspectRatio != null) { skeleton = AspectRatio( aspectRatio: aspectRatio!, child: skeleton, ); } return skeleton; } } /// 文本骨架屏组件 - 用于文本内容加载时的占位显示 /// 模拟不同长度的文本行 /// [width] 文本宽度,null时根据父容器自适应 /// [height] 文本高度,null时使用默认行高 /// [lineCount] 文本行数,大于1时显示多行 class TextSkeleton extends StatelessWidget { /// 文本宽度,null时根据父容器自适应 final double? width; /// 文本高度,null时使用默认行高 final double? height; /// 文本行数,大于1时显示多行 final int lineCount; /// 行间距,控制多行文本的间距 final double lineSpacing; /// 外边距,控制骨架屏与其他元素的间距 final EdgeInsetsGeometry? margin; /// 背景颜色,null时使用主题色彩 final Color? backgroundColor; /// 骨架屏高亮颜色,null时使用主题色彩 final Color? highlightColor; const TextSkeleton({ super.key, this.width, this.height, this.lineCount = 1, this.lineSpacing = 8, this.margin, this.backgroundColor, this.highlightColor, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); final baseColor = backgroundColor ?? theme.colorScheme.surfaceVariant.withOpacity(0.5); final highlightColor = this.highlightColor ?? theme.colorScheme.surfaceVariant.withOpacity(0.8); final textHeight = height ?? theme.textTheme.bodyMedium?.fontSize ?? 14; if (lineCount == 1) { return Container( width: width, height: textHeight, margin: margin, decoration: BoxDecoration( color: baseColor, borderRadius: BorderRadius.circular(4), ), child: AnimatedContainer( duration: const Duration(milliseconds: 1500), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, colors: [ baseColor, highlightColor, baseColor, ], ), borderRadius: BorderRadius.circular(4), ), ), ); } return Container( margin: margin, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: List.generate(lineCount, (index) { // 最后一行宽度减短,模拟真实的文本效果 final lineWidth = index == lineCount - 1 ? (width ?? double.infinity) * 0.7 : width; return Padding( padding: EdgeInsets.only(bottom: index < lineCount - 1 ? lineSpacing : 0), child: Container( width: lineWidth, height: textHeight, decoration: BoxDecoration( color: baseColor, borderRadius: BorderRadius.circular(4), ), child: AnimatedContainer( duration: const Duration(milliseconds: 1500), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, colors: [ baseColor, highlightColor, baseColor, ], stops: const [0.0, 0.5, 1.0], ), borderRadius: BorderRadius.circular(4), ), ), ), ); }), ), ); } } /// 圆形进度指示器 - 标准的圆形加载动画 /// [size] 指示器大小,null时使用默认大小 /// [strokeWidth] 线条宽度,null时使用默认宽度 /// [color] 指示器颜色,null时使用主题主色调 class LoadingIndicator extends StatelessWidget { /// 指示器大小,null时使用默认大小 final double? size; /// 线条宽度,null时使用默认宽度 final double? strokeWidth; /// 指示器颜色,null时使用主题主色调 final Color? color; const LoadingIndicator({ super.key, this.size, this.strokeWidth, this.color, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); final indicatorColor = color ?? theme.colorScheme.primary; final indicatorSize = size ?? 24; return SizedBox( width: indicatorSize, height: indicatorSize, child: CircularProgressIndicator( strokeWidth: strokeWidth ?? 2, valueColor: AlwaysStoppedAnimation(indicatorColor), ), ); } } /// 线性进度指示器 - 水平进度条加载动画 /// [width] 进度条宽度,null时根据父容器自适应 /// [height] 进度条高度,null时使用默认高度 /// [color] 进度条颜色,null时使用主题主色调 class LinearLoadingIndicator extends StatelessWidget { /// 进度条宽度,null时根据父容器自适应 final double? width; /// 进度条高度,null时使用默认高度 final double? height; /// 进度条颜色,null时使用主题主色调 final Color? color; /// 背景颜色,null时使用主题背景色 final Color? backgroundColor; /// 是否显示百分比文本 final bool showPercentage; const LinearLoadingIndicator({ super.key, this.width, this.height, this.color, this.backgroundColor, this.showPercentage = false, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); final progressColor = color ?? theme.colorScheme.primary; final bgColor = backgroundColor ?? theme.colorScheme.surfaceVariant; final progressHeight = height ?? 4; Widget progressBar = Container( width: width, height: progressHeight, decoration: BoxDecoration( color: bgColor, borderRadius: BorderRadius.circular(progressHeight / 2), ), child: ClipRRect( borderRadius: BorderRadius.circular(progressHeight / 2), child: LinearProgressIndicator( backgroundColor: Colors.transparent, valueColor: AlwaysStoppedAnimation(progressColor), minHeight: progressHeight, ), ), ); if (showPercentage) { return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ progressBar, const SizedBox(height: 8), Text( '加载中...', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurface.withOpacity(0.6), ), ), ], ); } return progressBar; } } /// 加载状态包装器 - 根据加载状态显示不同内容 /// [isLoading] 是否处于加载状态 /// [loadingWidget] 自定义加载组件,null时使用默认骨架屏 /// [child] 正常内容组件 /// [skeleton] 自定义骨架屏,null时使用默认骨架屏 class LoadingWrapper extends StatelessWidget { /// 是否处于加载状态 final bool isLoading; /// 自定义加载组件,null时使用默认骨架屏 final Widget? loadingWidget; /// 正常内容组件 final Widget child; /// 自定义骨架屏,null时使用默认骨架屏 final Widget? skeleton; const LoadingWrapper({ super.key, required this.isLoading, required this.child, this.loadingWidget, this.skeleton, }); @override Widget build(BuildContext context) { if (isLoading) { return loadingWidget ?? skeleton ?? Container(); } return child; } } /// 骨架屏列表 - 用于列表加载时的占位显示 /// [itemCount] 列表项数量 /// [itemHeight] 列表项高度 /// [itemSpacing] 列表项间距 /// [padding] 列表内边距 class SkeletonList extends StatelessWidget { /// 列表项数量 final int itemCount; /// 列表项高度 final double itemHeight; /// 列表项间距 final double itemSpacing; /// 列表内边距 final EdgeInsetsGeometry? padding; /// 是否显示圆形头像占位 final bool showAvatar; /// 是否显示多行文本占位 final bool showMultiLineText; const SkeletonList({ super.key, required this.itemCount, this.itemHeight = 80, this.itemSpacing = 8, this.padding, this.showAvatar = true, this.showMultiLineText = true, }); @override Widget build(BuildContext context) { return ListView.builder( padding: padding, itemCount: itemCount, itemBuilder: (context, index) { return Padding( padding: EdgeInsets.only(bottom: itemSpacing), child: SizedBox( height: itemHeight, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (showAvatar) ...[ CircleSkeleton(size: itemHeight * 0.6), const SizedBox(width: 12), ], Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ TextSkeleton( width: double.infinity, height: 16, lineCount: showMultiLineText ? 2 : 1, lineSpacing: 6, ), ], ), ), ], ), ), ); }, ); } } /// 骨架屏网格 - 用于网格布局加载时的占位显示 /// [crossAxisCount] 交叉轴数量 /// [itemAspectRatio] 列表项宽高比 /// [itemSpacing] 列表项间距 /// [padding] 网格内边距 class SkeletonGrid extends StatelessWidget { /// 交叉轴数量 final int crossAxisCount; /// 列表项宽高比 final double itemAspectRatio; /// 列表项间距 final double itemSpacing; /// 网格内边距 final EdgeInsetsGeometry? padding; /// 列表项数量 final int itemCount; const SkeletonGrid({ super.key, required this.crossAxisCount, this.itemAspectRatio = 1.0, this.itemSpacing = 8, this.padding, this.itemCount = 6, }); @override Widget build(BuildContext context) { return GridView.builder( padding: padding, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: crossAxisCount, childAspectRatio: itemAspectRatio, crossAxisSpacing: itemSpacing, mainAxisSpacing: itemSpacing, ), itemCount: itemCount, itemBuilder: (context, index) { return const Skeleton( width: double.infinity, height: double.infinity, ); }, ); } } /// 全屏加载组件 - 覆盖整个屏幕的加载状态 /// [message] 加载提示文本 /// [showProgress] 是否显示进度指示器 /// [backgroundColor] 背景颜色,null时使用半透明黑色 class FullScreenLoading extends StatelessWidget { /// 加载提示文本 final String? message; /// 是否显示进度指示器 final bool showProgress; /// 背景颜色,null时使用半透明黑色 final Color? backgroundColor; /// 文本颜色,null时使用白色 final Color? textColor; const FullScreenLoading({ super.key, this.message, this.showProgress = true, this.backgroundColor, this.textColor, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); final bgColor = backgroundColor ?? Colors.black.withOpacity(0.7); final textColor = this.textColor ?? Colors.white; return Container( color: bgColor, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ if (showProgress) ...[ LoadingIndicator( size: 48, color: textColor, ), const SizedBox(height: 16), ], if (message != null) ...[ Text( message!, style: theme.textTheme.bodyLarge?.copyWith( color: textColor, ), textAlign: TextAlign.center, ), ], ], ), ), ); } } /// 加载状态枚举 - 定义不同的加载状态 enum LoadingState { /// 初始加载状态 initial, /// 加载中状态 loading, /// 加载完成状态 completed, /// 加载失败状态 error, /// 空数据状态 empty, } /// 加载状态构建器 - 根据加载状态构建不同的UI /// [state] 当前加载状态 /// [loadingBuilder] 加载状态构建器 /// [completedBuilder] 完成状态构建器 /// [errorBuilder] 错误状态构建器 /// [emptyBuilder] 空状态构建器 /// [initialBuilder] 初始状态构建器 class LoadingStateBuilder extends StatelessWidget { /// 当前加载状态 final LoadingState state; /// 加载状态构建器 final WidgetBuilder? loadingBuilder; /// 完成状态构建器 final WidgetBuilder completedBuilder; /// 错误状态构建器 final WidgetBuilder? errorBuilder; /// 空状态构建器 final WidgetBuilder? emptyBuilder; /// 初始状态构建器 final WidgetBuilder? initialBuilder; const LoadingStateBuilder({ super.key, required this.state, required this.completedBuilder, this.loadingBuilder, this.errorBuilder, this.emptyBuilder, this.initialBuilder, }); @override Widget build(BuildContext context) { switch (state) { case LoadingState.initial: return initialBuilder?.call(context) ?? Container(); case LoadingState.loading: return loadingBuilder?.call(context) ?? const Center(child: LoadingIndicator()); case LoadingState.completed: return completedBuilder(context); case LoadingState.error: return errorBuilder?.call(context) ?? const Center(child: Text('加载失败,请重试')); case LoadingState.empty: return emptyBuilder?.call(context) ?? const Center(child: Text('暂无数据')); } } }