forked from mirrors/material_3_expressive
Update pubspec dependencies, refactor progress indicators, and enhance README documentation
This commit is contained in:
parent
020db0ac38
commit
687bca8817
18 changed files with 1013 additions and 916 deletions
|
|
@ -23,15 +23,17 @@ class ProgressSection extends StatelessWidget {
|
|||
spacing: 16,
|
||||
runSpacing: 16,
|
||||
children: [
|
||||
for (final s in ProgressM3ESize.values) ...[
|
||||
CircularProgressM3E(
|
||||
for (final s in CircularProgressM3ESize.values) ...[
|
||||
CircularProgressIndicatorM3E(
|
||||
size: s,
|
||||
value: 0.4,
|
||||
shape: ProgressM3EShape.wavy,
|
||||
),
|
||||
CircularProgressM3E(
|
||||
CircularProgressIndicatorM3E(
|
||||
size: s,
|
||||
value: 0.6,
|
||||
showCenterLabel: s != ProgressM3ESize.small),
|
||||
shape: ProgressM3EShape.wavy,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
|
@ -45,17 +47,17 @@ class ProgressSection extends StatelessWidget {
|
|||
spacing: 16,
|
||||
runSpacing: 16,
|
||||
children: [
|
||||
for (final s in ProgressM3ESize.values) ...[
|
||||
CircularProgressM3E(
|
||||
for (final s in CircularProgressM3ESize.values) ...[
|
||||
CircularProgressIndicatorM3E(
|
||||
size: s,
|
||||
value: 0.4,
|
||||
shape: CircularBarShapeM3E.flat,
|
||||
shape: ProgressM3EShape.flat,
|
||||
),
|
||||
CircularProgressM3E(
|
||||
CircularProgressIndicatorM3E(
|
||||
size: s,
|
||||
shape: CircularBarShapeM3E.flat,
|
||||
shape: ProgressM3EShape.flat,
|
||||
value: 0.6,
|
||||
showCenterLabel: s != ProgressM3ESize.small),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
|
@ -69,13 +71,19 @@ class ProgressSection extends StatelessWidget {
|
|||
spacing: 16,
|
||||
runSpacing: 16,
|
||||
children: [
|
||||
for (final v in LinearProgressM3EVariant.values)
|
||||
LinearProgressM3E(
|
||||
minWidth: 220,
|
||||
variant: v,
|
||||
value: v == LinearProgressM3EVariant.determinate ? 0.6 : null,
|
||||
bufferValue:
|
||||
v == LinearProgressM3EVariant.buffer ? 0.8 : null,
|
||||
SizedBox(
|
||||
width: 250,
|
||||
child: LinearProgressIndicatorM3E(
|
||||
value: null,
|
||||
shape: ProgressM3EShape.wavy,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 250,
|
||||
child: LinearProgressIndicatorM3E(
|
||||
value: 0.6,
|
||||
shape: ProgressM3EShape.wavy,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -89,14 +97,19 @@ class ProgressSection extends StatelessWidget {
|
|||
spacing: 16,
|
||||
runSpacing: 16,
|
||||
children: [
|
||||
for (final v in LinearProgressM3EVariant.values)
|
||||
LinearProgressM3E(
|
||||
minWidth: 220,
|
||||
variant: v,
|
||||
shape: LinearBarShapeM3E.flat,
|
||||
value: v == LinearProgressM3EVariant.determinate ? 0.6 : null,
|
||||
bufferValue:
|
||||
v == LinearProgressM3EVariant.buffer ? 0.8 : null,
|
||||
SizedBox(
|
||||
width: 250,
|
||||
child: LinearProgressIndicatorM3E(
|
||||
value: null,
|
||||
shape: ProgressM3EShape.flat,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 250,
|
||||
child: LinearProgressIndicatorM3E(
|
||||
value: 0.6,
|
||||
shape: ProgressM3EShape.flat,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# m3e_collection
|
||||
|
||||
Single import that re-exports all M3E component packages plus `m3e_design`.
|
||||
|
||||
The packages `material_new_shapes` by [ulims](https://github.com/ulims) and `expressive_refresh` by [alvaronp](https://github.com/alvaronp) are reexported to complete the collection.
|
||||
|
|
@ -3,6 +3,7 @@ library m3e_collection;
|
|||
export 'package:app_bar_m3e/app_bar_m3e.dart';
|
||||
export 'package:button_group_m3e/button_group_m3e.dart';
|
||||
export 'package:button_m3e/button_m3e.dart';
|
||||
export 'package:expressive_refresh/expressive_refresh.dart';
|
||||
export 'package:fab_m3e/fab_m3e.dart';
|
||||
export 'package:icon_button_m3e/icon_button_m3e.dart';
|
||||
export 'package:loading_indicator_m3e/loading_indicator_m3e.dart';
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ dependencies:
|
|||
flutter:
|
||||
sdk: flutter
|
||||
material_new_shapes: ^1.0.0
|
||||
expressive_refresh: ^0.1.2
|
||||
m3e_design:
|
||||
path: ../m3e_design
|
||||
icon_button_m3e:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,27 @@
|
|||
// Material 3 Expressive — Color Tokens for Flutter
|
||||
// -------------------------------------------------
|
||||
// This file defines an app-level color token object (M3EColors) that wraps
|
||||
// Flutter's ColorScheme (M3) and adds expressive/custom roles (info, success,
|
||||
// warning, danger, emphasis, strong surface/outline), with safe fallbacks for
|
||||
// older Flutter SDKs. It also includes helpers for dynamic color and
|
||||
// harmonization per M3 guidance.
|
||||
//
|
||||
// References:
|
||||
// - M3 color system & roles: https://m3.material.io/styles/color/roles
|
||||
// - How the system works (tones, surfaces): https://m3.material.io/styles/color/system/how-the-system-works
|
||||
// - Define new colors (custom colors + harmonization): https://m3.material.io/styles/color/advanced/define-new-colors
|
||||
// - Codelab (customizing Material color): https://codelabs.developers.google.com/customizing-material-color
|
||||
// - dynamic_color package: https://pub.dev/packages/dynamic_color
|
||||
|
||||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'package:dynamic_color/dynamic_color.dart'
|
||||
as dyn; // for .harmonizeWith and DynamicColorBuilder
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@immutable
|
||||
class M3EColors {
|
||||
// --- Expressive / semantic extras ---------------------------------------
|
||||
final Color emphasis;
|
||||
final Color onEmphasis;
|
||||
final Color info;
|
||||
|
|
@ -13,7 +33,7 @@ class M3EColors {
|
|||
final Color onSurfaceStrong;
|
||||
final Color outlineStrong;
|
||||
|
||||
// New: proxy common ColorScheme fields used across packages
|
||||
// --- Core ColorScheme proxies (subset used across packages) --------------
|
||||
final Color primary;
|
||||
final Color onPrimary;
|
||||
final Color primaryContainer;
|
||||
|
|
@ -41,11 +61,41 @@ class M3EColors {
|
|||
final Color outline;
|
||||
final Color outlineVariant;
|
||||
|
||||
// New: container surface tokens not always present on older ColorScheme
|
||||
final Color surfaceContainerHigh;
|
||||
// --- Tone-based surfaces (M3) --------------------------------------------
|
||||
final Color surfaceDim;
|
||||
final Color surfaceBright;
|
||||
final Color surfaceContainerLowest;
|
||||
final Color surfaceContainerLow;
|
||||
final Color surfaceContainer;
|
||||
final Color surfaceContainerHigh;
|
||||
final Color surfaceContainerHighest;
|
||||
|
||||
// --- Inverse & overlays ---------------------------------------------------
|
||||
final Color inverseSurface;
|
||||
final Color onInverseSurface;
|
||||
final Color inversePrimary;
|
||||
final Color surfaceTint;
|
||||
final Color shadow;
|
||||
final Color scrim;
|
||||
|
||||
// --- Optional fixed accent roles (present in newer Flutter SDKs) ---------
|
||||
final Color? primaryFixed;
|
||||
final Color? primaryFixedDim;
|
||||
final Color? onPrimaryFixed;
|
||||
final Color? onPrimaryFixedVariant;
|
||||
|
||||
final Color? secondaryFixed;
|
||||
final Color? secondaryFixedDim;
|
||||
final Color? onSecondaryFixed;
|
||||
final Color? onSecondaryFixedVariant;
|
||||
|
||||
final Color? tertiaryFixed;
|
||||
final Color? tertiaryFixedDim;
|
||||
final Color? onTertiaryFixed;
|
||||
final Color? onTertiaryFixedVariant;
|
||||
|
||||
const M3EColors({
|
||||
// expressive
|
||||
required this.emphasis,
|
||||
required this.onEmphasis,
|
||||
required this.info,
|
||||
|
|
@ -55,7 +105,7 @@ class M3EColors {
|
|||
required this.surfaceStrong,
|
||||
required this.onSurfaceStrong,
|
||||
required this.outlineStrong,
|
||||
// New fields
|
||||
// core
|
||||
required this.primary,
|
||||
required this.onPrimary,
|
||||
required this.primaryContainer,
|
||||
|
|
@ -77,34 +127,234 @@ class M3EColors {
|
|||
required this.onErrorContainer,
|
||||
required this.outline,
|
||||
required this.outlineVariant,
|
||||
required this.surfaceContainerHigh,
|
||||
// tone-based surfaces
|
||||
required this.surfaceDim,
|
||||
required this.surfaceBright,
|
||||
required this.surfaceContainerLowest,
|
||||
required this.surfaceContainerLow,
|
||||
required this.surfaceContainer,
|
||||
required this.surfaceContainerHigh,
|
||||
required this.surfaceContainerHighest,
|
||||
// inverse & overlays
|
||||
required this.inverseSurface,
|
||||
required this.onInverseSurface,
|
||||
required this.inversePrimary,
|
||||
required this.surfaceTint,
|
||||
required this.shadow,
|
||||
required this.scrim,
|
||||
// fixed accents (nullable for SDK-compat)
|
||||
this.primaryFixed,
|
||||
this.primaryFixedDim,
|
||||
this.onPrimaryFixed,
|
||||
this.onPrimaryFixedVariant,
|
||||
this.secondaryFixed,
|
||||
this.secondaryFixedDim,
|
||||
this.onSecondaryFixed,
|
||||
this.onSecondaryFixedVariant,
|
||||
this.tertiaryFixed,
|
||||
this.tertiaryFixedDim,
|
||||
this.onTertiaryFixed,
|
||||
this.onTertiaryFixedVariant,
|
||||
});
|
||||
|
||||
/// Build from a Flutter [ColorScheme].
|
||||
///
|
||||
/// Includes safe fallbacks for tone-based surfaces and fixed roles if the
|
||||
/// running SDK predates those fields.
|
||||
factory M3EColors.from(ColorScheme s) {
|
||||
// Compute container surface variants if not available on the ColorScheme version in use.
|
||||
// We prefer mild blends that work in both light/dark.
|
||||
Color computeSurfaceContainerHigh() =>
|
||||
Color.alphaBlend(s.primary.withValues(alpha: 0.12), s.surface);
|
||||
Color computeSurfaceContainerLowest() =>
|
||||
Color.alphaBlend(s.onSurface.withValues(alpha: 0.05), s.surface);
|
||||
Color _blend(Color base, Color overlay, double a) =>
|
||||
Color.alphaBlend(overlay.withValues(alpha: a), base);
|
||||
|
||||
// Surface tone fallbacks (approximate, spec-agnostic but pleasant)
|
||||
Color _containerLow() => _blend(s.surface, s.primary, 0.04);
|
||||
Color _container() => _blend(s.surface, s.primary, 0.06);
|
||||
Color _containerHigh() => _blend(s.surface, s.primary, 0.08);
|
||||
Color _containerLowest() => _blend(s.surface, s.onSurface, 0.03);
|
||||
Color _containerHighest() => _blend(s.surface, s.onSurface, 0.10);
|
||||
Color _surfaceDim() => _blend(s.surface, s.onSurface, 0.05);
|
||||
Color _surfaceBright() => _blend(s.surface, s.primary, 0.04);
|
||||
|
||||
Function(Invocation invocation)? _tryGet<T>(
|
||||
ColorScheme scheme, String getter) {
|
||||
try {
|
||||
final dyn = scheme as dynamic;
|
||||
final v =
|
||||
dyn.__noSuchMethod__ == null ? null : null; // keep analyzer happy
|
||||
return (dyn as dynamic)
|
||||
.noSuchMethod; // never executed, trick to silence lints in try
|
||||
} catch (_) {
|
||||
// ignored
|
||||
}
|
||||
// Fallback path using mirrors is not available; we'll use a switch below.
|
||||
return null;
|
||||
}
|
||||
|
||||
// Read new fields via dynamic with try-catch (avoids hard SDK requirement)
|
||||
Color _getOr(Color? c, Color Function() orElse) => c ?? orElse();
|
||||
|
||||
Color? _readColor(String name) {
|
||||
try {
|
||||
final dyn = s as dynamic;
|
||||
return dyn.toJson != null ? null : (dyn as dynamic)[name] as Color?;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// While dynamic lookup above is intentionally conservative, we can directly
|
||||
// access when fields exist in the current SDK, otherwise compute.
|
||||
Color surfaceDim = (() {
|
||||
try {
|
||||
return (s.surfaceDim);
|
||||
} catch (_) {
|
||||
return _surfaceDim();
|
||||
}
|
||||
})();
|
||||
Color surfaceBright = (() {
|
||||
try {
|
||||
return (s.surfaceBright);
|
||||
} catch (_) {
|
||||
return _surfaceBright();
|
||||
}
|
||||
})();
|
||||
Color surfaceContainerLowest = (() {
|
||||
try {
|
||||
return (s.surfaceContainerLowest);
|
||||
} catch (_) {
|
||||
return _containerLowest();
|
||||
}
|
||||
})();
|
||||
Color surfaceContainerLow = (() {
|
||||
try {
|
||||
return (s.surfaceContainerLow);
|
||||
} catch (_) {
|
||||
return _containerLow();
|
||||
}
|
||||
})();
|
||||
Color surfaceContainer = (() {
|
||||
try {
|
||||
return (s.surfaceContainer);
|
||||
} catch (_) {
|
||||
return _container();
|
||||
}
|
||||
})();
|
||||
Color surfaceContainerHigh = (() {
|
||||
try {
|
||||
return (s.surfaceContainerHigh);
|
||||
} catch (_) {
|
||||
return _containerHigh();
|
||||
}
|
||||
})();
|
||||
Color surfaceContainerHighest = (() {
|
||||
try {
|
||||
return (s.surfaceContainerHighest);
|
||||
} catch (_) {
|
||||
return _containerHighest();
|
||||
}
|
||||
})();
|
||||
|
||||
Color inverseSurface = (() {
|
||||
try {
|
||||
return (s.inverseSurface);
|
||||
} catch (_) {
|
||||
return _blend(s.surface, s.onSurface, 0.12);
|
||||
}
|
||||
})();
|
||||
Color onInverseSurface = (() {
|
||||
try {
|
||||
return (s.onInverseSurface);
|
||||
} catch (_) {
|
||||
return s.surface; // decent fallback
|
||||
}
|
||||
})();
|
||||
Color inversePrimary = (() {
|
||||
try {
|
||||
return (s.inversePrimary);
|
||||
} catch (_) {
|
||||
return s.primary; // fallback: same hue
|
||||
}
|
||||
})();
|
||||
Color surfaceTint = (() {
|
||||
try {
|
||||
return (s.surfaceTint);
|
||||
} catch (_) {
|
||||
return s.primary; // fallback
|
||||
}
|
||||
})();
|
||||
Color shadow = (() {
|
||||
try {
|
||||
return (s.shadow);
|
||||
} catch (_) {
|
||||
return const Color(0xFF000000);
|
||||
}
|
||||
})();
|
||||
Color scrim = (() {
|
||||
try {
|
||||
return (s.scrim);
|
||||
} catch (_) {
|
||||
return const Color(0xFF000000).withValues(alpha: 0.32);
|
||||
}
|
||||
})();
|
||||
|
||||
// Fixed roles (nullable)
|
||||
Color? primaryFixed,
|
||||
primaryFixedDim,
|
||||
onPrimaryFixed,
|
||||
onPrimaryFixedVariant,
|
||||
secondaryFixed,
|
||||
secondaryFixedDim,
|
||||
onSecondaryFixed,
|
||||
onSecondaryFixedVariant,
|
||||
tertiaryFixed,
|
||||
tertiaryFixedDim,
|
||||
onTertiaryFixed,
|
||||
onTertiaryFixedVariant;
|
||||
try {
|
||||
primaryFixed = (s as dynamic).primaryFixed as Color?;
|
||||
primaryFixedDim = (s as dynamic).primaryFixedDim as Color?;
|
||||
onPrimaryFixed = (s as dynamic).onPrimaryFixed as Color?;
|
||||
onPrimaryFixedVariant = (s as dynamic).onPrimaryFixedVariant as Color?;
|
||||
|
||||
secondaryFixed = (s as dynamic).secondaryFixed as Color?;
|
||||
secondaryFixedDim = (s as dynamic).secondaryFixedDim as Color?;
|
||||
onSecondaryFixed = (s as dynamic).onSecondaryFixed as Color?;
|
||||
onSecondaryFixedVariant =
|
||||
(s as dynamic).onSecondaryFixedVariant as Color?;
|
||||
|
||||
tertiaryFixed = (s as dynamic).tertiaryFixed as Color?;
|
||||
tertiaryFixedDim = (s as dynamic).tertiaryFixedDim as Color?;
|
||||
onTertiaryFixed = (s as dynamic).onTertiaryFixed as Color?;
|
||||
onTertiaryFixedVariant = (s as dynamic).onTertiaryFixedVariant as Color?;
|
||||
} catch (_) {
|
||||
// Older SDK – leave null
|
||||
}
|
||||
|
||||
// Expressive semantics (harmonized with primary)
|
||||
final Color info = s.tertiary;
|
||||
final Color success =
|
||||
Colors.green.shade600.harmonizeWith(s.primary); // dyn extension
|
||||
final Color warning = Colors.orange.shade700.harmonizeWith(s.primary);
|
||||
final Color danger = s.error; // already semantic
|
||||
|
||||
final Color emphasis = s.primary;
|
||||
final Color onEmphasis = s.onPrimary;
|
||||
|
||||
final Color surfaceStrong = _blend(s.surface, s.primary, 0.06);
|
||||
final Color onSurfaceStrong = s.onSurface;
|
||||
final Color outlineStrong = _blend(s.outline, s.primary, 0.40);
|
||||
|
||||
return M3EColors(
|
||||
emphasis: s.primary,
|
||||
onEmphasis: s.onPrimary,
|
||||
info: s.tertiary,
|
||||
success: Color.alphaBlend(
|
||||
Colors.green.shade400.withValues(alpha: 0.2), s.primaryContainer),
|
||||
warning: Color.alphaBlend(
|
||||
Colors.orange.shade400.withValues(alpha: 0.2), s.secondaryContainer),
|
||||
danger: Color.alphaBlend(
|
||||
Colors.red.shade400.withValues(alpha: 0.2), s.errorContainer),
|
||||
surfaceStrong:
|
||||
Color.alphaBlend(s.primary.withValues(alpha: 0.06), s.surface),
|
||||
onSurfaceStrong: s.onSurface,
|
||||
outlineStrong:
|
||||
Color.alphaBlend(s.primary.withValues(alpha: 0.40), s.outlineVariant),
|
||||
// New fields mapped from ColorScheme
|
||||
// expressive
|
||||
emphasis: emphasis,
|
||||
onEmphasis: onEmphasis,
|
||||
info: info,
|
||||
success: success,
|
||||
warning: warning,
|
||||
danger: danger,
|
||||
surfaceStrong: surfaceStrong,
|
||||
onSurfaceStrong: onSurfaceStrong,
|
||||
outlineStrong: outlineStrong,
|
||||
// core
|
||||
primary: s.primary,
|
||||
onPrimary: s.onPrimary,
|
||||
primaryContainer: s.primaryContainer,
|
||||
|
|
@ -126,25 +376,168 @@ class M3EColors {
|
|||
onErrorContainer: s.onErrorContainer,
|
||||
outline: s.outline,
|
||||
outlineVariant: s.outlineVariant,
|
||||
surfaceContainerHigh: (() {
|
||||
// If the ColorScheme already has a matching field, prefer that via dynamic access; otherwise compute.
|
||||
try {
|
||||
final dynamic dyn = s;
|
||||
final c = dyn.surfaceContainerHigh as Color?;
|
||||
return c ?? computeSurfaceContainerHigh();
|
||||
} catch (_) {
|
||||
return computeSurfaceContainerHigh();
|
||||
// tone-based
|
||||
surfaceDim: surfaceDim,
|
||||
surfaceBright: surfaceBright,
|
||||
surfaceContainerLowest: surfaceContainerLowest,
|
||||
surfaceContainerLow: surfaceContainerLow,
|
||||
surfaceContainer: surfaceContainer,
|
||||
surfaceContainerHigh: surfaceContainerHigh,
|
||||
surfaceContainerHighest: surfaceContainerHighest,
|
||||
// inverse & overlays
|
||||
inverseSurface: inverseSurface,
|
||||
onInverseSurface: onInverseSurface,
|
||||
inversePrimary: inversePrimary,
|
||||
surfaceTint: surfaceTint,
|
||||
shadow: shadow,
|
||||
scrim: scrim,
|
||||
// fixed accents
|
||||
primaryFixed: primaryFixed,
|
||||
primaryFixedDim: primaryFixedDim,
|
||||
onPrimaryFixed: onPrimaryFixed,
|
||||
onPrimaryFixedVariant: onPrimaryFixedVariant,
|
||||
secondaryFixed: secondaryFixed,
|
||||
secondaryFixedDim: secondaryFixedDim,
|
||||
onSecondaryFixed: onSecondaryFixed,
|
||||
onSecondaryFixedVariant: onSecondaryFixedVariant,
|
||||
tertiaryFixed: tertiaryFixed,
|
||||
tertiaryFixedDim: tertiaryFixedDim,
|
||||
onTertiaryFixed: onTertiaryFixed,
|
||||
onTertiaryFixedVariant: onTertiaryFixedVariant,
|
||||
);
|
||||
}
|
||||
})(),
|
||||
surfaceContainerLowest: (() {
|
||||
try {
|
||||
final dynamic dyn = s;
|
||||
final c = dyn.surfaceContainerLowest as Color?;
|
||||
return c ?? computeSurfaceContainerLowest();
|
||||
} catch (_) {
|
||||
return computeSurfaceContainerLowest();
|
||||
|
||||
/// Returns a copy with [info]/[success]/[warning]/[danger] harmonized to the
|
||||
/// given [scheme.primary] (per M3 custom color guidance).
|
||||
M3EColors harmonizedTo(ColorScheme scheme) {
|
||||
Color h(Color c) => c.harmonizeWith(scheme.primary);
|
||||
return copyWith(
|
||||
info: h(info),
|
||||
success: h(success),
|
||||
warning: h(warning),
|
||||
danger: h(danger),
|
||||
emphasis: h(emphasis),
|
||||
);
|
||||
}
|
||||
})(),
|
||||
|
||||
M3EColors copyWith({
|
||||
Color? emphasis,
|
||||
Color? onEmphasis,
|
||||
Color? info,
|
||||
Color? success,
|
||||
Color? warning,
|
||||
Color? danger,
|
||||
Color? surfaceStrong,
|
||||
Color? onSurfaceStrong,
|
||||
Color? outlineStrong,
|
||||
Color? primary,
|
||||
Color? onPrimary,
|
||||
Color? primaryContainer,
|
||||
Color? onPrimaryContainer,
|
||||
Color? secondary,
|
||||
Color? onSecondary,
|
||||
Color? secondaryContainer,
|
||||
Color? onSecondaryContainer,
|
||||
Color? tertiary,
|
||||
Color? onTertiary,
|
||||
Color? tertiaryContainer,
|
||||
Color? onTertiaryContainer,
|
||||
Color? surface,
|
||||
Color? onSurface,
|
||||
Color? onSurfaceVariant,
|
||||
Color? error,
|
||||
Color? onError,
|
||||
Color? errorContainer,
|
||||
Color? onErrorContainer,
|
||||
Color? outline,
|
||||
Color? outlineVariant,
|
||||
Color? surfaceDim,
|
||||
Color? surfaceBright,
|
||||
Color? surfaceContainerLowest,
|
||||
Color? surfaceContainerLow,
|
||||
Color? surfaceContainer,
|
||||
Color? surfaceContainerHigh,
|
||||
Color? surfaceContainerHighest,
|
||||
Color? inverseSurface,
|
||||
Color? onInverseSurface,
|
||||
Color? inversePrimary,
|
||||
Color? surfaceTint,
|
||||
Color? shadow,
|
||||
Color? scrim,
|
||||
Color? primaryFixed,
|
||||
Color? primaryFixedDim,
|
||||
Color? onPrimaryFixed,
|
||||
Color? onPrimaryFixedVariant,
|
||||
Color? secondaryFixed,
|
||||
Color? secondaryFixedDim,
|
||||
Color? onSecondaryFixed,
|
||||
Color? onSecondaryFixedVariant,
|
||||
Color? tertiaryFixed,
|
||||
Color? tertiaryFixedDim,
|
||||
Color? onTertiaryFixed,
|
||||
Color? onTertiaryFixedVariant,
|
||||
}) {
|
||||
return M3EColors(
|
||||
emphasis: emphasis ?? this.emphasis,
|
||||
onEmphasis: onEmphasis ?? this.onEmphasis,
|
||||
info: info ?? this.info,
|
||||
success: success ?? this.success,
|
||||
warning: warning ?? this.warning,
|
||||
danger: danger ?? this.danger,
|
||||
surfaceStrong: surfaceStrong ?? this.surfaceStrong,
|
||||
onSurfaceStrong: onSurfaceStrong ?? this.onSurfaceStrong,
|
||||
outlineStrong: outlineStrong ?? this.outlineStrong,
|
||||
primary: primary ?? this.primary,
|
||||
onPrimary: onPrimary ?? this.onPrimary,
|
||||
primaryContainer: primaryContainer ?? this.primaryContainer,
|
||||
onPrimaryContainer: onPrimaryContainer ?? this.onPrimaryContainer,
|
||||
secondary: secondary ?? this.secondary,
|
||||
onSecondary: onSecondary ?? this.onSecondary,
|
||||
secondaryContainer: secondaryContainer ?? this.secondaryContainer,
|
||||
onSecondaryContainer: onSecondaryContainer ?? this.onSecondaryContainer,
|
||||
tertiary: tertiary ?? this.tertiary,
|
||||
onTertiary: onTertiary ?? this.onTertiary,
|
||||
tertiaryContainer: tertiaryContainer ?? this.tertiaryContainer,
|
||||
onTertiaryContainer: onTertiaryContainer ?? this.onTertiaryContainer,
|
||||
surface: surface ?? this.surface,
|
||||
onSurface: onSurface ?? this.onSurface,
|
||||
onSurfaceVariant: onSurfaceVariant ?? this.onSurfaceVariant,
|
||||
error: error ?? this.error,
|
||||
onError: onError ?? this.onError,
|
||||
errorContainer: errorContainer ?? this.errorContainer,
|
||||
onErrorContainer: onErrorContainer ?? this.onErrorContainer,
|
||||
outline: outline ?? this.outline,
|
||||
outlineVariant: outlineVariant ?? this.outlineVariant,
|
||||
surfaceDim: surfaceDim ?? this.surfaceDim,
|
||||
surfaceBright: surfaceBright ?? this.surfaceBright,
|
||||
surfaceContainerLowest:
|
||||
surfaceContainerLowest ?? this.surfaceContainerLowest,
|
||||
surfaceContainerLow: surfaceContainerLow ?? this.surfaceContainerLow,
|
||||
surfaceContainer: surfaceContainer ?? this.surfaceContainer,
|
||||
surfaceContainerHigh: surfaceContainerHigh ?? this.surfaceContainerHigh,
|
||||
surfaceContainerHighest:
|
||||
surfaceContainerHighest ?? this.surfaceContainerHighest,
|
||||
inverseSurface: inverseSurface ?? this.inverseSurface,
|
||||
onInverseSurface: onInverseSurface ?? this.onInverseSurface,
|
||||
inversePrimary: inversePrimary ?? this.inversePrimary,
|
||||
surfaceTint: surfaceTint ?? this.surfaceTint,
|
||||
shadow: shadow ?? this.shadow,
|
||||
scrim: scrim ?? this.scrim,
|
||||
primaryFixed: primaryFixed ?? this.primaryFixed,
|
||||
primaryFixedDim: primaryFixedDim ?? this.primaryFixedDim,
|
||||
onPrimaryFixed: onPrimaryFixed ?? this.onPrimaryFixed,
|
||||
onPrimaryFixedVariant:
|
||||
onPrimaryFixedVariant ?? this.onPrimaryFixedVariant,
|
||||
secondaryFixed: secondaryFixed ?? this.secondaryFixed,
|
||||
secondaryFixedDim: secondaryFixedDim ?? this.secondaryFixedDim,
|
||||
onSecondaryFixed: onSecondaryFixed ?? this.onSecondaryFixed,
|
||||
onSecondaryFixedVariant:
|
||||
onSecondaryFixedVariant ?? this.onSecondaryFixedVariant,
|
||||
tertiaryFixed: tertiaryFixed ?? this.tertiaryFixed,
|
||||
tertiaryFixedDim: tertiaryFixedDim ?? this.tertiaryFixedDim,
|
||||
onTertiaryFixed: onTertiaryFixed ?? this.onTertiaryFixed,
|
||||
onTertiaryFixedVariant:
|
||||
onTertiaryFixedVariant ?? this.onTertiaryFixedVariant,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -158,7 +551,6 @@ class M3EColors {
|
|||
surfaceStrong: Color.lerp(a.surfaceStrong, b.surfaceStrong, t)!,
|
||||
onSurfaceStrong: Color.lerp(a.onSurfaceStrong, b.onSurfaceStrong, t)!,
|
||||
outlineStrong: Color.lerp(a.outlineStrong, b.outlineStrong, t)!,
|
||||
// New fields
|
||||
primary: Color.lerp(a.primary, b.primary, t)!,
|
||||
onPrimary: Color.lerp(a.onPrimary, b.onPrimary, t)!,
|
||||
primaryContainer:
|
||||
|
|
@ -188,9 +580,112 @@ class M3EColors {
|
|||
Color.lerp(a.onErrorContainer, b.onErrorContainer, t)!,
|
||||
outline: Color.lerp(a.outline, b.outline, t)!,
|
||||
outlineVariant: Color.lerp(a.outlineVariant, b.outlineVariant, t)!,
|
||||
surfaceContainerHigh:
|
||||
Color.lerp(a.surfaceContainerHigh, b.surfaceContainerHigh, t)!,
|
||||
surfaceDim: Color.lerp(a.surfaceDim, b.surfaceDim, t)!,
|
||||
surfaceBright: Color.lerp(a.surfaceBright, b.surfaceBright, t)!,
|
||||
surfaceContainerLowest:
|
||||
Color.lerp(a.surfaceContainerLowest, b.surfaceContainerLowest, t)!,
|
||||
surfaceContainerLow:
|
||||
Color.lerp(a.surfaceContainerLow, b.surfaceContainerLow, t)!,
|
||||
surfaceContainer:
|
||||
Color.lerp(a.surfaceContainer, b.surfaceContainer, t)!,
|
||||
surfaceContainerHigh:
|
||||
Color.lerp(a.surfaceContainerHigh, b.surfaceContainerHigh, t)!,
|
||||
surfaceContainerHighest: Color.lerp(
|
||||
a.surfaceContainerHighest, b.surfaceContainerHighest, t)!,
|
||||
inverseSurface: Color.lerp(a.inverseSurface, b.inverseSurface, t)!,
|
||||
onInverseSurface:
|
||||
Color.lerp(a.onInverseSurface, b.onInverseSurface, t)!,
|
||||
inversePrimary: Color.lerp(a.inversePrimary, b.inversePrimary, t)!,
|
||||
surfaceTint: Color.lerp(a.surfaceTint, b.surfaceTint, t)!,
|
||||
shadow: Color.lerp(a.shadow, b.shadow, t)!,
|
||||
scrim: Color.lerp(a.scrim, b.scrim, t)!,
|
||||
primaryFixed: Color.lerp(a.primaryFixed, b.primaryFixed, t),
|
||||
primaryFixedDim: Color.lerp(a.primaryFixedDim, b.primaryFixedDim, t),
|
||||
onPrimaryFixed: Color.lerp(a.onPrimaryFixed, b.onPrimaryFixed, t),
|
||||
onPrimaryFixedVariant:
|
||||
Color.lerp(a.onPrimaryFixedVariant, b.onPrimaryFixedVariant, t),
|
||||
secondaryFixed: Color.lerp(a.secondaryFixed, b.secondaryFixed, t),
|
||||
secondaryFixedDim:
|
||||
Color.lerp(a.secondaryFixedDim, b.secondaryFixedDim, t),
|
||||
onSecondaryFixed: Color.lerp(a.onSecondaryFixed, b.onSecondaryFixed, t),
|
||||
onSecondaryFixedVariant:
|
||||
Color.lerp(a.onSecondaryFixedVariant, b.onSecondaryFixedVariant, t),
|
||||
tertiaryFixed: Color.lerp(a.tertiaryFixed, b.tertiaryFixed, t),
|
||||
tertiaryFixedDim: Color.lerp(a.tertiaryFixedDim, b.tertiaryFixedDim, t),
|
||||
onTertiaryFixed: Color.lerp(a.onTertiaryFixed, b.onTertiaryFixed, t),
|
||||
onTertiaryFixedVariant:
|
||||
Color.lerp(a.onTertiaryFixedVariant, b.onTertiaryFixedVariant, t),
|
||||
);
|
||||
}
|
||||
|
||||
/// Convenience extension to read expressive tokens from any [ColorScheme].
|
||||
extension M3EColorsX on ColorScheme {
|
||||
M3EColors get m3e => M3EColors.from(this);
|
||||
}
|
||||
/*
|
||||
|
||||
/// Theme helpers --------------------------------------------------------------
|
||||
class M3ETheme {
|
||||
M3ETheme._();
|
||||
|
||||
/// Build a [ThemeData] using dynamic colors when available, otherwise
|
||||
/// fall back to [ColorScheme.fromSeed]. Use inside a [dyn.DynamicColorBuilder].
|
||||
static ThemeData themeFromSchemes({
|
||||
required BuildContext context,
|
||||
required ColorScheme lightScheme,
|
||||
required ColorScheme darkScheme,
|
||||
VisualDensity visualDensity = VisualDensity.adaptivePlatformDensity,
|
||||
TargetPlatform? platform,
|
||||
TextTheme? textTheme,
|
||||
bool useMaterial3 = true,
|
||||
}) {
|
||||
return ThemeData(
|
||||
useMaterial3: useMaterial3,
|
||||
colorScheme: lightScheme,
|
||||
brightness: Brightness.light,
|
||||
visualDensity: visualDensity,
|
||||
platform: platform,
|
||||
textTheme: textTheme,
|
||||
);
|
||||
}
|
||||
|
||||
/// Utility to create [ColorScheme] either from platform dynamic colors or a seed.
|
||||
/// Typical usage in your app root:
|
||||
///
|
||||
/// ```dart
|
||||
/// return dyn.DynamicColorBuilder(
|
||||
/// builder: (lightDynamic, darkDynamic) {
|
||||
/// final Color seed = const Color(0xFF6750A4);
|
||||
/// final light = lightDynamic ?? ColorScheme.fromSeed(seedColor: seed);
|
||||
/// final dark = darkDynamic ??
|
||||
/// ColorScheme.fromSeed(seedColor: seed, brightness: Brightness.dark);
|
||||
/// return MaterialApp(theme: ThemeData(colorScheme: light), darkTheme: ThemeData(colorScheme: dark));
|
||||
/// },
|
||||
/// );
|
||||
/// ```
|
||||
static ({ColorScheme light, ColorScheme dark}) dynamicOrSeed(
|
||||
Color seed, {
|
||||
Color? secondarySeed,
|
||||
Color? tertiarySeed,
|
||||
double contrastLevel = 0.0,
|
||||
dyn.DynamicSchemeVariant variant = dyn.DynamicSchemeVariant.tonalSpot,
|
||||
}) {
|
||||
final light = ColorScheme.fromSeed(
|
||||
seedColor: seed,
|
||||
contrastLevel: contrastLevel,
|
||||
dynamicSchemeVariant: variant,
|
||||
secondary: secondarySeed,
|
||||
tertiary: tertiarySeed,
|
||||
);
|
||||
final dark = ColorScheme.fromSeed(
|
||||
seedColor: seed,
|
||||
contrastLevel: contrastLevel,
|
||||
dynamicSchemeVariant: variant,
|
||||
brightness: Brightness.dark,
|
||||
secondary: secondarySeed,
|
||||
tertiary: tertiarySeed,
|
||||
);
|
||||
return (light: light, dark: dark);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ environment:
|
|||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
dynamic_color: ^1.8.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) ...
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -1,65 +1,14 @@
|
|||
# progress_indicators_m3e
|
||||
|
||||
Material 3 **Expressive** progress indicators for Flutter:
|
||||
# progress_indicator_m3e (spec build)
|
||||
|
||||
- `LinearProgressM3E` — determinate, indeterminate, **buffer**, **query**, with **flat** or **wavy** shape
|
||||
- `CircularProgressM3E` — determinate & indeterminate, **flat** or **wavy** stroke (animated for indeterminate)
|
||||
- `ProgressWithLabelM3E` — compose a linear bar with inline/top/bottom/center labels
|
||||
**Visual rules implemented**
|
||||
- Active and track never overlap.
|
||||
- Circular ring is *broken* around the active sweep.
|
||||
- Squiggle variants (48/52) draw a sine-like stroke inside the ring with 2dp clearance.
|
||||
- Linear shows two lanes (active above, track below) with fixed gap and end-dot, per table.
|
||||
|
||||
All widgets read **M3E tokens** (`m3e_design`) for color, sizing, and typography.
|
||||
|
||||
## Defaults (from the spec illustrations)
|
||||
|
||||
- Linear: default thickness 4dp; configurable via `size` or `strokeHeight`
|
||||
- Linear (wavy): `wavelength=40dp`, `amplitude≈height/3`, **4dp** left/right inset
|
||||
- Circular: small/medium/large diameters ≈ 24/32/48 with stroke ≈ 3/4/6
|
||||
- Circular (wavy): default **10 waves** around the circle, amplitude ≈ 35% of stroke
|
||||
|
||||
## Quick start
|
||||
|
||||
```dart
|
||||
import 'package:progress_indicators_m3e/progress_indicators_m3e.dart';
|
||||
|
||||
// Linear (wavy, determinate)
|
||||
LinearProgressM3E(
|
||||
value: 0.62,
|
||||
shape: LinearBarShapeM3E.wavy,
|
||||
);
|
||||
|
||||
// Circular (wavy, indeterminate)
|
||||
const CircularProgressM3E(
|
||||
shape: CircularBarShapeM3E.wavy,
|
||||
);
|
||||
|
||||
// Linear (buffer) flat
|
||||
LinearProgressM3E(
|
||||
variant: LinearProgressM3EVariant.buffer,
|
||||
value: 0.3,
|
||||
bufferValue: 0.6,
|
||||
);
|
||||
|
||||
// Circular (flat) with center label
|
||||
CircularProgressM3E(
|
||||
value: 0.5,
|
||||
showCenterLabel: true,
|
||||
);
|
||||
```
|
||||
|
||||
## Monorepo layout
|
||||
|
||||
```
|
||||
packages/
|
||||
m3e_design/
|
||||
progress_indicators_m3e/
|
||||
```
|
||||
|
||||
`pubspec.yaml` references `../m3e_design`.
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Provide `semanticLabel` and (for determinate) the widgets expose a numeric **value** for screen readers.
|
||||
- Indeterminate wavy animations use modest motion; gate the speed with a future `m3e_design.motion` flag if you support "reduce motion".
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
**Linear variants**
|
||||
- `flatXS` — track 4, gap 4, dot Ø4, dotOffset 4, trailing 4
|
||||
- `flatS` — track 8, gap 4, dot Ø4, dotOffset 2, trailing 8
|
||||
- `wavyM` — track 4, wave amp 3, period 40, gap 4, dot Ø4, dotOffset 2, trailing 10
|
||||
- `wavyL` — track 8, wave amp 3, period 40, gap 4, dot Ø4, dotOffset 2, trailing 14
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
library progress_indicators_m3e;
|
||||
|
||||
library progress_indicator_m3e;
|
||||
|
||||
export 'src/enums.dart';
|
||||
export 'src/tokens_adapter.dart' show ProgressTokensAdapter;
|
||||
export 'src/linear_progress_m3e.dart';
|
||||
export 'src/circular_progress_m3e.dart';
|
||||
export 'src/progress_with_label_m3e.dart';
|
||||
|
|
|
|||
79
packages/progress_indicator_m3e/lib/src/_tokens.dart
Normal file
79
packages/progress_indicator_m3e/lib/src/_tokens.dart
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'enums.dart';
|
||||
|
||||
@immutable
|
||||
class Palette {
|
||||
const Palette(this.cs);
|
||||
final ColorScheme cs;
|
||||
|
||||
// Use theme roles; callers can override colors if needed.
|
||||
Color get active => cs.primary;
|
||||
Color get track => cs.onSurfaceVariant.withOpacity(0.24);
|
||||
Color get bg => cs.surface;
|
||||
}
|
||||
|
||||
@immutable
|
||||
class LinearSpec {
|
||||
const LinearSpec({
|
||||
required this.trackHeight,
|
||||
required this.gap,
|
||||
required this.dotDiameter,
|
||||
required this.dotOffset,
|
||||
required this.trailingMargin,
|
||||
required this.isWavy,
|
||||
this.waveAmplitude = 0,
|
||||
this.wavePeriod = 40,
|
||||
});
|
||||
|
||||
final double trackHeight;
|
||||
final double gap; // vertical space between active lane and track lane
|
||||
final double dotDiameter;
|
||||
final double dotOffset; // center offset from end of active segment
|
||||
final double trailingMargin; // empty space at the far right
|
||||
final bool isWavy;
|
||||
final double waveAmplitude;
|
||||
final double wavePeriod;
|
||||
}
|
||||
|
||||
LinearSpec specForLinear({
|
||||
required LinearProgressM3ESize size,
|
||||
required ProgressM3EShape shape,
|
||||
}) => switch ((shape, size)) {
|
||||
(ProgressM3EShape.flat, LinearProgressM3ESize.s) => const LinearSpec(
|
||||
trackHeight: 4,
|
||||
gap: 4,
|
||||
dotDiameter: 4,
|
||||
dotOffset: 4,
|
||||
trailingMargin: 4,
|
||||
isWavy: false,
|
||||
),
|
||||
(ProgressM3EShape.flat, LinearProgressM3ESize.m) => const LinearSpec(
|
||||
trackHeight: 8,
|
||||
gap: 4,
|
||||
dotDiameter: 4,
|
||||
dotOffset: 2,
|
||||
trailingMargin: 8,
|
||||
isWavy: false,
|
||||
),
|
||||
(ProgressM3EShape.wavy, LinearProgressM3ESize.s) => const LinearSpec(
|
||||
trackHeight: 4,
|
||||
gap: 4,
|
||||
dotDiameter: 4,
|
||||
dotOffset: 2,
|
||||
trailingMargin: 10,
|
||||
isWavy: true,
|
||||
waveAmplitude: 3,
|
||||
wavePeriod: 40,
|
||||
),
|
||||
(ProgressM3EShape.wavy, LinearProgressM3ESize.m) => const LinearSpec(
|
||||
trackHeight: 8,
|
||||
gap: 4,
|
||||
dotDiameter: 4,
|
||||
dotOffset: 2,
|
||||
trailingMargin: 14,
|
||||
isWavy: true,
|
||||
waveAmplitude: 3,
|
||||
wavePeriod: 40,
|
||||
),
|
||||
};
|
||||
|
|
@ -3,284 +3,199 @@ import 'dart:math' as math;
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'enums.dart';
|
||||
import 'tokens_adapter.dart';
|
||||
|
||||
class CircularProgressM3E extends StatefulWidget {
|
||||
const CircularProgressM3E({
|
||||
class CircularProgressIndicatorM3E extends StatelessWidget {
|
||||
const CircularProgressIndicatorM3E({
|
||||
super.key,
|
||||
this.value,
|
||||
this.size = ProgressM3ESize.medium,
|
||||
this.emphasis = ProgressM3EEmphasis.primary,
|
||||
this.density = ProgressM3EDensity.regular,
|
||||
this.backgroundColor,
|
||||
this.strokeWidth,
|
||||
this.semanticLabel,
|
||||
this.showCenterLabel = false,
|
||||
this.centerLabelBuilder,
|
||||
this.shape = CircularBarShapeM3E.wavy,
|
||||
this.waveCount,
|
||||
this.waveAmplitude,
|
||||
this.rotateClockwise = true,
|
||||
this.size = CircularProgressM3ESize.m,
|
||||
this.shape = ProgressM3EShape.wavy,
|
||||
this.activeColor,
|
||||
this.trackColor,
|
||||
this.rotation = 0.0, // radians, for indeterminate rotation
|
||||
});
|
||||
|
||||
/// Determinate value (0..1). If null, renders indeterminate.
|
||||
final double? value;
|
||||
|
||||
final ProgressM3ESize size;
|
||||
final ProgressM3EEmphasis emphasis;
|
||||
final ProgressM3EDensity density;
|
||||
|
||||
final Color? backgroundColor;
|
||||
final double? strokeWidth;
|
||||
|
||||
/// Optional semantics label.
|
||||
final String? semanticLabel;
|
||||
|
||||
/// Show a label centered inside (e.g., percentage).
|
||||
final bool showCenterLabel;
|
||||
|
||||
/// Builder for custom center label; if null and showCenterLabel==true, shows percentage text.
|
||||
final Widget Function(BuildContext context, double? value)?
|
||||
centerLabelBuilder;
|
||||
|
||||
/// Expressive shape
|
||||
final CircularBarShapeM3E shape;
|
||||
final int? waveCount;
|
||||
final double? waveAmplitude;
|
||||
final bool rotateClockwise;
|
||||
|
||||
@override
|
||||
State<CircularProgressM3E> createState() => _CircularProgressM3EState();
|
||||
}
|
||||
|
||||
class _CircularProgressM3EState extends State<CircularProgressM3E>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _anim = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1400),
|
||||
)..repeat();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_anim.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
final double? value; // 0..1 (null => indeterminate arc sweep)
|
||||
final CircularProgressM3ESize size;
|
||||
final ProgressM3EShape shape;
|
||||
final Color? activeColor;
|
||||
final Color? trackColor;
|
||||
final double rotation;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ProgressTokensAdapter(context);
|
||||
final m = tokens.metrics(widget.density);
|
||||
final color = tokens.color(widget.emphasis);
|
||||
final bg = widget.backgroundColor ?? tokens.trackColor();
|
||||
|
||||
final (diameter, stroke) = switch (widget.size) {
|
||||
ProgressM3ESize.small => (
|
||||
m.circularSmall,
|
||||
widget.strokeWidth ?? m.strokeSmall
|
||||
),
|
||||
ProgressM3ESize.medium => (
|
||||
m.circularMedium,
|
||||
widget.strokeWidth ?? m.strokeMedium
|
||||
),
|
||||
ProgressM3ESize.large => (
|
||||
m.circularLarge,
|
||||
widget.strokeWidth ?? m.strokeLarge
|
||||
),
|
||||
};
|
||||
|
||||
final indicator = SizedBox(
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final active = activeColor ?? cs.primary;
|
||||
final track = trackColor ?? cs.onSurfaceVariant.withOpacity(0.24);
|
||||
final wantsWavy = shape == ProgressM3EShape.wavy;
|
||||
final diameter = wantsWavy ? size.diameterWavy : size.diameterFlat;
|
||||
return RepaintBoundary(
|
||||
child: SizedBox(
|
||||
width: diameter,
|
||||
height: diameter,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Track ring
|
||||
CustomPaint(
|
||||
size: Size.square(diameter),
|
||||
painter: _RingPainter(color: bg, stroke: stroke),
|
||||
),
|
||||
// Progress
|
||||
if (widget.shape == CircularBarShapeM3E.flat) ...[
|
||||
CustomPaint(
|
||||
size: Size.square(diameter),
|
||||
painter: _ArcPainter(
|
||||
color: color,
|
||||
stroke: stroke,
|
||||
value: widget.value,
|
||||
clockwise: widget.rotateClockwise,
|
||||
child: CustomPaint(
|
||||
painter: wantsWavy
|
||||
? _CircularWavyPainter(
|
||||
value: value,
|
||||
active: active,
|
||||
track: track,
|
||||
rotation: rotation)
|
||||
: _CircularFlatPainter(
|
||||
value: value,
|
||||
active: active,
|
||||
track: track,
|
||||
rotation: rotation,
|
||||
size: size),
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
AnimatedBuilder(
|
||||
animation: _anim,
|
||||
builder: (context, _) => CustomPaint(
|
||||
size: Size.square(diameter),
|
||||
painter: _WavyArcPainter(
|
||||
color: color,
|
||||
stroke: stroke,
|
||||
value: widget.value,
|
||||
waves: widget.waveCount ?? m.circularWavesPerCircle,
|
||||
amplitude: (widget.waveAmplitude ??
|
||||
(m.circularWaveAmplitudeFactor * stroke))
|
||||
.clamp(0, stroke / 2),
|
||||
phase:
|
||||
(widget.value == null ? 2 * math.pi * _anim.value : 0) *
|
||||
(widget.rotateClockwise ? 1 : -1),
|
||||
clockwise: widget.rotateClockwise,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (widget.showCenterLabel)
|
||||
DefaultTextStyle(
|
||||
style: tokens.labelStyle().copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
child: widget.centerLabelBuilder?.call(context, widget.value) ??
|
||||
Text(widget.value != null
|
||||
? '${(widget.value! * 100).toStringAsFixed(0)}%'
|
||||
: ''),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (widget.semanticLabel == null) return indicator;
|
||||
return Semantics(
|
||||
label: widget.semanticLabel,
|
||||
value: widget.value != null
|
||||
? '${(widget.value! * 100).toStringAsFixed(0)}%'
|
||||
: null,
|
||||
child: indicator,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RingPainter extends CustomPainter {
|
||||
_RingPainter({required this.color, required this.stroke});
|
||||
final Color color;
|
||||
final double stroke;
|
||||
class _CircularFlatPainter extends CustomPainter {
|
||||
_CircularFlatPainter(
|
||||
{required this.value,
|
||||
required this.active,
|
||||
required this.track,
|
||||
required this.rotation,
|
||||
required this.size});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = stroke
|
||||
..strokeCap = StrokeCap.round;
|
||||
final rect = Offset.zero & size;
|
||||
final center = rect.center;
|
||||
final radius = (size.shortestSide - stroke) / 2;
|
||||
canvas.drawCircle(center, radius, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _RingPainter old) =>
|
||||
old.color != color || old.stroke != stroke;
|
||||
}
|
||||
|
||||
class _ArcPainter extends CustomPainter {
|
||||
_ArcPainter({
|
||||
required this.color,
|
||||
required this.stroke,
|
||||
required this.value,
|
||||
required this.clockwise,
|
||||
});
|
||||
|
||||
final Color color;
|
||||
final double stroke;
|
||||
final double? value;
|
||||
final bool clockwise;
|
||||
final Color active;
|
||||
final Color track;
|
||||
final double rotation;
|
||||
final CircularProgressM3ESize size;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
void paint(Canvas canvas, Size s) {
|
||||
final stroke = 4.0;
|
||||
final center = s.center(Offset.zero);
|
||||
final radius = (math.min(s.width, s.height) - stroke) / 2;
|
||||
final rect = Rect.fromCircle(center: center, radius: radius);
|
||||
|
||||
// gap before active in dp -> angle
|
||||
final gapDp = 8.0;
|
||||
final gapAngle = gapDp / radius; // s = r * angle
|
||||
|
||||
// active sweep
|
||||
final sweep =
|
||||
value == null ? math.pi * 1.5 : (value!.clamp(0.0, 1.0) * math.pi * 2);
|
||||
|
||||
final start = -math.pi / 2 + rotation;
|
||||
final activeStart = start;
|
||||
final activeEnd = start + sweep;
|
||||
|
||||
// TRACK: draw the rest of the ring, leaving a gap ahead of the active arc and no overlap.
|
||||
final trackPaint = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = stroke
|
||||
..strokeCap = StrokeCap.round;
|
||||
..strokeCap = StrokeCap.round
|
||||
..isAntiAlias = true
|
||||
..color = track;
|
||||
|
||||
final rect = Offset.zero & size;
|
||||
final center = rect.center;
|
||||
final radius = (size.shortestSide - stroke) / 2;
|
||||
final total = math.pi * 2;
|
||||
final a1 = (activeEnd + gapAngle);
|
||||
final a2 = (activeStart - gapAngle);
|
||||
double sweep1 = (a2 - a1);
|
||||
while (sweep1 <= 0) sweep1 += total;
|
||||
canvas.drawArc(rect, a1, sweep1, false, trackPaint);
|
||||
|
||||
final start = -math.pi / 2;
|
||||
final sweep = (value ?? 0.25) * 2 * math.pi * (clockwise ? 1 : -1);
|
||||
|
||||
canvas.drawArc(Rect.fromCircle(center: center, radius: radius), start,
|
||||
sweep, false, paint);
|
||||
if (value == null) {
|
||||
// indeterminate - draw a moving arc; this painter is used only for determinate (flat)
|
||||
}
|
||||
// ACTIVE arc
|
||||
final activePaint = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = stroke
|
||||
..strokeCap = StrokeCap.round
|
||||
..isAntiAlias = true
|
||||
..color = active;
|
||||
canvas.drawArc(rect, activeStart, sweep, false, activePaint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _ArcPainter old) =>
|
||||
old.color != color ||
|
||||
old.stroke != stroke ||
|
||||
old.value != value ||
|
||||
old.clockwise != clockwise;
|
||||
bool shouldRepaint(covariant _CircularFlatPainter old) =>
|
||||
value != old.value ||
|
||||
active != old.active ||
|
||||
track != old.track ||
|
||||
rotation != old.rotation ||
|
||||
size != old.size;
|
||||
}
|
||||
|
||||
class _WavyArcPainter extends CustomPainter {
|
||||
_WavyArcPainter({
|
||||
required this.color,
|
||||
required this.stroke,
|
||||
required this.value,
|
||||
required this.waves,
|
||||
required this.amplitude,
|
||||
required this.phase,
|
||||
required this.clockwise,
|
||||
});
|
||||
class _CircularWavyPainter extends CustomPainter {
|
||||
_CircularWavyPainter(
|
||||
{required this.value,
|
||||
required this.active,
|
||||
required this.track,
|
||||
required this.rotation});
|
||||
|
||||
final Color color;
|
||||
final double stroke;
|
||||
final double? value;
|
||||
final int waves;
|
||||
final double amplitude;
|
||||
final double phase;
|
||||
final bool clockwise;
|
||||
final Color active;
|
||||
final Color track;
|
||||
final double rotation;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final rect = Offset.zero & size;
|
||||
final center = rect.center;
|
||||
final baseRadius = (size.shortestSide - stroke) / 2;
|
||||
void paint(Canvas canvas, Size s) {
|
||||
const stroke = 4.0;
|
||||
final center = s.center(Offset.zero);
|
||||
final baseRadius = (math.min(s.width, s.height) - stroke) / 2;
|
||||
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
// 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 scallopLen = 18.0; // along-arc wavelength proxy (dp)
|
||||
|
||||
// Active sweep
|
||||
final activeSweep =
|
||||
value == null ? math.pi * 1.5 : (value!.clamp(0.0, 1.0) * math.pi * 2);
|
||||
final start = -math.pi / 2 + rotation;
|
||||
final end = start + activeSweep;
|
||||
|
||||
// Track ring with gap around active
|
||||
final trackPaint = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = stroke
|
||||
..strokeCap = StrokeCap.round;
|
||||
..strokeCap = StrokeCap.round
|
||||
..isAntiAlias = true
|
||||
..color = track;
|
||||
|
||||
final totalAngle = (value ?? 1.0) * 2 * math.pi * (clockwise ? 1 : -1);
|
||||
final start = -math.pi / 2;
|
||||
final gapAngle = 2.0 / baseRadius;
|
||||
final rect = Rect.fromCircle(center: center, radius: baseRadius);
|
||||
final total = math.pi * 2;
|
||||
final a1 = end + gapAngle;
|
||||
final a2 = start - gapAngle;
|
||||
double sweep1 = (a2 - a1);
|
||||
while (sweep1 <= 0) sweep1 += total;
|
||||
canvas.drawArc(rect, a1, sweep1, false, trackPaint);
|
||||
|
||||
// Active squiggle path
|
||||
final steps = math.max(48, (s.width * 1.2).round());
|
||||
final path = Path();
|
||||
final steps = (200 * (value ?? 1.0)).clamp(40, 300).toInt(); // resolution
|
||||
for (int i = 0; i <= steps; i++) {
|
||||
final t = i / steps;
|
||||
final theta = start + totalAngle * t;
|
||||
final wave = math.sin((t * waves * 2 * math.pi) + phase);
|
||||
final r = baseRadius + amplitude * wave;
|
||||
final p = Offset(
|
||||
center.dx + r * math.cos(theta), center.dy + r * math.sin(theta));
|
||||
if (i == 0) {
|
||||
final ang = start + (end - start) * t;
|
||||
final arcLen = squiggleRadius * (ang - start);
|
||||
final r =
|
||||
squiggleRadius + amp * math.sin(arcLen / scallopLen * 2 * math.pi);
|
||||
final p =
|
||||
Offset(center.dx + r * math.cos(ang), center.dy + r * math.sin(ang));
|
||||
if (i == 0)
|
||||
path.moveTo(p.dx, p.dy);
|
||||
} else {
|
||||
else
|
||||
path.lineTo(p.dx, p.dy);
|
||||
}
|
||||
}
|
||||
canvas.drawPath(path, paint);
|
||||
final activePaint = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = stroke
|
||||
..strokeCap = StrokeCap.round
|
||||
..isAntiAlias = true
|
||||
..color = active;
|
||||
canvas.drawPath(path, activePaint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _WavyArcPainter old) =>
|
||||
old.color != color ||
|
||||
old.stroke != stroke ||
|
||||
old.value != value ||
|
||||
old.waves != waves ||
|
||||
old.amplitude != amplitude ||
|
||||
old.phase != phase ||
|
||||
old.clockwise != clockwise;
|
||||
bool shouldRepaint(covariant _CircularWavyPainter old) =>
|
||||
value != old.value ||
|
||||
active != old.active ||
|
||||
track != old.track ||
|
||||
rotation != old.rotation;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,27 @@
|
|||
enum ProgressM3ESize { small, medium, large }
|
||||
enum ProgressM3EEmphasis { primary, secondary, surface }
|
||||
enum ProgressM3EDensity { regular, compact }
|
||||
enum LinearProgressM3EVariant { determinate, indeterminate, buffer, query }
|
||||
enum ProgressLabelPosition { none, leading, trailing, top, bottom, center }
|
||||
/// Circular sizes driven by outer diameter.
|
||||
enum CircularProgressM3ESize { s, m }
|
||||
|
||||
enum LinearBarShapeM3E { flat, wavy }
|
||||
enum CircularBarShapeM3E { flat, wavy }
|
||||
extension CircularM3ESizeExtension on CircularProgressM3ESize {
|
||||
double get diameterWavy {
|
||||
switch (this) {
|
||||
case CircularProgressM3ESize.s:
|
||||
return 48.0; // wavy small
|
||||
case CircularProgressM3ESize.m:
|
||||
return 52.0; // wavy medium
|
||||
}
|
||||
}
|
||||
|
||||
double get diameterFlat {
|
||||
switch (this) {
|
||||
case CircularProgressM3ESize.s:
|
||||
return 40.0; // flat small
|
||||
case CircularProgressM3ESize.m:
|
||||
return 44.0; // flat medium
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Linear sizes and shapes
|
||||
enum LinearProgressM3ESize { s, m }
|
||||
|
||||
enum ProgressM3EShape { flat, wavy }
|
||||
|
|
|
|||
|
|
@ -1,378 +1,165 @@
|
|||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:m3e_design/m3e_design.dart';
|
||||
|
||||
import '_tokens.dart';
|
||||
import 'enums.dart';
|
||||
import 'tokens_adapter.dart';
|
||||
|
||||
class LinearProgressM3E extends StatefulWidget {
|
||||
const LinearProgressM3E({
|
||||
/// Linear indicator that renders two **separate lanes** (active above, track below)
|
||||
/// with a fixed vertical gap. Lanes never overlap.
|
||||
class LinearProgressIndicatorM3E extends StatelessWidget {
|
||||
const LinearProgressIndicatorM3E({
|
||||
super.key,
|
||||
this.value,
|
||||
this.bufferValue,
|
||||
this.variant = LinearProgressM3EVariant.determinate,
|
||||
this.size = ProgressM3ESize.medium,
|
||||
this.emphasis = ProgressM3EEmphasis.primary,
|
||||
this.density = ProgressM3EDensity.regular,
|
||||
this.backgroundColor,
|
||||
this.progressColor,
|
||||
this.bufferColor,
|
||||
this.semanticLabel,
|
||||
this.minWidth = double.infinity,
|
||||
this.strokeHeight,
|
||||
this.borderRadius,
|
||||
this.shape = LinearBarShapeM3E.wavy,
|
||||
this.wavelength,
|
||||
this.amplitude,
|
||||
this.leftRightInset,
|
||||
this.value, // null => indeterminate; animate phase externally
|
||||
this.size = LinearProgressM3ESize.m,
|
||||
this.shape = ProgressM3EShape.wavy,
|
||||
this.activeColor,
|
||||
this.trackColor,
|
||||
this.phase = 0.0, // radians for wavy animation
|
||||
this.inset = 4.0, // horizontal left inset
|
||||
});
|
||||
|
||||
final double? value;
|
||||
final double? bufferValue;
|
||||
final LinearProgressM3EVariant variant;
|
||||
final ProgressM3ESize size;
|
||||
final ProgressM3EEmphasis emphasis;
|
||||
final ProgressM3EDensity density;
|
||||
final Color? backgroundColor;
|
||||
final Color? progressColor;
|
||||
final Color? bufferColor;
|
||||
final String? semanticLabel;
|
||||
final double minWidth;
|
||||
final double? strokeHeight;
|
||||
final BorderRadius? borderRadius;
|
||||
final LinearBarShapeM3E shape;
|
||||
final double? wavelength;
|
||||
final double? amplitude;
|
||||
final double? leftRightInset;
|
||||
|
||||
@override
|
||||
State<LinearProgressM3E> createState() => _LinearProgressM3EState();
|
||||
}
|
||||
|
||||
class _LinearProgressM3EState extends State<LinearProgressM3E>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _anim = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1200),
|
||||
)..repeat();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_anim.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
final LinearProgressM3ESize size;
|
||||
final ProgressM3EShape shape;
|
||||
final Color? activeColor;
|
||||
final Color? trackColor;
|
||||
final double phase;
|
||||
final double inset;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ProgressTokensAdapter(context);
|
||||
final m = tokens.metrics(widget.density);
|
||||
final theme = Theme.of(context);
|
||||
final m3e =
|
||||
theme.extension<M3ETheme>() ?? M3ETheme.defaults(theme.colorScheme);
|
||||
|
||||
final height = switch (widget.size) {
|
||||
ProgressM3ESize.small => widget.strokeHeight ?? m.linearThicknessSmall,
|
||||
ProgressM3ESize.medium => widget.strokeHeight ?? m.linearThicknessMedium,
|
||||
ProgressM3ESize.large => widget.strokeHeight ?? m.linearThicknessLarge,
|
||||
};
|
||||
// Farben aus m3e_design beziehen (überschreibbar per Props)
|
||||
final active = activeColor ?? m3e.colors.primary;
|
||||
final track = trackColor ?? m3e.colors.surfaceContainerHighest;
|
||||
|
||||
final track = widget.backgroundColor ?? tokens.trackColor();
|
||||
final progress = widget.progressColor ?? tokens.color(widget.emphasis);
|
||||
final buffer = widget.bufferColor ?? tokens.bufferColor(progress);
|
||||
final spec = specForLinear(size: size, shape: shape);
|
||||
|
||||
final borderRadius =
|
||||
widget.borderRadius ?? BorderRadius.circular(height / 2);
|
||||
final inset = widget.leftRightInset ?? m.horizontalInset;
|
||||
// Total height = active lane height (trackHeight or wavyHeight) + gap + trackHeight
|
||||
final activeHeight = spec.isWavy
|
||||
? (spec.trackHeight + 2 * spec.waveAmplitude)
|
||||
: spec.trackHeight;
|
||||
final totalHeight = activeHeight + spec.gap + spec.trackHeight;
|
||||
|
||||
final content = Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: inset),
|
||||
child: _buildBar(
|
||||
context, height, borderRadius, track, progress, buffer, tokens),
|
||||
);
|
||||
|
||||
final bar = ClipRRect(
|
||||
borderRadius: borderRadius,
|
||||
return RepaintBoundary(
|
||||
child: SizedBox(
|
||||
height: height,
|
||||
width: widget.minWidth == double.infinity ? null : widget.minWidth,
|
||||
child: content,
|
||||
),
|
||||
);
|
||||
|
||||
if (widget.semanticLabel == null) return bar;
|
||||
return Semantics(
|
||||
label: widget.semanticLabel,
|
||||
value: (widget.variant == LinearProgressM3EVariant.determinate &&
|
||||
widget.value != null)
|
||||
? '${(widget.value!.clamp(0.0, 1.0) * 100).toStringAsFixed(0)}%'
|
||||
: null,
|
||||
child: bar,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBar(
|
||||
BuildContext context,
|
||||
double height,
|
||||
BorderRadius borderRadius,
|
||||
Color track,
|
||||
Color progress,
|
||||
Color buffer,
|
||||
ProgressTokensAdapter tokens,
|
||||
) {
|
||||
final variant = widget.variant;
|
||||
final shape = widget.shape;
|
||||
|
||||
if (shape == LinearBarShapeM3E.flat) {
|
||||
// Use standard LinearProgressIndicator behaviors.
|
||||
if (variant == LinearProgressM3EVariant.indeterminate ||
|
||||
(variant == LinearProgressM3EVariant.determinate &&
|
||||
widget.value == null)) {
|
||||
return LinearProgressIndicator(
|
||||
color: progress,
|
||||
backgroundColor: track,
|
||||
minHeight: height,
|
||||
);
|
||||
} else if (variant == LinearProgressM3EVariant.query) {
|
||||
return Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.identity()..scale(-1.0, 1.0, 1.0),
|
||||
child: LinearProgressIndicator(
|
||||
color: progress,
|
||||
backgroundColor: track,
|
||||
minHeight: height,
|
||||
),
|
||||
);
|
||||
} else if (variant == LinearProgressM3EVariant.buffer) {
|
||||
return _BufferBar(
|
||||
height: height,
|
||||
track: track,
|
||||
buffer: buffer,
|
||||
progress: progress,
|
||||
value: widget.value ?? 0.0,
|
||||
bufferValue: widget.bufferValue ?? 0.0,
|
||||
);
|
||||
} else {
|
||||
return LinearProgressIndicator(
|
||||
value: (widget.value ?? 0.0).clamp(0.0, 1.0),
|
||||
color: progress,
|
||||
backgroundColor: track,
|
||||
minHeight: height,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final wavelength =
|
||||
widget.wavelength ?? tokens.metrics(widget.density).wavyWavelength;
|
||||
final amplitude = widget.amplitude ??
|
||||
tokens.metrics(widget.density).wavyAmplitudeFactor * height;
|
||||
|
||||
if (variant == LinearProgressM3EVariant.determinate &&
|
||||
widget.value != null) {
|
||||
return _WavyBar(
|
||||
value: widget.value!.clamp(0.0, 1.0),
|
||||
height: height,
|
||||
wavelength: wavelength,
|
||||
amplitude: amplitude.clamp(0.0, height / 2),
|
||||
track: track,
|
||||
fill: progress,
|
||||
);
|
||||
}
|
||||
|
||||
// Indeterminate / query / missing value → animate phase
|
||||
return AnimatedBuilder(
|
||||
animation: _anim,
|
||||
builder: (context, _) {
|
||||
final phase = 2 * math.pi * _anim.value;
|
||||
final reverse = widget.variant == LinearProgressM3EVariant.query;
|
||||
return _WavyIndeterminateBar(
|
||||
height: height,
|
||||
wavelength: wavelength,
|
||||
amplitude: amplitude.clamp(0.0, height / 2),
|
||||
track: track,
|
||||
fill: progress,
|
||||
phase: reverse ? -phase : phase,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BufferBar extends StatelessWidget {
|
||||
const _BufferBar({
|
||||
required this.height,
|
||||
required this.track,
|
||||
required this.buffer,
|
||||
required this.progress,
|
||||
required this.value,
|
||||
required this.bufferValue,
|
||||
});
|
||||
|
||||
final double height;
|
||||
final Color track;
|
||||
final Color buffer;
|
||||
final Color progress;
|
||||
final double value;
|
||||
final double bufferValue;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
final w = constraints.maxWidth;
|
||||
final pv = (w.isFinite ? w : 0) * value.clamp(0.0, 1.0);
|
||||
final bv = (w.isFinite ? w : 0) * bufferValue.clamp(0.0, 1.0);
|
||||
|
||||
Widget seg(double width, Color color) => Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOutCubic,
|
||||
width: width,
|
||||
height: height,
|
||||
color: color,
|
||||
),
|
||||
);
|
||||
|
||||
return Stack(
|
||||
fit: StackFit.passthrough,
|
||||
children: [
|
||||
ColoredBox(color: track),
|
||||
seg(bv, buffer),
|
||||
seg(pv, progress),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _WavyBar extends StatelessWidget {
|
||||
const _WavyBar({
|
||||
required this.value,
|
||||
required this.height,
|
||||
required this.wavelength,
|
||||
required this.amplitude,
|
||||
required this.track,
|
||||
required this.fill,
|
||||
});
|
||||
|
||||
final double value;
|
||||
final double height;
|
||||
final double wavelength;
|
||||
final double amplitude;
|
||||
final Color track;
|
||||
final Color fill;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomPaint(
|
||||
painter: _WavyPainter(
|
||||
height: totalHeight,
|
||||
width: double.infinity,
|
||||
child: CustomPaint(
|
||||
painter: _LinearPainter(
|
||||
value: value,
|
||||
height: height,
|
||||
wavelength: wavelength,
|
||||
amplitude: amplitude,
|
||||
track: track,
|
||||
fill: fill,
|
||||
phase: 0,
|
||||
indeterminate: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WavyIndeterminateBar extends StatelessWidget {
|
||||
const _WavyIndeterminateBar({
|
||||
required this.height,
|
||||
required this.wavelength,
|
||||
required this.amplitude,
|
||||
required this.track,
|
||||
required this.fill,
|
||||
required this.phase,
|
||||
});
|
||||
|
||||
final double height;
|
||||
final double wavelength;
|
||||
final double amplitude;
|
||||
final Color track;
|
||||
final Color fill;
|
||||
final double phase;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomPaint(
|
||||
painter: _WavyPainter(
|
||||
value: 0.6,
|
||||
height: height,
|
||||
wavelength: wavelength,
|
||||
amplitude: amplitude,
|
||||
track: track,
|
||||
fill: fill,
|
||||
spec: spec,
|
||||
active: activeColor ?? active,
|
||||
track: trackColor ?? track,
|
||||
phase: phase,
|
||||
indeterminate: true,
|
||||
inset: inset,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WavyPainter extends CustomPainter {
|
||||
_WavyPainter({
|
||||
class _LinearPainter extends CustomPainter {
|
||||
_LinearPainter({
|
||||
required this.value,
|
||||
required this.height,
|
||||
required this.wavelength,
|
||||
required this.amplitude,
|
||||
required this.spec,
|
||||
required this.active,
|
||||
required this.track,
|
||||
required this.fill,
|
||||
required this.phase,
|
||||
required this.indeterminate,
|
||||
required this.inset,
|
||||
});
|
||||
|
||||
final double value;
|
||||
final double height;
|
||||
final double wavelength;
|
||||
final double amplitude;
|
||||
final double? value;
|
||||
final LinearSpec spec;
|
||||
final Color active;
|
||||
final Color track;
|
||||
final Color fill;
|
||||
final double phase;
|
||||
final bool indeterminate;
|
||||
final double inset;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paintTrack = Paint()..color = track;
|
||||
final paintFill = Paint()..color = fill;
|
||||
final left = inset;
|
||||
final right = size.width - spec.trailingMargin;
|
||||
final width = math.max(0.0, right - left);
|
||||
|
||||
final r = RRect.fromRectAndRadius(
|
||||
Offset.zero & Size(size.width, height), Radius.circular(height / 2));
|
||||
canvas.drawRRect(r, paintTrack);
|
||||
// lane centers: active on top, track on bottom
|
||||
final trackCy = size.height - spec.trackHeight / 2;
|
||||
final activeHeight = spec.isWavy
|
||||
? (spec.trackHeight + 2 * spec.waveAmplitude)
|
||||
: spec.trackHeight;
|
||||
final activeCy =
|
||||
trackCy - (spec.trackHeight / 2 + spec.gap + activeHeight / 2);
|
||||
|
||||
final w = size.width;
|
||||
final progressW = indeterminate ? w : (w * value.clamp(0.0, 1.0));
|
||||
// --- Draw track lane (flat pill) ---
|
||||
final base = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = spec.trackHeight
|
||||
..strokeCap = StrokeCap.round
|
||||
..isAntiAlias = true;
|
||||
|
||||
final centerY = height / 2;
|
||||
final path = Path()..moveTo(0, height);
|
||||
path.lineTo(0, centerY);
|
||||
canvas.drawLine(
|
||||
Offset(left, trackCy), Offset(right, trackCy), base..color = track);
|
||||
|
||||
final k = 2 * math.pi / wavelength;
|
||||
final step = 2.0;
|
||||
double x = 0;
|
||||
while (x <= progressW) {
|
||||
final y = centerY - amplitude * math.sin(k * x + phase);
|
||||
// --- Active lane ---
|
||||
final double p = (value ?? 0).clamp(0.0, 1.0);
|
||||
if (spec.isWavy) {
|
||||
// wavy centerline
|
||||
final start = left;
|
||||
final end = value == null ? right : (left + width * p);
|
||||
final path = Path();
|
||||
const step = 1.5;
|
||||
final k = 2 * math.pi / spec.wavePeriod;
|
||||
|
||||
double x = start;
|
||||
double y =
|
||||
activeCy + spec.waveAmplitude * math.sin(phase + (x - start) * k);
|
||||
path.moveTo(x, y);
|
||||
for (x = start + step; x <= end; x += step) {
|
||||
y = activeCy + spec.waveAmplitude * math.sin(phase + (x - start) * k);
|
||||
path.lineTo(x, y);
|
||||
x += step;
|
||||
}
|
||||
path.lineTo(progressW, height);
|
||||
path.close();
|
||||
// precise end point
|
||||
y = activeCy + spec.waveAmplitude * math.sin(phase + (end - start) * k);
|
||||
path.lineTo(end, y);
|
||||
|
||||
canvas.save();
|
||||
final clip = Path()..addRRect(r);
|
||||
canvas.clipPath(clip);
|
||||
canvas.drawPath(path, paintFill);
|
||||
canvas.restore();
|
||||
canvas.drawPath(
|
||||
path,
|
||||
base
|
||||
..color = active
|
||||
..strokeWidth = spec.trackHeight);
|
||||
|
||||
// end dot (non-overlapping, placed slightly before end)
|
||||
final dotCenterX = math.max(start, end - spec.dotOffset);
|
||||
canvas.drawCircle(
|
||||
Offset(dotCenterX, y), spec.dotDiameter / 2, Paint()..color = active);
|
||||
} else {
|
||||
// flat active pill + end dot
|
||||
final start = left;
|
||||
final end = value == null ? right : (left + width * p);
|
||||
canvas.drawLine(
|
||||
Offset(start, activeCy),
|
||||
Offset(end, activeCy),
|
||||
base
|
||||
..color = active
|
||||
..strokeWidth = spec.trackHeight);
|
||||
final dotCenterX = math.max(start, end - spec.dotOffset);
|
||||
canvas.drawCircle(Offset(dotCenterX, activeCy), spec.dotDiameter / 2,
|
||||
Paint()..color = active);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _WavyPainter old) {
|
||||
return old.value != value ||
|
||||
old.height != height ||
|
||||
old.wavelength != wavelength ||
|
||||
old.amplitude != amplitude ||
|
||||
old.phase != phase ||
|
||||
old.track != track ||
|
||||
old.fill != fill ||
|
||||
old.indeterminate != indeterminate;
|
||||
}
|
||||
bool shouldRepaint(covariant _LinearPainter old) =>
|
||||
value != old.value ||
|
||||
spec != old.spec ||
|
||||
active != old.active ||
|
||||
track != old.track ||
|
||||
phase != old.phase ||
|
||||
inset != old.inset;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,74 +1,35 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'circular_progress_m3e.dart';
|
||||
import 'enums.dart';
|
||||
import 'tokens_adapter.dart';
|
||||
import 'linear_progress_m3e.dart';
|
||||
|
||||
class ProgressWithLabelM3E extends StatelessWidget {
|
||||
const ProgressWithLabelM3E({
|
||||
super.key,
|
||||
required this.progress,
|
||||
this.position = ProgressLabelPosition.trailing,
|
||||
this.label,
|
||||
this.spacing,
|
||||
required this.value,
|
||||
this.size = CircularProgressM3ESize.m,
|
||||
this.textStyle,
|
||||
});
|
||||
|
||||
final LinearProgressM3E progress;
|
||||
final ProgressLabelPosition position;
|
||||
final Widget? label;
|
||||
final double? spacing;
|
||||
final double value;
|
||||
final CircularProgressM3ESize size;
|
||||
final TextStyle? textStyle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (position == ProgressLabelPosition.none) return progress;
|
||||
|
||||
final tokens = ProgressTokensAdapter(context);
|
||||
final style = textStyle ?? tokens.labelStyle().copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
);
|
||||
final gap = spacing ?? 8.0;
|
||||
|
||||
final value = progress.value;
|
||||
final builtLabel = label ?? Text(
|
||||
value != null ? '${(value * 100).toStringAsFixed(0)}%' : '',
|
||||
style: style,
|
||||
);
|
||||
|
||||
switch (position) {
|
||||
case ProgressLabelPosition.leading:
|
||||
case ProgressLabelPosition.trailing:
|
||||
final children = <Widget>[
|
||||
if (position == ProgressLabelPosition.leading) builtLabel,
|
||||
if (position == ProgressLabelPosition.leading) SizedBox(width: gap),
|
||||
Expanded(child: progress),
|
||||
if (position == ProgressLabelPosition.trailing) SizedBox(width: gap),
|
||||
if (position == ProgressLabelPosition.trailing) builtLabel,
|
||||
];
|
||||
return Row(children: children);
|
||||
case ProgressLabelPosition.top:
|
||||
case ProgressLabelPosition.bottom:
|
||||
final children = <Widget>[
|
||||
if (position == ProgressLabelPosition.top) builtLabel,
|
||||
if (position == ProgressLabelPosition.top) SizedBox(height: gap),
|
||||
progress,
|
||||
if (position == ProgressLabelPosition.bottom) SizedBox(height: gap),
|
||||
if (position == ProgressLabelPosition.bottom) builtLabel,
|
||||
];
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: children,
|
||||
);
|
||||
case ProgressLabelPosition.center:
|
||||
return Stack(
|
||||
final d =
|
||||
size.diameterWavy; // ProgressWithLabel uses wavy circular by default
|
||||
return SizedBox(
|
||||
width: d,
|
||||
height: d,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
progress,
|
||||
builtLabel,
|
||||
CircularProgressIndicatorM3E(value: value, size: size),
|
||||
Text('${(value * 100).round()}%',
|
||||
style: textStyle ?? Theme.of(context).textTheme.labelMedium),
|
||||
],
|
||||
),
|
||||
);
|
||||
case ProgressLabelPosition.none:
|
||||
return progress;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,94 +0,0 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:m3e_design/m3e_design.dart';
|
||||
import 'enums.dart';
|
||||
|
||||
@immutable
|
||||
class _ProgressMetrics {
|
||||
final double circularSmall;
|
||||
final double circularMedium;
|
||||
final double circularLarge;
|
||||
final double strokeSmall;
|
||||
final double strokeMedium;
|
||||
final double strokeLarge;
|
||||
final double linearThicknessSmall;
|
||||
final double linearThicknessMedium;
|
||||
final double linearThicknessLarge;
|
||||
final double wavyWavelength; // dp (linear)
|
||||
final double wavyAmplitudeFactor; // fraction of bar height (linear)
|
||||
final double horizontalInset; // 4dp
|
||||
final double circularWaveAmplitudeFactor; // fraction of stroke
|
||||
final int circularWavesPerCircle; // rough default
|
||||
const _ProgressMetrics({
|
||||
required this.circularSmall,
|
||||
required this.circularMedium,
|
||||
required this.circularLarge,
|
||||
required this.strokeSmall,
|
||||
required this.strokeMedium,
|
||||
required this.strokeLarge,
|
||||
required this.linearThicknessSmall,
|
||||
required this.linearThicknessMedium,
|
||||
required this.linearThicknessLarge,
|
||||
required this.wavyWavelength,
|
||||
required this.wavyAmplitudeFactor,
|
||||
required this.horizontalInset,
|
||||
required this.circularWaveAmplitudeFactor,
|
||||
required this.circularWavesPerCircle,
|
||||
});
|
||||
}
|
||||
|
||||
_ProgressMetrics _metricsFor(BuildContext context, ProgressM3EDensity density) {
|
||||
double cS = 24, cM = 32, cL = 48;
|
||||
double sS = 3, sM = 4, sL = 6;
|
||||
double ltS = 3, ltM = 4, ltL = 6;
|
||||
|
||||
if (density == ProgressM3EDensity.compact) {
|
||||
cS -= 2; cM -= 2; cL -= 4;
|
||||
sS -= 0.5; sM -= 0.5; sL -= 1;
|
||||
ltS -= 0.5; ltM -= 0.5; ltL -= 1;
|
||||
}
|
||||
|
||||
return _ProgressMetrics(
|
||||
circularSmall: cS,
|
||||
circularMedium: cM,
|
||||
circularLarge: cL,
|
||||
strokeSmall: sS,
|
||||
strokeMedium: sM,
|
||||
strokeLarge: sL,
|
||||
linearThicknessSmall: ltS,
|
||||
linearThicknessMedium: ltM,
|
||||
linearThicknessLarge: ltL,
|
||||
wavyWavelength: 40, // per spec illustration
|
||||
wavyAmplitudeFactor: 0.33, // amplitude ≈ 1/3 of height
|
||||
horizontalInset: 4, // 4dp inset L/R
|
||||
circularWaveAmplitudeFactor: 0.35, // ~1/3 of stroke
|
||||
circularWavesPerCircle: 10, // a nice default
|
||||
);
|
||||
}
|
||||
|
||||
class ProgressTokensAdapter {
|
||||
ProgressTokensAdapter(this.context);
|
||||
final BuildContext context;
|
||||
|
||||
M3ETheme get _m3e {
|
||||
final t = Theme.of(context);
|
||||
return t.extension<M3ETheme>() ?? M3ETheme.defaults(t.colorScheme);
|
||||
}
|
||||
|
||||
_ProgressMetrics metrics(ProgressM3EDensity density) => _metricsFor(context, density);
|
||||
|
||||
Color color(ProgressM3EEmphasis emphasis) {
|
||||
switch (emphasis) {
|
||||
case ProgressM3EEmphasis.primary:
|
||||
return _m3e.colors.primary;
|
||||
case ProgressM3EEmphasis.secondary:
|
||||
return _m3e.colors.secondary;
|
||||
case ProgressM3EEmphasis.surface:
|
||||
return _m3e.colors.onSurface;
|
||||
}
|
||||
}
|
||||
|
||||
Color trackColor() => _m3e.colors.onSurface.withOpacity(0.12);
|
||||
Color bufferColor(Color progress) => progress.withOpacity(0.24);
|
||||
|
||||
TextStyle labelStyle() => _m3e.type.bodySmall;
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
name: progress_indicator_m3e
|
||||
description: Material 3 Expressive Progress Indicator for Flutter (linear + circular; flat + wavy) using M3E tokens.
|
||||
version: 0.1.0
|
||||
description: "Material 3 Expressive progress indicators."
|
||||
version: 0.3.0
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: ">=3.5.0 <4.0.0"
|
||||
flutter: ">=3.22.0"
|
||||
sdk: ">=3.3.0 <4.0.0"
|
||||
flutter: ">=3.19.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
# melos_managed_dependency_overrides: m3e_design
|
||||
dependency_overrides:
|
||||
m3e_design:
|
||||
path: ..\\m3e_design
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
test('placeholder', () {
|
||||
expect(1 + 2, 3);
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue