snap_wish/lib/presentation/widgets/loading_widgets.dart
2025-09-17 13:32:25 +08:00

860 lines
23 KiB
Dart
Raw Permalink 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';
/// 简单的骨架屏动画组件 - 使用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<SimpleShimmer> createState() => _SimpleShimmerState();
}
class _SimpleShimmerState extends State<SimpleShimmer> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: widget.duration,
vsync: this,
)..repeat();
_animation = Tween<double>(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<Skeleton> createState() => _SkeletonState();
}
class _SkeletonState extends State<Skeleton> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
)..repeat();
_animation = Tween<double>(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<CircleSkeleton> createState() => _CircleSkeletonState();
}
class _CircleSkeletonState extends State<CircleSkeleton> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
)..repeat();
_animation = Tween<double>(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<Color>(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<Color>(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('暂无数据'));
}
}
}