860 lines
23 KiB
Dart
860 lines
23 KiB
Dart
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('暂无数据'));
|
||
}
|
||
}
|
||
} |