290 lines
7.7 KiB
Dart
290 lines
7.7 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
/// 骨架屏加载组件 - 提供更好的加载体验
|
|
class ShimmerLoading extends StatefulWidget {
|
|
final Widget child;
|
|
final bool isLoading;
|
|
|
|
const ShimmerLoading({
|
|
super.key,
|
|
required this.child,
|
|
this.isLoading = true,
|
|
});
|
|
|
|
@override
|
|
State<ShimmerLoading> createState() => _ShimmerLoadingState();
|
|
}
|
|
|
|
class _ShimmerLoadingState extends State<ShimmerLoading>
|
|
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) {
|
|
if (!widget.isLoading) return widget.child;
|
|
|
|
return AnimatedBuilder(
|
|
animation: _animation,
|
|
builder: (context, child) {
|
|
return ShaderMask(
|
|
shaderCallback: (bounds) {
|
|
return LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: const [
|
|
Color(0xFFEBEBF4),
|
|
Color(0xFFF4F4F4),
|
|
Color(0xFFEBEBF4),
|
|
],
|
|
stops: [
|
|
_animation.value - 1,
|
|
_animation.value,
|
|
_animation.value + 1,
|
|
].map((e) => e.clamp(0.0, 1.0)).toList(),
|
|
).createShader(bounds);
|
|
},
|
|
blendMode: BlendMode.srcATop,
|
|
child: widget.child,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 骨架屏占位框
|
|
class ShimmerBox extends StatelessWidget {
|
|
final double? width;
|
|
final double height;
|
|
final double borderRadius;
|
|
|
|
const ShimmerBox({
|
|
super.key,
|
|
this.width,
|
|
required this.height,
|
|
this.borderRadius = 8,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
width: width,
|
|
height: height,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFE5E7EB),
|
|
borderRadius: BorderRadius.circular(borderRadius),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 资产卡片骨架屏
|
|
class AssetCardSkeleton extends StatelessWidget {
|
|
const AssetCardSkeleton({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ShimmerLoading(
|
|
child: Container(
|
|
margin: const EdgeInsets.all(16),
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.04),
|
|
blurRadius: 30,
|
|
offset: const Offset(0, 8),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const ShimmerBox(width: 80, height: 16),
|
|
const SizedBox(height: 12),
|
|
const ShimmerBox(width: 180, height: 36),
|
|
const SizedBox(height: 8),
|
|
const ShimmerBox(width: 120, height: 14),
|
|
const SizedBox(height: 12),
|
|
const ShimmerBox(width: 100, height: 24, borderRadius: 12),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 资产项目骨架屏
|
|
class AssetItemSkeleton extends StatelessWidget {
|
|
const AssetItemSkeleton({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ShimmerLoading(
|
|
child: Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 2,
|
|
offset: const Offset(0, 1),
|
|
),
|
|
],
|
|
),
|
|
child: Row(
|
|
children: [
|
|
const ShimmerBox(width: 40, height: 40, borderRadius: 20),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const ShimmerBox(width: 60, height: 16),
|
|
const SizedBox(height: 8),
|
|
const ShimmerBox(width: 100, height: 20),
|
|
const SizedBox(height: 4),
|
|
const ShimmerBox(width: 80, height: 12),
|
|
],
|
|
),
|
|
),
|
|
const ShimmerBox(width: 14, height: 20),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 收益统计骨架屏
|
|
class EarningsStatsSkeleton extends StatelessWidget {
|
|
const EarningsStatsSkeleton({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ShimmerLoading(
|
|
child: Container(
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 2,
|
|
offset: const Offset(0, 1),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
width: 4,
|
|
height: 20,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFE5E7EB),
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
const ShimmerBox(width: 60, height: 16),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
children: const [
|
|
ShimmerBox(width: 50, height: 12),
|
|
SizedBox(height: 8),
|
|
ShimmerBox(width: 70, height: 16),
|
|
],
|
|
),
|
|
),
|
|
Container(
|
|
width: 1,
|
|
height: 40,
|
|
color: const Color(0xFFE5E7EB),
|
|
),
|
|
Expanded(
|
|
child: Column(
|
|
children: const [
|
|
ShimmerBox(width: 50, height: 12),
|
|
SizedBox(height: 8),
|
|
ShimmerBox(width: 70, height: 16),
|
|
],
|
|
),
|
|
),
|
|
Container(
|
|
width: 1,
|
|
height: 40,
|
|
color: const Color(0xFFE5E7EB),
|
|
),
|
|
Expanded(
|
|
child: Column(
|
|
children: const [
|
|
ShimmerBox(width: 50, height: 12),
|
|
SizedBox(height: 8),
|
|
ShimmerBox(width: 70, height: 16),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 页面骨架屏
|
|
class PageSkeleton extends StatelessWidget {
|
|
const PageSkeleton({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
children: const [
|
|
AssetCardSkeleton(),
|
|
SizedBox(height: 24),
|
|
AssetItemSkeleton(),
|
|
SizedBox(height: 16),
|
|
AssetItemSkeleton(),
|
|
SizedBox(height: 16),
|
|
AssetItemSkeleton(),
|
|
SizedBox(height: 24),
|
|
EarningsStatsSkeleton(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|