forked from mirrors/material_3_expressive
Refactor progress indicators to use stateful widgets, enhance animation control, and remove unnecessary subtitles in section cards
This commit is contained in:
parent
687bca8817
commit
b4ccdd7750
11 changed files with 316 additions and 130 deletions
|
|
@ -10,8 +10,6 @@ class ButtonSection extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SectionCard(
|
return SectionCard(
|
||||||
title: 'ButtonM3E',
|
title: 'ButtonM3E',
|
||||||
subtitle:
|
|
||||||
'Generated from enums: variant × size; grouped by shape family.',
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@ class FabSection extends StatelessWidget {
|
||||||
|
|
||||||
return SectionCard(
|
return SectionCard(
|
||||||
title: 'FabM3E',
|
title: 'FabM3E',
|
||||||
subtitle:
|
|
||||||
'Generated from enums: kind × size. Includes Extended FAB examples.',
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,14 @@ class IconButtonSection extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SectionCard(
|
return SectionCard(
|
||||||
title: 'IconButtonM3E',
|
title: 'IconButtonM3E',
|
||||||
subtitle: 'Generated from enums: variant × size (round shape, default width).',
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
for (final variant in IconButtonM3EVariant.values) ...[
|
for (final variant in IconButtonM3EVariant.values) ...[
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
child: Text(variant.name, style: Theme.of(context).textTheme.titleMedium),
|
child: Text(variant.name,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium),
|
||||||
),
|
),
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 12,
|
spacing: 12,
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ class LoadingIndicatorSection extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SectionCard(
|
return SectionCard(
|
||||||
title: 'LoadingIndicatorM3E',
|
title: 'LoadingIndicatorM3E',
|
||||||
subtitle: 'Generated from enums: variant values.',
|
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
spacing: 16,
|
spacing: 16,
|
||||||
runSpacing: 16,
|
runSpacing: 16,
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,11 @@ class _NavigationSectionState extends State<NavigationSection> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final m3e = theme.extension<M3ETheme>() ?? M3ETheme.defaults(theme.colorScheme);
|
final m3e =
|
||||||
|
theme.extension<M3ETheme>() ?? M3ETheme.defaults(theme.colorScheme);
|
||||||
|
|
||||||
return SectionCard(
|
return SectionCard(
|
||||||
title: 'Navigation',
|
title: 'Navigation',
|
||||||
subtitle: 'Generated from enums: NavBar indicator styles and Rail indicator styles.',
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -38,17 +38,33 @@ class _NavigationSectionState extends State<NavigationSection> {
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 4),
|
padding: const EdgeInsets.only(bottom: 4),
|
||||||
child: Text('indicator: ${style.name}', style: theme.textTheme.labelLarge),
|
child: Text('indicator: ${style.name}',
|
||||||
|
style: theme.textTheme.labelLarge),
|
||||||
),
|
),
|
||||||
NavigationBarM3E(
|
NavigationBarM3E(
|
||||||
selectedIndex: _barIndex,
|
selectedIndex: _barIndex,
|
||||||
onDestinationSelected: (i) => setState(() => _barIndex = i),
|
onDestinationSelected: (i) =>
|
||||||
|
setState(() => _barIndex = i),
|
||||||
indicatorStyle: style,
|
indicatorStyle: style,
|
||||||
destinations: const [
|
destinations: const [
|
||||||
NavigationDestinationM3E(icon: Icon(Icons.home_outlined), selectedIcon: Icon(Icons.home), label: 'Home'),
|
NavigationDestinationM3E(
|
||||||
NavigationDestinationM3E(icon: Icon(Icons.search_outlined), selectedIcon: Icon(Icons.search), label: 'Search', badgeDot: true),
|
icon: Icon(Icons.home_outlined),
|
||||||
NavigationDestinationM3E(icon: Icon(Icons.favorite_outline), selectedIcon: Icon(Icons.favorite), label: 'Favorites', badgeCount: 2),
|
selectedIcon: Icon(Icons.home),
|
||||||
NavigationDestinationM3E(icon: Icon(Icons.person_outline), selectedIcon: Icon(Icons.person), label: 'Profile'),
|
label: 'Home'),
|
||||||
|
NavigationDestinationM3E(
|
||||||
|
icon: Icon(Icons.search_outlined),
|
||||||
|
selectedIcon: Icon(Icons.search),
|
||||||
|
label: 'Search',
|
||||||
|
badgeDot: true),
|
||||||
|
NavigationDestinationM3E(
|
||||||
|
icon: Icon(Icons.favorite_outline),
|
||||||
|
selectedIcon: Icon(Icons.favorite),
|
||||||
|
label: 'Favorites',
|
||||||
|
badgeCount: 2),
|
||||||
|
NavigationDestinationM3E(
|
||||||
|
icon: Icon(Icons.person_outline),
|
||||||
|
selectedIcon: Icon(Icons.person),
|
||||||
|
label: 'Profile'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
@ -72,12 +88,22 @@ class _NavigationSectionState extends State<NavigationSection> {
|
||||||
for (final style in RailIndicatorStyle.values) ...[
|
for (final style in RailIndicatorStyle.values) ...[
|
||||||
NavigationRailM3E(
|
NavigationRailM3E(
|
||||||
selectedIndex: _railIndex,
|
selectedIndex: _railIndex,
|
||||||
onDestinationSelected: (i) => setState(() => _railIndex = i),
|
onDestinationSelected: (i) =>
|
||||||
|
setState(() => _railIndex = i),
|
||||||
indicatorStyle: style,
|
indicatorStyle: style,
|
||||||
destinations: const [
|
destinations: const [
|
||||||
RailDestinationM3E(icon: Icon(Icons.dashboard_outlined), selectedIcon: Icon(Icons.dashboard), label: 'Dash'),
|
RailDestinationM3E(
|
||||||
RailDestinationM3E(icon: Icon(Icons.analytics_outlined), selectedIcon: Icon(Icons.analytics), label: 'Reports'),
|
icon: Icon(Icons.dashboard_outlined),
|
||||||
RailDestinationM3E(icon: Icon(Icons.settings_outlined), selectedIcon: Icon(Icons.settings), label: 'Settings'),
|
selectedIcon: Icon(Icons.dashboard),
|
||||||
|
label: 'Dash'),
|
||||||
|
RailDestinationM3E(
|
||||||
|
icon: Icon(Icons.analytics_outlined),
|
||||||
|
selectedIcon: Icon(Icons.analytics),
|
||||||
|
label: 'Reports'),
|
||||||
|
RailDestinationM3E(
|
||||||
|
icon: Icon(Icons.settings_outlined),
|
||||||
|
selectedIcon: Icon(Icons.settings),
|
||||||
|
label: 'Settings'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const VerticalDivider(width: 1),
|
const VerticalDivider(width: 1),
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,49 @@ import 'package:m3e_collection/m3e_collection.dart';
|
||||||
|
|
||||||
import 'section_card.dart';
|
import 'section_card.dart';
|
||||||
|
|
||||||
class ProgressSection extends StatelessWidget {
|
class ProgressSection extends StatefulWidget {
|
||||||
const ProgressSection({super.key});
|
const ProgressSection({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ProgressSection> createState() => _ProgressSectionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProgressSectionState extends State<ProgressSection>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
double? _value = 0.6;
|
||||||
|
late final AnimationController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
vsync: this, duration: const Duration(milliseconds: 2400))
|
||||||
|
..repeat();
|
||||||
|
_controller.addListener(() {
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SectionCard(
|
return SectionCard(
|
||||||
title: 'ProgressIndicatorM3E',
|
title: 'ProgressIndicatorM3E',
|
||||||
subtitle: 'Generated from enums: circular sizes and linear variants.',
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
Text('Value: $_value (set 0 for null)'),
|
||||||
|
SliderM3E(
|
||||||
|
value: _value ?? 0,
|
||||||
|
onChanged: (v) => setState(
|
||||||
|
() => _value = v == 0 ? null : v,
|
||||||
|
),
|
||||||
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
child: Text('Circular - Wavy',
|
child: Text('Circular - Wavy',
|
||||||
|
|
@ -26,12 +58,12 @@ class ProgressSection extends StatelessWidget {
|
||||||
for (final s in CircularProgressM3ESize.values) ...[
|
for (final s in CircularProgressM3ESize.values) ...[
|
||||||
CircularProgressIndicatorM3E(
|
CircularProgressIndicatorM3E(
|
||||||
size: s,
|
size: s,
|
||||||
value: 0.4,
|
value: _value,
|
||||||
shape: ProgressM3EShape.wavy,
|
shape: ProgressM3EShape.wavy,
|
||||||
),
|
),
|
||||||
CircularProgressIndicatorM3E(
|
CircularProgressIndicatorM3E(
|
||||||
size: s,
|
size: s,
|
||||||
value: 0.6,
|
value: _value,
|
||||||
shape: ProgressM3EShape.wavy,
|
shape: ProgressM3EShape.wavy,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -50,13 +82,13 @@ class ProgressSection extends StatelessWidget {
|
||||||
for (final s in CircularProgressM3ESize.values) ...[
|
for (final s in CircularProgressM3ESize.values) ...[
|
||||||
CircularProgressIndicatorM3E(
|
CircularProgressIndicatorM3E(
|
||||||
size: s,
|
size: s,
|
||||||
value: 0.4,
|
value: _value,
|
||||||
shape: ProgressM3EShape.flat,
|
shape: ProgressM3EShape.flat,
|
||||||
),
|
),
|
||||||
CircularProgressIndicatorM3E(
|
CircularProgressIndicatorM3E(
|
||||||
size: s,
|
size: s,
|
||||||
shape: ProgressM3EShape.flat,
|
shape: ProgressM3EShape.flat,
|
||||||
value: 0.6,
|
value: _value,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
@ -74,14 +106,16 @@ class ProgressSection extends StatelessWidget {
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 250,
|
width: 250,
|
||||||
child: LinearProgressIndicatorM3E(
|
child: LinearProgressIndicatorM3E(
|
||||||
value: null,
|
value: _value,
|
||||||
|
size: LinearProgressM3ESize.s,
|
||||||
shape: ProgressM3EShape.wavy,
|
shape: ProgressM3EShape.wavy,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 250,
|
width: 250,
|
||||||
child: LinearProgressIndicatorM3E(
|
child: LinearProgressIndicatorM3E(
|
||||||
value: 0.6,
|
value: _value,
|
||||||
|
size: LinearProgressM3ESize.m,
|
||||||
shape: ProgressM3EShape.wavy,
|
shape: ProgressM3EShape.wavy,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -100,14 +134,16 @@ class ProgressSection extends StatelessWidget {
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 250,
|
width: 250,
|
||||||
child: LinearProgressIndicatorM3E(
|
child: LinearProgressIndicatorM3E(
|
||||||
value: null,
|
value: _value,
|
||||||
|
size: LinearProgressM3ESize.s,
|
||||||
shape: ProgressM3EShape.flat,
|
shape: ProgressM3EShape.flat,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 250,
|
width: 250,
|
||||||
child: LinearProgressIndicatorM3E(
|
child: LinearProgressIndicatorM3E(
|
||||||
value: 0.6,
|
value: _value,
|
||||||
|
size: LinearProgressM3ESize.m,
|
||||||
shape: ProgressM3EShape.flat,
|
shape: ProgressM3EShape.flat,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -18,51 +18,49 @@ class _SliderSectionState extends State<SliderSection> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SectionCard(
|
return SectionCard(
|
||||||
title: 'SliderM3E',
|
title: 'SliderM3E',
|
||||||
subtitle: 'Generated from enums: size × emphasis (round shape).',
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
for (final size in SliderM3ESize.values) ...[
|
for (final size in SliderM3ESize.values) ...[
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
child: Text('size: ${size.name}', style: Theme.of(context).textTheme.titleMedium),
|
child: Text('size: ${size.name}',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium),
|
||||||
),
|
),
|
||||||
Wrap(
|
Wrap(
|
||||||
runSpacing: 12,
|
runSpacing: 12,
|
||||||
children: [
|
children: [
|
||||||
for (final emp in SliderM3EEmphasis.values)
|
Padding(
|
||||||
Padding(
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
child: Column(
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: [
|
SliderM3E(
|
||||||
Text('emphasis: ${emp.name}', style: Theme.of(context).textTheme.labelLarge),
|
value: _value,
|
||||||
SliderM3E(
|
onChanged: (v) => setState(() => _value = v),
|
||||||
value: _value,
|
min: 0,
|
||||||
onChanged: (v) => setState(() => _value = v),
|
max: 100,
|
||||||
min: 0,
|
label: _value.toStringAsFixed(0),
|
||||||
max: 100,
|
size: size,
|
||||||
label: _value.toStringAsFixed(0),
|
emphasis: SliderM3EEmphasis.primary,
|
||||||
size: size,
|
startIcon: const Icon(Icons.volume_mute),
|
||||||
emphasis: emp,
|
endIcon: const Icon(Icons.volume_up),
|
||||||
startIcon: const Icon(Icons.volume_mute),
|
),
|
||||||
endIcon: const Icon(Icons.volume_up),
|
RangeSliderM3E(
|
||||||
|
values: _range,
|
||||||
|
onChanged: (v) => setState(() => _range = v),
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
size: size,
|
||||||
|
emphasis: SliderM3EEmphasis.primary,
|
||||||
|
labels: RangeLabels(
|
||||||
|
_range.start.toStringAsFixed(0),
|
||||||
|
_range.end.toStringAsFixed(0),
|
||||||
),
|
),
|
||||||
RangeSliderM3E(
|
),
|
||||||
values: _range,
|
],
|
||||||
onChanged: (v) => setState(() => _range = v),
|
|
||||||
min: 0,
|
|
||||||
max: 100,
|
|
||||||
size: size,
|
|
||||||
emphasis: emp,
|
|
||||||
labels: RangeLabels(
|
|
||||||
_range.start.toStringAsFixed(0),
|
|
||||||
_range.end.toStringAsFixed(0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,14 @@ class SplitButtonSection extends StatelessWidget {
|
||||||
|
|
||||||
return SectionCard(
|
return SectionCard(
|
||||||
title: 'SplitButtonM3E',
|
title: 'SplitButtonM3E',
|
||||||
subtitle: 'Generated from enums: emphasis × size (round shape).',
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
for (final emphasis in SplitButtonM3EEmphasis.values) ...[
|
for (final emphasis in SplitButtonM3EEmphasis.values) ...[
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
child: Text(emphasis.name, style: Theme.of(context).textTheme.titleMedium),
|
child: Text(emphasis.name,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium),
|
||||||
),
|
),
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 12,
|
spacing: 12,
|
||||||
|
|
|
||||||
|
|
@ -11,20 +11,25 @@ class ToolbarSection extends StatelessWidget {
|
||||||
final actions = [
|
final actions = [
|
||||||
ToolbarActionM3E(icon: Icons.search, onPressed: () {}),
|
ToolbarActionM3E(icon: Icons.search, onPressed: () {}),
|
||||||
ToolbarActionM3E(icon: Icons.share, onPressed: () {}),
|
ToolbarActionM3E(icon: Icons.share, onPressed: () {}),
|
||||||
ToolbarActionM3E(icon: Icons.delete, onPressed: () {}, isDestructive: true, label: 'Delete'),
|
ToolbarActionM3E(
|
||||||
ToolbarActionM3E(icon: Icons.settings, onPressed: () {}, label: 'Settings'),
|
icon: Icons.delete,
|
||||||
|
onPressed: () {},
|
||||||
|
isDestructive: true,
|
||||||
|
label: 'Delete'),
|
||||||
|
ToolbarActionM3E(
|
||||||
|
icon: Icons.settings, onPressed: () {}, label: 'Settings'),
|
||||||
];
|
];
|
||||||
|
|
||||||
return SectionCard(
|
return SectionCard(
|
||||||
title: 'ToolbarM3E',
|
title: 'ToolbarM3E',
|
||||||
subtitle: 'Generated from enums: variant × size (round shape).',
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
for (final variant in ToolbarM3EVariant.values) ...[
|
for (final variant in ToolbarM3EVariant.values) ...[
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
child: Text(variant.name, style: Theme.of(context).textTheme.titleMedium),
|
child: Text(variant.name,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium),
|
||||||
),
|
),
|
||||||
Wrap(
|
Wrap(
|
||||||
runSpacing: 12,
|
runSpacing: 12,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'enums.dart';
|
import 'enums.dart';
|
||||||
|
|
||||||
class CircularProgressIndicatorM3E extends StatelessWidget {
|
class CircularProgressIndicatorM3E extends StatefulWidget {
|
||||||
const CircularProgressIndicatorM3E({
|
const CircularProgressIndicatorM3E({
|
||||||
super.key,
|
super.key,
|
||||||
this.value,
|
this.value,
|
||||||
|
|
@ -22,13 +22,66 @@ class CircularProgressIndicatorM3E extends StatelessWidget {
|
||||||
final Color? trackColor;
|
final Color? trackColor;
|
||||||
final double rotation;
|
final double rotation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CircularProgressIndicatorM3E> createState() =>
|
||||||
|
_CircularProgressIndicatorM3EState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CircularProgressIndicatorM3EState
|
||||||
|
extends State<CircularProgressIndicatorM3E>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController _controller;
|
||||||
|
|
||||||
|
bool get _shouldAnimate {
|
||||||
|
final v = widget.value;
|
||||||
|
return widget.shape == ProgressM3EShape.wavy &&
|
||||||
|
(v == null || (v >= 1.0)) &&
|
||||||
|
widget.rotation == 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 3600),
|
||||||
|
)..addListener(() {
|
||||||
|
if (mounted && _shouldAnimate) setState(() {});
|
||||||
|
});
|
||||||
|
if (_shouldAnimate) {
|
||||||
|
_controller.repeat();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant CircularProgressIndicatorM3E oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (_shouldAnimate) {
|
||||||
|
if (!_controller.isAnimating) _controller.repeat();
|
||||||
|
} else {
|
||||||
|
if (_controller.isAnimating) _controller.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final cs = Theme.of(context).colorScheme;
|
final cs = Theme.of(context).colorScheme;
|
||||||
final active = activeColor ?? cs.primary;
|
final active = widget.activeColor ?? cs.primary;
|
||||||
final track = trackColor ?? cs.onSurfaceVariant.withOpacity(0.24);
|
final track = widget.trackColor ?? cs.onSurfaceVariant.withOpacity(0.24);
|
||||||
final wantsWavy = shape == ProgressM3EShape.wavy;
|
final wantsWavy = widget.shape == ProgressM3EShape.wavy;
|
||||||
final diameter = wantsWavy ? size.diameterWavy : size.diameterFlat;
|
final diameter =
|
||||||
|
wantsWavy ? widget.size.diameterWavy : widget.size.diameterFlat;
|
||||||
|
|
||||||
|
final double rot = widget.rotation != 0.0
|
||||||
|
? widget.rotation
|
||||||
|
: (_shouldAnimate ? _controller.value * 2 * math.pi : 0.0);
|
||||||
|
|
||||||
return RepaintBoundary(
|
return RepaintBoundary(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: diameter,
|
width: diameter,
|
||||||
|
|
@ -36,16 +89,16 @@ class CircularProgressIndicatorM3E extends StatelessWidget {
|
||||||
child: CustomPaint(
|
child: CustomPaint(
|
||||||
painter: wantsWavy
|
painter: wantsWavy
|
||||||
? _CircularWavyPainter(
|
? _CircularWavyPainter(
|
||||||
value: value,
|
value: widget.value,
|
||||||
active: active,
|
active: active,
|
||||||
track: track,
|
track: track,
|
||||||
rotation: rotation)
|
rotation: rot)
|
||||||
: _CircularFlatPainter(
|
: _CircularFlatPainter(
|
||||||
value: value,
|
value: widget.value,
|
||||||
active: active,
|
active: active,
|
||||||
track: track,
|
track: track,
|
||||||
rotation: rotation,
|
rotation: rot,
|
||||||
size: size),
|
size: widget.size),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -137,35 +190,36 @@ class _CircularWavyPainter extends CustomPainter {
|
||||||
final center = s.center(Offset.zero);
|
final center = s.center(Offset.zero);
|
||||||
final baseRadius = (math.min(s.width, s.height) - stroke) / 2;
|
final baseRadius = (math.min(s.width, s.height) - stroke) / 2;
|
||||||
|
|
||||||
// Squiggle clearance: 2dp (edge-to-edge). Approximate by insetting the squiggle centerline by 6dp.
|
|
||||||
final clearance = 2.0;
|
|
||||||
final squiggleRadius =
|
|
||||||
baseRadius - (stroke / 2 + clearance + stroke / 2); // baseRadius - 6
|
|
||||||
final amp = 2.0; // radial amplitude of squiggle
|
final amp = 2.0; // radial amplitude of squiggle
|
||||||
final scallopLen = 18.0; // along-arc wavelength proxy (dp)
|
final scallopLen = 18.0; // along-arc wavelength proxy (dp)
|
||||||
|
// Taper length to fade the wave amplitude to zero near the end so the line ends "closed".
|
||||||
|
final taperLen = scallopLen / 2;
|
||||||
|
|
||||||
// Active sweep
|
// Active sweep
|
||||||
final activeSweep =
|
final activeSweep =
|
||||||
value == null ? math.pi * 1.5 : (value!.clamp(0.0, 1.0) * math.pi * 2);
|
value == null ? math.pi * 2 : (value!.clamp(0.0, 1.0) * math.pi * 2);
|
||||||
final start = -math.pi / 2 + rotation;
|
final start = -math.pi / 2 + rotation;
|
||||||
final end = start + activeSweep;
|
final end = start + activeSweep;
|
||||||
|
|
||||||
// Track ring with gap around active
|
// Track ring with gap around active (skip when wave-only: indeterminate or 100%)
|
||||||
final trackPaint = Paint()
|
final bool waveOnly = value == null || (value != null && value! >= 1.0);
|
||||||
..style = PaintingStyle.stroke
|
if (!waveOnly) {
|
||||||
..strokeWidth = stroke
|
final trackPaint = Paint()
|
||||||
..strokeCap = StrokeCap.round
|
..style = PaintingStyle.stroke
|
||||||
..isAntiAlias = true
|
..strokeWidth = stroke
|
||||||
..color = track;
|
..strokeCap = StrokeCap.round
|
||||||
|
..isAntiAlias = true
|
||||||
|
..color = track;
|
||||||
|
|
||||||
final gapAngle = 2.0 / baseRadius;
|
final gapAngle = 2.0 / baseRadius;
|
||||||
final rect = Rect.fromCircle(center: center, radius: baseRadius);
|
final rect = Rect.fromCircle(center: center, radius: baseRadius);
|
||||||
final total = math.pi * 2;
|
final total = math.pi * 2;
|
||||||
final a1 = end + gapAngle;
|
final a1 = end + gapAngle;
|
||||||
final a2 = start - gapAngle;
|
final a2 = start - gapAngle;
|
||||||
double sweep1 = (a2 - a1);
|
double sweep1 = (a2 - a1);
|
||||||
while (sweep1 <= 0) sweep1 += total;
|
while (sweep1 <= 0) sweep1 += total;
|
||||||
canvas.drawArc(rect, a1, sweep1, false, trackPaint);
|
canvas.drawArc(rect, a1, sweep1, false, trackPaint);
|
||||||
|
}
|
||||||
|
|
||||||
// Active squiggle path
|
// Active squiggle path
|
||||||
final steps = math.max(48, (s.width * 1.2).round());
|
final steps = math.max(48, (s.width * 1.2).round());
|
||||||
|
|
@ -173,9 +227,17 @@ class _CircularWavyPainter extends CustomPainter {
|
||||||
for (int i = 0; i <= steps; i++) {
|
for (int i = 0; i <= steps; i++) {
|
||||||
final t = i / steps;
|
final t = i / steps;
|
||||||
final ang = start + (end - start) * t;
|
final ang = start + (end - start) * t;
|
||||||
final arcLen = squiggleRadius * (ang - start);
|
final arcLen = baseRadius * (ang - start);
|
||||||
final r =
|
// Fade amplitude to 0 near the end so the path ends on the base radius (closed look).
|
||||||
squiggleRadius + amp * math.sin(arcLen / scallopLen * 2 * math.pi);
|
final arcToEnd = baseRadius * (end - ang);
|
||||||
|
double taperFactor = 1.0;
|
||||||
|
if (arcToEnd < taperLen) {
|
||||||
|
final tEnd = (arcToEnd / taperLen).clamp(0.0, 1.0);
|
||||||
|
// Ease-out to 0 at the very end.
|
||||||
|
taperFactor = math.sin(tEnd * math.pi / 2);
|
||||||
|
}
|
||||||
|
final r = baseRadius +
|
||||||
|
(amp * taperFactor) * math.sin(arcLen / scallopLen * 2 * math.pi);
|
||||||
final p =
|
final p =
|
||||||
Offset(center.dx + r * math.cos(ang), center.dy + r * math.sin(ang));
|
Offset(center.dx + r * math.cos(ang), center.dy + r * math.sin(ang));
|
||||||
if (i == 0)
|
if (i == 0)
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,15 @@ import 'enums.dart';
|
||||||
|
|
||||||
/// Linear indicator that renders two **separate lanes** (active above, track below)
|
/// Linear indicator that renders two **separate lanes** (active above, track below)
|
||||||
/// with a fixed vertical gap. Lanes never overlap.
|
/// with a fixed vertical gap. Lanes never overlap.
|
||||||
class LinearProgressIndicatorM3E extends StatelessWidget {
|
class LinearProgressIndicatorM3E extends StatefulWidget {
|
||||||
const LinearProgressIndicatorM3E({
|
const LinearProgressIndicatorM3E({
|
||||||
super.key,
|
super.key,
|
||||||
this.value, // null => indeterminate; animate phase externally
|
this.value, // null => indeterminate
|
||||||
this.size = LinearProgressM3ESize.m,
|
this.size = LinearProgressM3ESize.m,
|
||||||
this.shape = ProgressM3EShape.wavy,
|
this.shape = ProgressM3EShape.wavy,
|
||||||
this.activeColor,
|
this.activeColor,
|
||||||
this.trackColor,
|
this.trackColor,
|
||||||
this.phase = 0.0, // radians for wavy animation
|
this.phase = 0.0, // radians for wavy animation (external override)
|
||||||
this.inset = 4.0, // horizontal left inset
|
this.inset = 4.0, // horizontal left inset
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -28,6 +28,52 @@ class LinearProgressIndicatorM3E extends StatelessWidget {
|
||||||
final double phase;
|
final double phase;
|
||||||
final double inset;
|
final double inset;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<LinearProgressIndicatorM3E> createState() =>
|
||||||
|
_LinearProgressIndicatorM3EState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LinearProgressIndicatorM3EState extends State<LinearProgressIndicatorM3E>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController _controller;
|
||||||
|
|
||||||
|
bool get _shouldAnimate {
|
||||||
|
final v = widget.value;
|
||||||
|
return widget.shape == ProgressM3EShape.wavy &&
|
||||||
|
(v == null || (v >= 1.0)) &&
|
||||||
|
widget.phase == 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 1200),
|
||||||
|
)..addListener(() {
|
||||||
|
if (mounted && _shouldAnimate) setState(() {});
|
||||||
|
});
|
||||||
|
if (_shouldAnimate) {
|
||||||
|
_controller.repeat();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant LinearProgressIndicatorM3E oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (_shouldAnimate) {
|
||||||
|
if (!_controller.isAnimating) _controller.repeat();
|
||||||
|
} else {
|
||||||
|
if (_controller.isAnimating) _controller.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
@ -35,16 +81,21 @@ class LinearProgressIndicatorM3E extends StatelessWidget {
|
||||||
theme.extension<M3ETheme>() ?? M3ETheme.defaults(theme.colorScheme);
|
theme.extension<M3ETheme>() ?? M3ETheme.defaults(theme.colorScheme);
|
||||||
|
|
||||||
// Farben aus m3e_design beziehen (überschreibbar per Props)
|
// Farben aus m3e_design beziehen (überschreibbar per Props)
|
||||||
final active = activeColor ?? m3e.colors.primary;
|
final active = widget.activeColor ?? m3e.colors.primary;
|
||||||
final track = trackColor ?? m3e.colors.surfaceContainerHighest;
|
final track = widget.trackColor ?? m3e.colors.surfaceContainerHighest;
|
||||||
|
|
||||||
final spec = specForLinear(size: size, shape: shape);
|
final spec = specForLinear(size: widget.size, shape: widget.shape);
|
||||||
|
|
||||||
// Total height = active lane height (trackHeight or wavyHeight) + gap + trackHeight
|
// Total height equals the taller of the two strokes sharing the same baseline.
|
||||||
|
// For wavy, add vertical amplitude; for flat, it's just the trackHeight.
|
||||||
final activeHeight = spec.isWavy
|
final activeHeight = spec.isWavy
|
||||||
? (spec.trackHeight + 2 * spec.waveAmplitude)
|
? (spec.trackHeight + 2 * spec.waveAmplitude)
|
||||||
: spec.trackHeight;
|
: spec.trackHeight;
|
||||||
final totalHeight = activeHeight + spec.gap + spec.trackHeight;
|
final totalHeight = activeHeight;
|
||||||
|
|
||||||
|
final double phaseValue = widget.phase != 0.0
|
||||||
|
? widget.phase
|
||||||
|
: (_shouldAnimate ? _controller.value * 2 * math.pi : 0.0);
|
||||||
|
|
||||||
return RepaintBoundary(
|
return RepaintBoundary(
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
|
|
@ -52,12 +103,12 @@ class LinearProgressIndicatorM3E extends StatelessWidget {
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: CustomPaint(
|
child: CustomPaint(
|
||||||
painter: _LinearPainter(
|
painter: _LinearPainter(
|
||||||
value: value,
|
value: widget.value,
|
||||||
spec: spec,
|
spec: spec,
|
||||||
active: activeColor ?? active,
|
active: widget.activeColor ?? active,
|
||||||
track: trackColor ?? track,
|
track: widget.trackColor ?? track,
|
||||||
phase: phase,
|
phase: phaseValue,
|
||||||
inset: inset,
|
inset: widget.inset,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -88,13 +139,10 @@ class _LinearPainter extends CustomPainter {
|
||||||
final right = size.width - spec.trailingMargin;
|
final right = size.width - spec.trailingMargin;
|
||||||
final width = math.max(0.0, right - left);
|
final width = math.max(0.0, right - left);
|
||||||
|
|
||||||
// lane centers: active on top, track on bottom
|
// both strokes share the same baseline (centerline)
|
||||||
final trackCy = size.height - spec.trackHeight / 2;
|
final cy = size.height / 2;
|
||||||
final activeHeight = spec.isWavy
|
final trackCy = cy;
|
||||||
? (spec.trackHeight + 2 * spec.waveAmplitude)
|
final activeCy = cy;
|
||||||
: spec.trackHeight;
|
|
||||||
final activeCy =
|
|
||||||
trackCy - (spec.trackHeight / 2 + spec.gap + activeHeight / 2);
|
|
||||||
|
|
||||||
// --- Draw track lane (flat pill) ---
|
// --- Draw track lane (flat pill) ---
|
||||||
final base = Paint()
|
final base = Paint()
|
||||||
|
|
@ -103,11 +151,25 @@ class _LinearPainter extends CustomPainter {
|
||||||
..strokeCap = StrokeCap.round
|
..strokeCap = StrokeCap.round
|
||||||
..isAntiAlias = true;
|
..isAntiAlias = true;
|
||||||
|
|
||||||
canvas.drawLine(
|
// compute progress fraction early for both lanes
|
||||||
Offset(left, trackCy), Offset(right, trackCy), base..color = track);
|
final double p = (value ?? 0).clamp(0.0, 1.0);
|
||||||
|
|
||||||
|
// Wave-only mode: in wavy shape, when indeterminate or full (100%),
|
||||||
|
// hide the track and end-dot; show only the wave which is animated via phase.
|
||||||
|
final bool waveOnly = spec.isWavy && (value == null || p >= 1.0);
|
||||||
|
|
||||||
|
// Track occupies the remaining segment to the right of the active,
|
||||||
|
// leaving a fixed inter-stroke gap. For indeterminate, fill full width.
|
||||||
|
final double activeEndX = value == null ? right : (left + width * p);
|
||||||
|
final double trackStartX =
|
||||||
|
value == null ? left : math.min(right, activeEndX + spec.gap);
|
||||||
|
|
||||||
|
if (!waveOnly) {
|
||||||
|
canvas.drawLine(Offset(trackStartX, trackCy), Offset(right, trackCy),
|
||||||
|
base..color = track);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Active lane ---
|
// --- Active lane ---
|
||||||
final double p = (value ?? 0).clamp(0.0, 1.0);
|
|
||||||
if (spec.isWavy) {
|
if (spec.isWavy) {
|
||||||
// wavy centerline
|
// wavy centerline
|
||||||
final start = left;
|
final start = left;
|
||||||
|
|
@ -134,10 +196,12 @@ class _LinearPainter extends CustomPainter {
|
||||||
..color = active
|
..color = active
|
||||||
..strokeWidth = spec.trackHeight);
|
..strokeWidth = spec.trackHeight);
|
||||||
|
|
||||||
// end dot (non-overlapping, placed slightly before end)
|
// end dot: accent at far right end of the track (shared baseline)
|
||||||
final dotCenterX = math.max(start, end - spec.dotOffset);
|
if (!waveOnly) {
|
||||||
canvas.drawCircle(
|
final dotCenterX = math.max(left, right - spec.dotOffset);
|
||||||
Offset(dotCenterX, y), spec.dotDiameter / 2, Paint()..color = active);
|
canvas.drawCircle(Offset(dotCenterX, trackCy), spec.dotDiameter / 2,
|
||||||
|
Paint()..color = active);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// flat active pill + end dot
|
// flat active pill + end dot
|
||||||
final start = left;
|
final start = left;
|
||||||
|
|
@ -148,8 +212,8 @@ class _LinearPainter extends CustomPainter {
|
||||||
base
|
base
|
||||||
..color = active
|
..color = active
|
||||||
..strokeWidth = spec.trackHeight);
|
..strokeWidth = spec.trackHeight);
|
||||||
final dotCenterX = math.max(start, end - spec.dotOffset);
|
final dotCenterX = math.max(left, right - spec.dotOffset);
|
||||||
canvas.drawCircle(Offset(dotCenterX, activeCy), spec.dotDiameter / 2,
|
canvas.drawCircle(Offset(dotCenterX, trackCy), spec.dotDiameter / 2,
|
||||||
Paint()..color = active);
|
Paint()..color = active);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue