material3_expressive_loading_indicator 0.1.0
material3_expressive_loading_indicator: ^0.1.0 copied to clipboard
Material Design 3 expressive loading and progress indicators for Flutter: morphing circular indicator, outlined variant, and wavy linear progress.
example/lib/main.dart
import 'package:material3_expressive_loading_indicator/material3_expressive_loading_indicator.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:material_new_shapes/material_new_shapes.dart';
import 'demo_providers.dart';
void main() {
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Expressive Loading Indicator Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends ConsumerWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final linearProgress = ref.watch(linearDemoProgressProvider);
final linearAmplitude = ref.watch(linearDemoAmplitudeProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Expressive Loading Indicator Demo'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'Default (filled) loading indicator:',
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 16),
const ExpressiveLoadingIndicator(),
const SizedBox(height: 32),
const Text(
'Outlined loading indicator:',
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 16),
const ExpressiveLoadingIndicator(
style: ExpressiveLoadingIndicatorStyle.outlined,
),
const SizedBox(height: 32),
const Text(
'Outlined, custom color & stroke:',
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 16),
const ExpressiveLoadingIndicator(
style: ExpressiveLoadingIndicatorStyle.outlined,
color: Colors.teal,
strokeWidth: 4,
),
const SizedBox(height: 32),
const Text(
'Custom color (filled):',
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 16),
const ExpressiveLoadingIndicator(color: Colors.orange),
const SizedBox(height: 32),
const Text(
'Custom size (filled):',
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 16),
const ExpressiveLoadingIndicator(
constraints: BoxConstraints(
minWidth: 72.0,
minHeight: 72.0,
maxWidth: 72.0,
maxHeight: 72.0,
),
),
const SizedBox(height: 32),
const Text(
'Custom shapes (filled):',
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 16),
ExpressiveLoadingIndicator(
polygons: [
MaterialShapes.softBurst,
MaterialShapes.pill,
MaterialShapes.pentagon,
],
),
const SizedBox(height: 32),
const Text(
'Same shapes (outlined):',
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 16),
ExpressiveLoadingIndicator(
style: ExpressiveLoadingIndicatorStyle.outlined,
polygons: [
MaterialShapes.softBurst,
MaterialShapes.pill,
MaterialShapes.pentagon,
],
),
const SizedBox(height: 40),
const Divider(),
const SizedBox(height: 24),
const Text(
'Expressive linear (M3 wavy)',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
),
const SizedBox(height: 8),
const Text('Indeterminate', style: TextStyle(fontSize: 14)),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: ExpressiveLinearProgressIndicator(),
),
const SizedBox(height: 24),
Text(
'Determinate (${(linearProgress * 100).round()}%) — drag slider',
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: ExpressiveLinearProgressIndicator(value: linearProgress),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Slider(
value: linearProgress,
onChanged: (v) {
_hapticIfSliderBucketChanged(
previous: linearProgress,
next: v,
buckets: 20,
);
ref.read(linearDemoProgressProvider.notifier).state = v;
},
),
),
const SizedBox(height: 24),
Text(
'Taller bar, amplitude ${linearAmplitude.toStringAsFixed(2)}',
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: ExpressiveLinearProgressIndicator(
minHeight: 14,
amplitude: linearAmplitude,
value: 0.72,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Slider(
value: linearAmplitude,
max: 1,
divisions: 20,
label: linearAmplitude.toStringAsFixed(2),
onChanged: (v) {
_hapticIfSliderBucketChanged(
previous: linearAmplitude,
next: v,
buckets: 20,
);
ref.read(linearDemoAmplitudeProvider.notifier).state = v;
},
),
),
],
),
),
),
);
}
}
/// Fires a light selection haptic when [next] crosses into another bucket vs
/// [previous]. Matches discrete [Slider] steps (e.g. [divisions]: 20 → 20 buckets).
///
/// Uses [HapticFeedback.selectionClick], which is implemented for iOS and
/// Android in Flutter; desktop/web typically no-op.
void _hapticIfSliderBucketChanged({
required double previous,
required double next,
required int buckets,
}) {
final prev = (previous * buckets).round();
final nxt = (next * buckets).round();
if (prev != nxt) {
HapticFeedback.selectionClick();
}
}