fix(ui): unify scroll and improve playback switching
Build & Deploy Pluriwave / Análisis de código (push) Successful in 12s
Build & Deploy Pluriwave / Build APK + AAB release (push) Successful in 1m17s

This commit is contained in:
2026-05-20 23:44:14 +02:00
parent 34022e0814
commit 10520fef48
5 changed files with 138 additions and 191 deletions
+78 -88
View File
@@ -45,31 +45,20 @@ class VisualizadorAudio extends StatefulWidget {
}
class _VisualizadorAudioState extends State<VisualizadorAudio>
with TickerProviderStateMixin {
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late List<_BarraState> _barras;
final _random = Random();
bool _activo = false;
StreamSubscription<EstadoReproduccion>? _estadoSubscription;
@override
void initState() {
super.initState();
_barras = List.generate(
widget.barras,
(i) => _BarraState(
fase: _random.nextDouble() * pi * 2,
velocidad: 0.8 + _random.nextDouble() * 1.4,
amplitud: 0.4 + _random.nextDouble() * 0.6,
offset: _random.nextDouble() * 0.3,
),
);
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
)..addListener(_actualizar);
duration: const Duration(seconds: 2),
)..addListener(() {
if (mounted) setState(() {});
});
_estadoSubscription = widget.estadoStream.listen(_onEstado);
}
@@ -79,17 +68,7 @@ class _VisualizadorAudioState extends State<VisualizadorAudio>
if (nuevoActivo == _activo) return;
if (!mounted) return;
setState(() => _activo = nuevoActivo);
if (nuevoActivo) {
_controller.repeat();
} else {
_controller.forward(from: _controller.value).whenComplete(() {
if (!_activo && mounted) _controller.stop();
});
}
}
void _actualizar() {
if (mounted) setState(() {});
nuevoActivo ? _controller.repeat() : _controller.stop();
}
@override
@@ -103,75 +82,86 @@ class _VisualizadorAudioState extends State<VisualizadorAudio>
Widget build(BuildContext context) {
final color = widget.color ?? Theme.of(context).colorScheme.primary;
final t = _controller.value * pi * 2;
return SizedBox(
height: widget.altura,
child: LayoutBuilder(
builder: (context, constraints) {
final totalAncho = constraints.maxWidth == double.infinity
? 300.0
: constraints.maxWidth;
final espaciado = totalAncho / widget.barras;
final anchoBar = (espaciado * 0.55).clamp(2.0, 8.0);
return RepaintBoundary(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: List.generate(widget.barras, (i) {
final b = _barras[i];
final double altura;
if (_activo) {
// Movimiento orgánico: combinación de senos con diferentes fases
final onda1 = sin(t * b.velocidad + b.fase);
final onda2 = sin(t * b.velocidad * 0.7 + b.fase * 1.3) * 0.5;
final valor = ((onda1 + onda2 + 1.5) / 3.0).clamp(0.0, 1.0);
altura = (b.offset + valor * b.amplitud) * widget.altura;
} else {
// Decaer suavemente a altura mínima
final progreso = _controller.value;
final alturaActual = b.alturaActual;
b.alturaActual = alturaActual * (1 - progreso * 0.1);
altura = b.alturaActual.clamp(2.0, widget.altura * 0.05);
}
return Padding(
padding: EdgeInsets.symmetric(horizontal: (espaciado - anchoBar) / 2),
child: AnimatedContainer(
duration: const Duration(milliseconds: 80),
width: anchoBar,
height: altura.clamp(2.0, widget.altura),
decoration: BoxDecoration(
color: color.withValues(
alpha: _activo ? 0.7 + (altura / widget.altura) * 0.3 : 0.3,
),
borderRadius: BorderRadius.circular(anchoBar / 2),
),
),
);
}),
),
);
},
child: RepaintBoundary(
child: CustomPaint(
painter: _WaveFlowPainter(
color: color,
phase: t,
active: _activo,
),
child: const SizedBox.expand(),
),
),
);
}
}
class _BarraState {
final double fase;
final double velocidad;
final double amplitud;
final double offset;
double alturaActual;
class _WaveFlowPainter extends CustomPainter {
const _WaveFlowPainter({
required this.color,
required this.phase,
required this.active,
});
_BarraState({
required this.fase,
required this.velocidad,
required this.amplitud,
required this.offset,
}) : alturaActual = offset * 20;
final Color color;
final double phase;
final bool active;
@override
void paint(Canvas canvas, Size size) {
final center = size.height / 2;
final amp = active ? size.height * 0.24 : size.height * 0.06;
final glowPaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = active ? 8 : 4
..strokeCap = StrokeCap.round
..color = color.withValues(alpha: active ? 0.18 : 0.08);
final linePaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 3
..strokeCap = StrokeCap.round
..shader = LinearGradient(
colors: [
color.withValues(alpha: 0.08),
color.withValues(alpha: active ? 0.95 : 0.35),
const Color(0xFFF4B860).withValues(alpha: active ? 0.75 : 0.20),
color.withValues(alpha: 0.08),
],
).createShader(Offset.zero & size);
Path pathFor(double shift, double amplitude) {
final path = Path()..moveTo(0, center);
for (double x = 0; x <= size.width; x += 8) {
final p = x / size.width;
final y = center +
sin((p * pi * 2.4) + phase + shift) * amplitude +
sin((p * pi * 5.2) - phase * 0.7 + shift) * amplitude * 0.32;
path.lineTo(x, y);
}
return path;
}
canvas.drawPath(pathFor(0, amp), glowPaint);
canvas.drawPath(pathFor(0.6, amp * 0.62), glowPaint..color = color.withValues(alpha: active ? 0.10 : 0.05));
canvas.drawPath(pathFor(0, amp), linePaint);
canvas.drawPath(
pathFor(0.9, amp * 0.58),
Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2
..strokeCap = StrokeCap.round
..color = Colors.white.withValues(alpha: active ? 0.35 : 0.12),
);
}
@override
bool shouldRepaint(covariant _WaveFlowPainter oldDelegate) {
return oldDelegate.phase != phase ||
oldDelegate.active != active ||
oldDelegate.color != color;
}
}
/// Versión compacta del visualizador — 5 barras, para uso en MiniReproductor