redesign app phase 3
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
@@ -68,6 +69,7 @@ class PlayerScreen extends StatelessWidget {
|
||||
children: [
|
||||
if (FinampSettingsHelper.finampSettings.showCoverAsPlayerBackground)
|
||||
const _BlurredPlayerScreenBackground(),
|
||||
const _PlayerAmbientVisualizer(),
|
||||
SafeArea(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
@@ -113,6 +115,158 @@ class PlayerScreen extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _PlayerAmbientVisualizer extends StatefulWidget {
|
||||
const _PlayerAmbientVisualizer({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_PlayerAmbientVisualizer> createState() =>
|
||||
_PlayerAmbientVisualizerState();
|
||||
}
|
||||
|
||||
class _PlayerAmbientVisualizerState extends State<_PlayerAmbientVisualizer>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
StreamSubscription<PlaybackState>? _subscription;
|
||||
bool _isPlaying = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1800),
|
||||
);
|
||||
|
||||
final audioHandler = GetIt.instance<MusicPlayerBackgroundTask>();
|
||||
_subscription = audioHandler.playbackState.listen((playbackState) {
|
||||
if (_isPlaying == playbackState.playing) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isPlaying = playbackState.playing;
|
||||
});
|
||||
|
||||
if (_isPlaying) {
|
||||
_controller.repeat();
|
||||
} else {
|
||||
_controller.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subscription?.cancel();
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return IgnorePointer(
|
||||
child: AnimatedOpacity(
|
||||
opacity: _isPlaying ? 1 : 0.18,
|
||||
duration: const Duration(milliseconds: 450),
|
||||
child: RepaintBoundary(
|
||||
child: AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, _) {
|
||||
return CustomPaint(
|
||||
painter: _AmbientVisualizerPainter(
|
||||
progress: _controller.value,
|
||||
primary: colorScheme.primary,
|
||||
secondary: colorScheme.secondary,
|
||||
tertiary: colorScheme.tertiary,
|
||||
),
|
||||
size: Size.infinite,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AmbientVisualizerPainter extends CustomPainter {
|
||||
const _AmbientVisualizerPainter({
|
||||
required this.progress,
|
||||
required this.primary,
|
||||
required this.secondary,
|
||||
required this.tertiary,
|
||||
});
|
||||
|
||||
final double progress;
|
||||
final Color primary;
|
||||
final Color secondary;
|
||||
final Color tertiary;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final center = Offset(size.width * 0.5, size.height * 0.42);
|
||||
final pulse = 0.5 + sin(progress * pi * 2) * 0.5;
|
||||
|
||||
_paintOrb(
|
||||
canvas,
|
||||
center.translate(size.width * -0.22, size.height * -0.08),
|
||||
size.shortestSide * (0.34 + pulse * 0.05),
|
||||
primary.withOpacity(0.16),
|
||||
);
|
||||
_paintOrb(
|
||||
canvas,
|
||||
center.translate(size.width * 0.23, size.height * 0.02),
|
||||
size.shortestSide * (0.30 + (1 - pulse) * 0.07),
|
||||
tertiary.withOpacity(0.14),
|
||||
);
|
||||
|
||||
final barPaint = Paint()..style = PaintingStyle.fill;
|
||||
final barCount = max(16, (size.width / 28).floor());
|
||||
final maxBarHeight = size.height * 0.14;
|
||||
final baseline = size.height * 0.86;
|
||||
final totalWidth = min(size.width * 0.72, 620.0);
|
||||
final gap = totalWidth / barCount;
|
||||
final startX = (size.width - totalWidth) / 2;
|
||||
|
||||
for (var i = 0; i < barCount; i++) {
|
||||
final wave = sin(progress * pi * 2 + i * 0.62);
|
||||
final alternateWave = sin(progress * pi * 4 + i * 0.31);
|
||||
final normalized = (wave + alternateWave * 0.35 + 1.35) / 2.7;
|
||||
final barHeight = 12 + normalized * maxBarHeight;
|
||||
final x = startX + i * gap + gap * 0.22;
|
||||
final width = gap * 0.48;
|
||||
final rect = RRect.fromRectAndRadius(
|
||||
Rect.fromLTWH(x, baseline - barHeight, width, barHeight),
|
||||
Radius.circular(width),
|
||||
);
|
||||
|
||||
barPaint.color = Color.lerp(primary, secondary, i / barCount)!
|
||||
.withOpacity(0.16 + normalized * 0.26);
|
||||
canvas.drawRRect(rect, barPaint);
|
||||
}
|
||||
}
|
||||
|
||||
void _paintOrb(Canvas canvas, Offset center, double radius, Color color) {
|
||||
final paint = Paint()
|
||||
..shader = RadialGradient(
|
||||
colors: [color, Colors.transparent],
|
||||
).createShader(Rect.fromCircle(center: center, radius: radius));
|
||||
|
||||
canvas.drawCircle(center, radius, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _AmbientVisualizerPainter oldDelegate) {
|
||||
return progress != oldDelegate.progress ||
|
||||
primary != oldDelegate.primary ||
|
||||
secondary != oldDelegate.secondary ||
|
||||
tertiary != oldDelegate.tertiary;
|
||||
}
|
||||
}
|
||||
|
||||
class _PlayerArtworkSurface extends StatelessWidget {
|
||||
const _PlayerArtworkSurface({Key? key}) : super(key: key);
|
||||
|
||||
@@ -250,9 +404,9 @@ class _PlayerScreenAlbumImage extends ConsumerWidget {
|
||||
// We need a post frame callback because otherwise this
|
||||
// widget rebuilds on the same frame
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => ref.read(_albumImageProvider.notifier).state =
|
||||
imageProvider,
|
||||
),
|
||||
(_) => ref.read(_albumImageProvider.notifier).state =
|
||||
imageProvider,
|
||||
),
|
||||
// Here we awkwardly get the next 3 queue items so that we
|
||||
// can precache them (so that the image is already loaded
|
||||
// when the next song comes on).
|
||||
|
||||
Reference in New Issue
Block a user