opt: m3e loading (#1877)

* opt: loading

* feat: refresh m3e

* restore refreshIndicator

---------

Co-authored-by: dom <githubaccount56556@proton.me>
This commit is contained in:
My-Responsitories
2026-03-29 23:34:04 +08:00
committed by GitHub
parent f0050dd6e6
commit 886c53c7d8
6 changed files with 173 additions and 70 deletions

View File

@@ -10,23 +10,21 @@ class CustomToast extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context); final colorScheme = ColorScheme.of(context);
return Container( return Container(
margin: EdgeInsets.only( margin: EdgeInsets.only(
bottom: MediaQuery.viewPaddingOf(context).bottom + 30, bottom: MediaQuery.viewPaddingOf(context).bottom + 30,
), ),
padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 10),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer.withValues( color: colorScheme.primaryContainer.withValues(alpha: toastOpacity),
alpha: toastOpacity,
),
borderRadius: const BorderRadius.all(Radius.circular(20)), borderRadius: const BorderRadius.all(Radius.circular(20)),
), ),
child: Text( child: Text(
msg, msg,
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: theme.colorScheme.onPrimaryContainer, color: colorScheme.onPrimaryContainer,
), ),
), ),
); );
@@ -41,7 +39,7 @@ class LoadingWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context); final theme = Theme.of(context);
final onSurfaceVariant = theme.colorScheme.onSurfaceVariant; final onSurfaceVariant = theme.colorScheme.onSurfaceVariant;
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20), padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20),
@@ -58,7 +56,6 @@ class LoadingWidget extends StatelessWidget {
strokeWidth: 3, strokeWidth: 3,
valueColor: AlwaysStoppedAnimation(onSurfaceVariant), valueColor: AlwaysStoppedAnimation(onSurfaceVariant),
), ),
//msg //msg
Text(msg, style: TextStyle(color: onSurfaceVariant)), Text(msg, style: TextStyle(color: onSurfaceVariant)),
], ],

View File

@@ -217,8 +217,8 @@ class RefreshIndicatorState extends State<RefreshIndicator>
RefreshIndicatorStatus? _status; RefreshIndicatorStatus? _status;
late Future<void> _pendingRefreshFuture; late Future<void> _pendingRefreshFuture;
double? _dragOffset; double? _dragOffset;
late Color _effectiveValueColor = late Color _effectiveValueColor;
widget.color ?? Theme.of(context).colorScheme.primary; // late Color _backgroundColor;
static final Animatable<double> _threeQuarterTween = Tween<double>( static final Animatable<double> _threeQuarterTween = Tween<double>(
begin: 0.0, begin: 0.0,
@@ -274,9 +274,10 @@ class RefreshIndicatorState extends State<RefreshIndicator>
} }
void _setupColorTween() { void _setupColorTween() {
final colorScheme = ColorScheme.of(context);
// _backgroundColor = colorScheme.surfaceContainerHighest;
// Reset the current value color. // Reset the current value color.
_effectiveValueColor = _effectiveValueColor = widget.color ?? colorScheme.primary;
widget.color ?? Theme.of(context).colorScheme.primary;
final Color color = _effectiveValueColor; final Color color = _effectiveValueColor;
if (color.a == 0) { if (color.a == 0) {
// Set an always stopped animation instead of a driven tween. // Set an always stopped animation instead of a driven tween.
@@ -558,14 +559,52 @@ class RefreshIndicatorState extends State<RefreshIndicator>
} }
bool _onDrag(double offset, double viewportDimension) { bool _onDrag(double offset, double viewportDimension) {
if (_positionController.value > 0.0 && if (_positionController.value > 0.0 && _status == .drag) {
_status == RefreshIndicatorStatus.drag) {
_dragOffset = _dragOffset! + offset; _dragOffset = _dragOffset! + offset;
_checkDragOffset(viewportDimension); _checkDragOffset(viewportDimension);
return true; return true;
} }
return false; return false;
} }
// late final _refreshKey = GlobalKey();
// Widget _m3eRefreshProgressIndicator(bool showIndeterminateIndicator) {
// const indicatorMargin = EdgeInsets.all(4);
// const indicatorPadding = EdgeInsets.all(6);
// const indicatorSize = 41.0;
// final progress = _value.value;
// return Padding(
// padding: indicatorMargin,
// child: SizedBox(
// width: indicatorSize,
// height: indicatorSize,
// child: Material(
// type: MaterialType.circle,
// color: _backgroundColor,
// elevation: widget.elevation,
// child: Padding(
// padding: indicatorPadding,
// child: showIndeterminateIndicator
// ? M3ELoadingIndicator(
// childKey: _refreshKey,
// color: _effectiveValueColor,
// morphs: Morphs.refreshMorphs,
// size: null,
// )
// : RawM3ELoadingIndicator(
// key: _refreshKey,
// morph: Morphs.manualMorph,
// progress: progress,
// angle: -progress * math.pi,
// color: _valueColor.value!,
// size: null,
// ),
// ),
// ),
// ),
// );
// }
} }
// ignore: camel_case_types // ignore: camel_case_types

