forked from mirrors/material_3_expressive
Add initial configuration, tokens, and widgets for M3E components
- Introduced `.gitignore` and `.metadata` for apps and examples. - Added Flutter/Dart analysis configurations (`analysis_options.yaml`). - Implemented foundational tokens and themes for M3E (colors, shapes). - Created base implementations for `IconButtonM3E` and `SplitButtonM3E`. - Set up CI workflow (`ci.yaml`) to automate testing and analysis.
This commit is contained in:
parent
2c0f2df0b8
commit
62ecb86b76
184 changed files with 9872 additions and 0 deletions
21
packages/progress_indicator_m3e/LICENSE
Normal file
21
packages/progress_indicator_m3e/LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
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.
|
||||
65
packages/progress_indicator_m3e/README.md
Normal file
65
packages/progress_indicator_m3e/README.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# progress_indicators_m3e
|
||||
|
||||
Material 3 **Expressive** progress indicators for Flutter:
|
||||
|
||||
- `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
|
||||
|
||||
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
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
library progress_indicators_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';
|
||||
|
|
@ -0,0 +1,286 @@
|
|||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'enums.dart';
|
||||
import 'tokens_adapter.dart';
|
||||
|
||||
class CircularProgressM3E extends StatefulWidget {
|
||||
const CircularProgressM3E({
|
||||
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,
|
||||
});
|
||||
|
||||
/// 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();
|
||||
}
|
||||
|
||||
@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(
|
||||
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,
|
||||
),
|
||||
),
|
||||
] 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;
|
||||
|
||||
@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;
|
||||
|
||||
@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;
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _ArcPainter old) =>
|
||||
old.color != color ||
|
||||
old.stroke != stroke ||
|
||||
old.value != value ||
|
||||
old.clockwise != clockwise;
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
final Color color;
|
||||
final double stroke;
|
||||
final double? value;
|
||||
final int waves;
|
||||
final double amplitude;
|
||||
final double phase;
|
||||
final bool clockwise;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final rect = Offset.zero & size;
|
||||
final center = rect.center;
|
||||
final baseRadius = (size.shortestSide - stroke) / 2;
|
||||
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = stroke
|
||||
..strokeCap = StrokeCap.round;
|
||||
|
||||
final totalAngle = (value ?? 1.0) * 2 * math.pi * (clockwise ? 1 : -1);
|
||||
final start = -math.pi / 2;
|
||||
|
||||
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) {
|
||||
path.moveTo(p.dx, p.dy);
|
||||
} else {
|
||||
path.lineTo(p.dx, p.dy);
|
||||
}
|
||||
}
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
8
packages/progress_indicator_m3e/lib/src/enums.dart
Normal file
8
packages/progress_indicator_m3e/lib/src/enums.dart
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
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 }
|
||||
|
||||
enum LinearBarShapeM3E { flat, wavy }
|
||||
enum CircularBarShapeM3E { flat, wavy }
|
||||
378
packages/progress_indicator_m3e/lib/src/linear_progress_m3e.dart
Normal file
378
packages/progress_indicator_m3e/lib/src/linear_progress_m3e.dart
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'enums.dart';
|
||||
import 'tokens_adapter.dart';
|
||||
|
||||
class LinearProgressM3E extends StatefulWidget {
|
||||
const LinearProgressM3E({
|
||||
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,
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ProgressTokensAdapter(context);
|
||||
final m = tokens.metrics(widget.density);
|
||||
|
||||
final height = switch (widget.size) {
|
||||
ProgressM3ESize.small => widget.strokeHeight ?? m.linearThicknessSmall,
|
||||
ProgressM3ESize.medium => widget.strokeHeight ?? m.linearThicknessMedium,
|
||||
ProgressM3ESize.large => widget.strokeHeight ?? m.linearThicknessLarge,
|
||||
};
|
||||
|
||||
final track = widget.backgroundColor ?? tokens.trackColor();
|
||||
final progress = widget.progressColor ?? tokens.color(widget.emphasis);
|
||||
final buffer = widget.bufferColor ?? tokens.bufferColor(progress);
|
||||
|
||||
final borderRadius =
|
||||
widget.borderRadius ?? BorderRadius.circular(height / 2);
|
||||
final inset = widget.leftRightInset ?? m.horizontalInset;
|
||||
|
||||
final content = Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: inset),
|
||||
child: _buildBar(
|
||||
context, height, borderRadius, track, progress, buffer, tokens),
|
||||
);
|
||||
|
||||
final bar = ClipRRect(
|
||||
borderRadius: borderRadius,
|
||||
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(
|
||||
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,
|
||||
phase: phase,
|
||||
indeterminate: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WavyPainter extends CustomPainter {
|
||||
_WavyPainter({
|
||||
required this.value,
|
||||
required this.height,
|
||||
required this.wavelength,
|
||||
required this.amplitude,
|
||||
required this.track,
|
||||
required this.fill,
|
||||
required this.phase,
|
||||
required this.indeterminate,
|
||||
});
|
||||
|
||||
final double value;
|
||||
final double height;
|
||||
final double wavelength;
|
||||
final double amplitude;
|
||||
final Color track;
|
||||
final Color fill;
|
||||
final double phase;
|
||||
final bool indeterminate;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paintTrack = Paint()..color = track;
|
||||
final paintFill = Paint()..color = fill;
|
||||
|
||||
final r = RRect.fromRectAndRadius(
|
||||
Offset.zero & Size(size.width, height), Radius.circular(height / 2));
|
||||
canvas.drawRRect(r, paintTrack);
|
||||
|
||||
final w = size.width;
|
||||
final progressW = indeterminate ? w : (w * value.clamp(0.0, 1.0));
|
||||
|
||||
final centerY = height / 2;
|
||||
final path = Path()..moveTo(0, height);
|
||||
path.lineTo(0, centerY);
|
||||
|
||||
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);
|
||||
path.lineTo(x, y);
|
||||
x += step;
|
||||
}
|
||||
path.lineTo(progressW, height);
|
||||
path.close();
|
||||
|
||||
canvas.save();
|
||||
final clip = Path()..addRRect(r);
|
||||
canvas.clipPath(clip);
|
||||
canvas.drawPath(path, paintFill);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
import 'package:flutter/material.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,
|
||||
this.textStyle,
|
||||
});
|
||||
|
||||
final LinearProgressM3E progress;
|
||||
final ProgressLabelPosition position;
|
||||
final Widget? label;
|
||||
final double? spacing;
|
||||
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(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
progress,
|
||||
builtLabel,
|
||||
],
|
||||
);
|
||||
case ProgressLabelPosition.none:
|
||||
return progress;
|
||||
}
|
||||
}
|
||||
}
|
||||
94
packages/progress_indicator_m3e/lib/src/tokens_adapter.dart
Normal file
94
packages/progress_indicator_m3e/lib/src/tokens_adapter.dart
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.idea" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.pub" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/example/.dart_tool" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/example/.idea" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/example/.pub" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/example/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/example/android/.gradle" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/example/android/.idea" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/example/ios/Flutter" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/example/ios/Pods" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/example/ios/.symlinks" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/example/macos/Flutter" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/example/macos/Pods" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/example/macos/.symlinks" />
|
||||
</content>
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="Dart SDK" level="project" />
|
||||
<orderEntry type="library" name="Flutter Plugins" level="project" />
|
||||
<orderEntry type="library" name="Dart Packages" level="project" />
|
||||
</component>
|
||||
</module>
|
||||
18
packages/progress_indicator_m3e/pubspec.yaml
Normal file
18
packages/progress_indicator_m3e/pubspec.yaml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
name: progress_indicator_m3e
|
||||
description: Material 3 Expressive Progress Indicator for Flutter (linear + circular; flat + wavy) using M3E tokens.
|
||||
version: 0.1.0
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: ">=3.5.0 <4.0.0"
|
||||
flutter: ">=3.22.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
m3e_design:
|
||||
path: ../m3e_design
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
4
packages/progress_indicator_m3e/pubspec_overrides.yaml
Normal file
4
packages/progress_indicator_m3e/pubspec_overrides.yaml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# melos_managed_dependency_overrides: m3e_design
|
||||
dependency_overrides:
|
||||
m3e_design:
|
||||
path: ..\\m3e_design
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
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