339 lines
8.0 KiB
Dart
339 lines
8.0 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
/// Reusable loading indicator widget
|
|
///
|
|
/// Provides different loading indicator variants:
|
|
/// - Circular (default)
|
|
/// - Linear
|
|
/// - Overlay (full screen with backdrop)
|
|
/// - With message
|
|
///
|
|
/// Usage:
|
|
/// ```dart
|
|
/// // Simple circular indicator
|
|
/// LoadingIndicator()
|
|
///
|
|
/// // With custom size and color
|
|
/// LoadingIndicator(
|
|
/// size: 50,
|
|
/// color: Colors.blue,
|
|
/// )
|
|
///
|
|
/// // Linear indicator
|
|
/// LoadingIndicator.linear()
|
|
///
|
|
/// // Full screen overlay
|
|
/// LoadingIndicator.overlay(
|
|
/// message: 'Loading data...',
|
|
/// )
|
|
///
|
|
/// // Centered with message
|
|
/// LoadingIndicator.withMessage(
|
|
/// message: 'Please wait...',
|
|
/// )
|
|
/// ```
|
|
class LoadingIndicator extends StatelessWidget {
|
|
/// Size of the loading indicator
|
|
final double? size;
|
|
|
|
/// Color of the loading indicator
|
|
final Color? color;
|
|
|
|
/// Stroke width for circular indicator
|
|
final double strokeWidth;
|
|
|
|
/// Whether to use linear progress indicator
|
|
final bool isLinear;
|
|
|
|
/// Optional loading message
|
|
final String? message;
|
|
|
|
/// Text style for the message
|
|
final TextStyle? messageStyle;
|
|
|
|
/// Spacing between indicator and message
|
|
final double messageSpacing;
|
|
|
|
const LoadingIndicator({
|
|
super.key,
|
|
this.size,
|
|
this.color,
|
|
this.strokeWidth = 4.0,
|
|
this.message,
|
|
this.messageStyle,
|
|
this.messageSpacing = 16.0,
|
|
}) : isLinear = false;
|
|
|
|
/// Create a linear loading indicator
|
|
const LoadingIndicator.linear({
|
|
super.key,
|
|
this.color,
|
|
this.message,
|
|
this.messageStyle,
|
|
this.messageSpacing = 16.0,
|
|
}) : isLinear = true,
|
|
size = null,
|
|
strokeWidth = 4.0;
|
|
|
|
/// Create a full-screen loading overlay
|
|
static Widget overlay({
|
|
String? message,
|
|
Color? backgroundColor,
|
|
Color? indicatorColor,
|
|
TextStyle? messageStyle,
|
|
}) {
|
|
return _LoadingOverlay(
|
|
message: message,
|
|
backgroundColor: backgroundColor,
|
|
indicatorColor: indicatorColor,
|
|
messageStyle: messageStyle,
|
|
);
|
|
}
|
|
|
|
/// Create a loading indicator with a message below it
|
|
static Widget withMessage({
|
|
required String message,
|
|
double size = 40,
|
|
Color? color,
|
|
TextStyle? messageStyle,
|
|
double spacing = 16.0,
|
|
}) {
|
|
return LoadingIndicator(
|
|
size: size,
|
|
color: color,
|
|
message: message,
|
|
messageStyle: messageStyle,
|
|
messageSpacing: spacing,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final indicatorColor = color ?? theme.colorScheme.primary;
|
|
|
|
Widget indicator;
|
|
if (isLinear) {
|
|
indicator = LinearProgressIndicator(
|
|
color: indicatorColor,
|
|
backgroundColor: indicatorColor.withOpacity(0.1),
|
|
);
|
|
} else {
|
|
indicator = SizedBox(
|
|
width: size ?? 40,
|
|
height: size ?? 40,
|
|
child: CircularProgressIndicator(
|
|
color: indicatorColor,
|
|
strokeWidth: strokeWidth,
|
|
),
|
|
);
|
|
}
|
|
|
|
// If there's no message, return just the indicator
|
|
if (message == null) {
|
|
return indicator;
|
|
}
|
|
|
|
// If there's a message, wrap in column
|
|
return Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
indicator,
|
|
SizedBox(height: messageSpacing),
|
|
Text(
|
|
message!,
|
|
style: messageStyle ??
|
|
theme.textTheme.bodyMedium?.copyWith(
|
|
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Full-screen loading overlay
|
|
class _LoadingOverlay extends StatelessWidget {
|
|
final String? message;
|
|
final Color? backgroundColor;
|
|
final Color? indicatorColor;
|
|
final TextStyle? messageStyle;
|
|
|
|
const _LoadingOverlay({
|
|
this.message,
|
|
this.backgroundColor,
|
|
this.indicatorColor,
|
|
this.messageStyle,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
|
|
return Container(
|
|
color: backgroundColor ?? Colors.black.withOpacity(0.5),
|
|
child: Center(
|
|
child: Card(
|
|
elevation: 8,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(24.0),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
SizedBox(
|
|
width: 50,
|
|
height: 50,
|
|
child: CircularProgressIndicator(
|
|
color: indicatorColor ?? theme.colorScheme.primary,
|
|
strokeWidth: 4,
|
|
),
|
|
),
|
|
if (message != null) ...[
|
|
const SizedBox(height: 24),
|
|
SizedBox(
|
|
width: 200,
|
|
child: Text(
|
|
message!,
|
|
style: messageStyle ?? theme.textTheme.bodyLarge,
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Shimmer loading effect for list items
|
|
class ShimmerLoading extends StatefulWidget {
|
|
/// Width of the shimmer container
|
|
final double? width;
|
|
|
|
/// Height of the shimmer container
|
|
final double height;
|
|
|
|
/// Border radius
|
|
final double borderRadius;
|
|
|
|
/// Base color
|
|
final Color? baseColor;
|
|
|
|
/// Highlight color
|
|
final Color? highlightColor;
|
|
|
|
const ShimmerLoading({
|
|
super.key,
|
|
this.width,
|
|
this.height = 16,
|
|
this.borderRadius = 4,
|
|
this.baseColor,
|
|
this.highlightColor,
|
|
});
|
|
|
|
@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(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: 1500),
|
|
)..repeat();
|
|
|
|
_animation = Tween<double>(begin: -1.0, end: 2.0).animate(
|
|
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final baseColor = widget.baseColor ?? theme.colorScheme.surfaceVariant;
|
|
final highlightColor =
|
|
widget.highlightColor ?? theme.colorScheme.surface;
|
|
|
|
return AnimatedBuilder(
|
|
animation: _animation,
|
|
builder: (context, child) {
|
|
return Container(
|
|
width: widget.width,
|
|
height: widget.height,
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(widget.borderRadius),
|
|
gradient: LinearGradient(
|
|
begin: Alignment.centerLeft,
|
|
end: Alignment.centerRight,
|
|
colors: [
|
|
baseColor,
|
|
highlightColor,
|
|
baseColor,
|
|
],
|
|
stops: [
|
|
0.0,
|
|
_animation.value,
|
|
1.0,
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Loading state for list items
|
|
class ListLoadingIndicator extends StatelessWidget {
|
|
/// Number of shimmer items to show
|
|
final int itemCount;
|
|
|
|
/// Height of each item
|
|
final double itemHeight;
|
|
|
|
/// Spacing between items
|
|
final double spacing;
|
|
|
|
const ListLoadingIndicator({
|
|
super.key,
|
|
this.itemCount = 5,
|
|
this.itemHeight = 80,
|
|
this.spacing = 12,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ListView.separated(
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
itemCount: itemCount,
|
|
separatorBuilder: (context, index) => SizedBox(height: spacing),
|
|
itemBuilder: (context, index) => ShimmerLoading(
|
|
height: itemHeight,
|
|
width: double.infinity,
|
|
borderRadius: 8,
|
|
),
|
|
);
|
|
}
|
|
}
|