View File

@@ -18,6 +18,7 @@
import 'dart:math' show pi; import 'dart:math' show pi;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart' show SemanticsConfiguration;
/// ///
/// created by dom on 2026/02/14 /// created by dom on 2026/02/14
@@ -73,6 +74,7 @@ class RenderLoadingIndicator extends RenderBox {
if (_progress == value) return; if (_progress == value) return;
_progress = value; _progress = value;
markNeedsPaint(); markNeedsPaint();
markNeedsSemanticsUpdate();
} }
@override @override
@@ -119,6 +121,16 @@ class RenderLoadingIndicator extends RenderBox {
); );
} }
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config
..role = .progressBar
..minValue = '0'
..maxValue = '100'
..value = (_progress * 100).round().toString();
}
@override @override
bool get isRepaintBoundary => true; bool get isRepaintBoundary => true;
} }

View File

@@ -4,8 +4,6 @@ import 'package:flutter/material.dart';
const Widget m3eLoading = Center(child: M3ELoadingIndicator()); const Widget m3eLoading = Center(child: M3ELoadingIndicator());
const Widget circularLoading = Center(child: CircularProgressIndicator());
const Widget linearLoading = SliverToBoxAdapter( const Widget linearLoading = SliverToBoxAdapter(
child: LinearProgressIndicator(), child: LinearProgressIndicator(),
); );

View File

@@ -17,14 +17,27 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:PiliPlus/common/widgets/loading_widget/morphs.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/physics.dart' show SpringSimulation; import 'package:flutter/physics.dart' show SpringSimulation;
import 'package:flutter/semantics.dart';
import 'package:material_new_shapes/material_new_shapes.dart'; import 'package:material_new_shapes/material_new_shapes.dart';
/// reimplement of https://github.com/EmilyMoonstone/material_3_expressive/tree/main/packages/loading_indicator_m3e /// reimplement of https://github.com/EmilyMoonstone/material_3_expressive/tree/main/packages/loading_indicator_m3e
class M3ELoadingIndicator extends StatefulWidget { class M3ELoadingIndicator extends StatefulWidget {
const M3ELoadingIndicator({super.key}); const M3ELoadingIndicator({
super.key,
// this.childKey,
this.morphs,
this.color,
this.size = const Size.square(40),
});
final List<Morph>? morphs;
final Color? color;
final Size size;
// final Key? childKey;
@override @override
State<M3ELoadingIndicator> createState() => _M3ELoadingIndicatorState(); State<M3ELoadingIndicator> createState() => _M3ELoadingIndicatorState();
@@ -32,42 +45,25 @@ class M3ELoadingIndicator extends StatefulWidget {
class _M3ELoadingIndicatorState extends State<M3ELoadingIndicator> class _M3ELoadingIndicatorState extends State<M3ELoadingIndicator>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
static final List<Morph> _morphs = () {
final List<RoundedPolygon> shapes = [
MaterialShapes.softBurst,
MaterialShapes.cookie9Sided,
MaterialShapes.pentagon,
MaterialShapes.pill,
MaterialShapes.sunny,
MaterialShapes.cookie4Sided,
MaterialShapes.oval,
];
return [
for (var i = 0; i < shapes.length; i++)
Morph(
shapes[i],
shapes[(i + 1) % shapes.length],
),
];
}();
static const int _morphIntervalMs = 650; static const int _morphIntervalMs = 650;
static const double _fullRotation = 360.0; static const double _fullRotation = 2 * math.pi;
static const int _globalRotationDurationMs = 4666; static const int _globalRotationDurationMs = 4666;
static const double _quarterRotation = _fullRotation / 4; static const double _quarterRotation = _fullRotation / 4;
late final List<Morph> _morphs;
late final AnimationController _controller; late final AnimationController _controller;
int _morphIndex = 1; int _morphIndex = 1;
double _morphRotationTargetAngle = _quarterRotation; double _morphRotationTarget = _quarterRotation;
final _morphAnimationSpec = SpringSimulation( static final _morphAnimationSpec = SpringSimulation(
SpringDescription.withDampingRatio(ratio: 0.6, stiffness: 200.0, mass: 1.0), SpringDescription.withDampingRatio(ratio: 0.6, stiffness: 200.0, mass: 1.0),
0.0, 0.0,
1.0, 1.0,
5.0, 5.0,
snapToEnd: true, snapToEnd: true,
// tolerance: const Tolerance(velocity: 0.1, distance: 0.1),
); );
void _statusListener(AnimationStatus status) { void _statusListener(AnimationStatus status) {
@@ -78,23 +74,21 @@ class _M3ELoadingIndicatorState extends State<M3ELoadingIndicator>
void _startAnimation() { void _startAnimation() {
_morphIndex++; _morphIndex++;
_morphRotationTargetAngle = _morphRotationTarget =
(_morphRotationTargetAngle + _quarterRotation) % _fullRotation; (_morphRotationTarget + _quarterRotation) % _fullRotation;
_controller _controller.animateWith(_morphAnimationSpec);
..value = 0.0
..animateWith(_morphAnimationSpec);
} }
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_morphs = widget.morphs ?? Morphs.loadingMorphs;
_controller = _controller =
AnimationController( AnimationController(
vsync: this, vsync: this,
duration: const Duration(milliseconds: _morphIntervalMs), duration: const Duration(milliseconds: _morphIntervalMs),
) )
..addStatusListener(_statusListener) ..addStatusListener(_statusListener)
..value = 0.0
..animateWith(_morphAnimationSpec); ..animateWith(_morphAnimationSpec);
} }
@@ -110,82 +104,86 @@ class _M3ELoadingIndicatorState extends State<M3ELoadingIndicator>
final elapsedInMs = final elapsedInMs =
_morphIntervalMs * (_morphIndex - 1) + _morphIntervalMs * (_morphIndex - 1) +
(_controller.lastElapsedDuration?.inMilliseconds ?? 0); (_controller.lastElapsedDuration?.inMilliseconds ?? 0);
final globalRotationControllerValue = final globalRotation =
(elapsedInMs % _globalRotationDurationMs) / _globalRotationDurationMs; (elapsedInMs % _globalRotationDurationMs) /
final globalRotationDegrees = globalRotationControllerValue * _fullRotation; _globalRotationDurationMs *
final totalRotationDegrees = _fullRotation;
progress * _quarterRotation +
_morphRotationTargetAngle + return progress * _quarterRotation + _morphRotationTarget + globalRotation;
globalRotationDegrees;
return totalRotationDegrees * (math.pi / 180.0);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final color = Theme.of(context).colorScheme.secondaryFixedDim; final color = widget.color ?? ColorScheme.of(context).secondaryFixedDim;
return AnimatedBuilder( return AnimatedBuilder(
animation: _controller, animation: _controller,
builder: (context, child) { builder: (context, child) {
final progress = _controller.value; final progress = _controller.value;
return _M3ELoadingIndicator( return RawM3ELoadingIndicator(
// key: widget.childKey,
morph: _morphs[_morphIndex % _morphs.length], morph: _morphs[_morphIndex % _morphs.length],
progress: progress, progress: progress,
angle: _calcAngle(progress), angle: _calcAngle(progress),
color: color, color: color,
size: widget.size,
); );
}, },
); );
} }
} }
class _M3ELoadingIndicator extends LeafRenderObjectWidget { class RawM3ELoadingIndicator extends LeafRenderObjectWidget {
const _M3ELoadingIndicator({ const RawM3ELoadingIndicator({
super.key,
required this.morph, required this.morph,
required this.progress, required this.progress,
required this.angle, required this.angle,
required this.color, required this.color,
required this.size,
}); });
final Morph morph; final Morph morph;
final double progress; final double progress;
final double angle; final double angle;
final Color color; final Color color;
final Size size;
@override @override
RenderObject createRenderObject(BuildContext context) { RenderObject createRenderObject(BuildContext context) {
return _RenderM3ELoadingIndicator( return RenderM3ELoadingIndicator(
morph: morph, morph: morph,
progress: progress, progress: progress,
angle: angle, angle: angle,
color: color, color: color,
size: size,
); );
} }
@override @override
void updateRenderObject( void updateRenderObject(
BuildContext context, BuildContext context,
_RenderM3ELoadingIndicator renderObject, RenderM3ELoadingIndicator renderObject,
) { ) {
renderObject renderObject
..morph = morph ..morph = morph
..progress = progress ..progress = progress
..angle = angle ..angle = angle
..color = color; ..color = color
..preferredSize = size;
} }
} }
class _RenderM3ELoadingIndicator extends RenderBox { class RenderM3ELoadingIndicator extends RenderBox {
_RenderM3ELoadingIndicator({ RenderM3ELoadingIndicator({
required Morph morph, required Morph morph,
required double progress, required double progress,
required double angle, required double angle,
required Color color, required Color color,
required Size size,
}) : _morph = morph, }) : _morph = morph,
_progress = progress, _progress = progress,
_angle = angle, _angle = angle,
_preferredSize = size,
_color = color, _color = color,
_paint = Paint() _paint = Paint()
..style = PaintingStyle.fill ..style = PaintingStyle.fill
@@ -223,17 +221,35 @@ class _RenderM3ELoadingIndicator extends RenderBox {
markNeedsPaint(); markNeedsPaint();
} }
Size _preferredSize;
set preferredSize(Size value) {
if (_preferredSize == value) return;
_preferredSize = size;
markNeedsLayout();
}
@override
Size computeDryLayout(covariant BoxConstraints constraints) {
return constraints.constrain(_preferredSize);
}
@override @override
void performLayout() { void performLayout() {
size = constraints.constrainDimensions(40, 40); size = computeDryLayout(constraints);
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
config.role = .loadingSpinner;
} }
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
final width = size.width; final width = size.width;
final value = size.width / 2; final value = size.width / 2;
final matrix = Matrix4.identity() final matrix =
..translateByDouble(offset.dx + value, offset.dy + value, 0.0, 1.0) Matrix4.translationValues(offset.dx + value, offset.dy + value, 0.0)
..rotateZ(angle) ..rotateZ(angle)
..translateByDouble(-value, -value, 0.0, 1.0) ..translateByDouble(-value, -value, 0.0, 1.0)
..scaleByDouble(width, width, width, 1.0); ..scaleByDouble(width, width, width, 1.0);

View File

@@ -0,0 +1,41 @@
import 'package:material_new_shapes/material_new_shapes.dart';
abstract final class Morphs {
static List<Morph> buildMorph(
List<RoundedPolygon> shapes, {
bool loop = true,
}) {
assert(shapes.length >= 2);
return [
for (var i = 0; i < shapes.length - 1; i++)
Morph(shapes[i], shapes[i + 1]),
if (loop) Morph(shapes[shapes.length - 1], shapes[0]),
];
}
static final loadingMorphs = buildMorph([
MaterialShapes.softBurst,
MaterialShapes.cookie9Sided,
MaterialShapes.pentagon,
MaterialShapes.pill,
MaterialShapes.sunny,
MaterialShapes.cookie4Sided,
MaterialShapes.oval,
]);
// static final refreshMorphs = buildMorph([
// MaterialShapes.softBurst,
// MaterialShapes.cookie9Sided,
// MaterialShapes.gem,
// MaterialShapes.flower,
// MaterialShapes.sunny,
// MaterialShapes.cookie4Sided,
// MaterialShapes.oval,
// MaterialShapes.cookie12Sided,
// ]);
// static final manualMorph = Morph(
// MaterialShapes.circle,
// MaterialShapes.softBurst,
// );
}