From 648a8ff7e8db1c6eb5b68775a8e9d3e21ed0259f Mon Sep 17 00:00:00 2001 From: pato05 Date: Mon, 5 Apr 2021 00:59:57 +0200 Subject: [PATCH] lyrics: add go-to on click feature player_bar: animated play/pause icon --- lib/ui/lyrics.dart | 65 ++++++----- lib/ui/player_bar.dart | 238 +++++++++++++++++++++++++---------------- 2 files changed, 178 insertions(+), 125 deletions(-) diff --git a/lib/ui/lyrics.dart b/lib/ui/lyrics.dart index b0927db..5cab83d 100644 --- a/lib/ui/lyrics.dart +++ b/lib/ui/lyrics.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:audio_service/audio_service.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/definitions.dart'; @@ -37,7 +36,7 @@ class _LyricsScreenState extends State { Future _load() async { //Already available if (this.lyrics != null) return; - if (widget.lyrics != null && widget.lyrics.lyrics != null && widget.lyrics.lyrics.length > 0) { + if (widget.lyrics?.lyrics != null && widget.lyrics.lyrics.length > 0) { setState(() { lyrics = widget.lyrics; _loading = false; @@ -70,18 +69,19 @@ class _LyricsScreenState extends State { Timer.periodic(Duration(milliseconds: 350), (timer) { _timer = timer; + _currentIndex = lyrics?.lyrics?.lastIndexWhere((l) => l.offset <= AudioService.playbackState.currentPosition); if (_loading) return; - - //Update current lyric index - setState(() => _currentIndex = lyrics.lyrics.lastIndexWhere((l) => l.offset <= AudioService.playbackState.currentPosition)); - //Scroll to current lyric if (_currentIndex <= 0) return; if (_prevIndex == _currentIndex) return; + //Update current lyric index + setState(() => null); _prevIndex = _currentIndex; + //Lyric height, screen height, appbar height + double _scrollTo = (height * _currentIndex) - (MediaQuery.of(context).size.height / 2) + (height / 2) + 56; + if (0 > _scrollTo) return; _controller.animateTo( - //Lyric height, screen height, appbar height - (height * _currentIndex) - (MediaQuery.of(context).size.height / 2) + (height / 2) + 56, + _scrollTo, duration: Duration(milliseconds: 250), curve: Curves.ease ); @@ -141,16 +141,11 @@ class _LyricsScreenState extends State { //Lyrics Padding( padding: EdgeInsets.fromLTRB(0, 0, 0, settings.lyricsVisualizer ? 100 : 0), - child: ListView( - controller: _controller, - children: [ - //Shouldn't really happen, empty lyrics have own text - if (_error) - ErrorScreen(), - - //Loading - if (_loading) - Padding( + child: _error ? + //Shouldn't really happen, empty lyrics have own text + ErrorScreen() : + // Loading + _loading ? Padding( padding: EdgeInsets.all(8.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -158,32 +153,34 @@ class _LyricsScreenState extends State { CircularProgressIndicator() ], ), - ), - - if (lyrics != null) - ...List.generate(lyrics.lyrics.length, (i) { + ) : ListView.builder( + controller: _controller, + itemCount: lyrics.lyrics.length, + itemBuilder: (BuildContext context, int i) { return Padding( padding: EdgeInsets.symmetric(horizontal: 8.0), child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(8.0), - color: (_currentIndex == i) ? Colors.grey.withOpacity(0.25) : Colors.transparent, + color: _currentIndex == i ? Colors.grey.withOpacity(0.25) : Colors.transparent, ), height: height, - child: Center( - child: Text( - lyrics.lyrics[i].text, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 26.0, - fontWeight: (_currentIndex == i) ? FontWeight.bold : FontWeight.normal + child: InkWell( + borderRadius: BorderRadius.circular(8.0), + onTap: lyrics.id != null ? () => AudioService.seekTo(lyrics.lyrics[i].offset) : null, + child: Center( + child: Text( + lyrics.lyrics[i].text, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 26.0, + fontWeight: (_currentIndex == i) ? FontWeight.bold : FontWeight.normal + ), ), - ), - ) + )) ) ); - }), - ], + }, ), ) ], diff --git a/lib/ui/player_bar.dart b/lib/ui/player_bar.dart index 26dbe72..8ad3be3 100644 --- a/lib/ui/player_bar.dart +++ b/lib/ui/player_bar.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:audio_service/audio_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -11,8 +13,10 @@ class PlayerBar extends StatelessWidget { double get progress { if (AudioService.playbackState == null) return 0.0; if (AudioService.currentMediaItem == null) return 0.0; - if (AudioService.currentMediaItem.duration.inSeconds == 0) return 0.0; //Division by 0 - return AudioService.playbackState.currentPosition.inSeconds / AudioService.currentMediaItem.duration.inSeconds; + if (AudioService.currentMediaItem.duration.inSeconds == 0) + return 0.0; //Division by 0 + return AudioService.playbackState.currentPosition.inSeconds / + AudioService.currentMediaItem.duration.inSeconds; } double iconSize = 28; @@ -32,72 +36,80 @@ class PlayerBar extends StatelessWidget { } //Left if (details.delta.dx < -sensitivity) { - await AudioService.skipToNext(); } _gestureRegistered = false; return; }, child: StreamBuilder( - stream: Stream.periodic(Duration(milliseconds: 250)), - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (AudioService.currentMediaItem == null) - return Container(width: 0, height: 0,); - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - // For Android TV: indicate focus by grey - color: focusNode.hasFocus ? Colors.black26 : Theme.of(context).bottomAppBarColor, - child: ListTile( - dense: true, - focusNode: focusNode, - contentPadding: EdgeInsets.symmetric(horizontal: 8.0), - onTap: () { - Navigator.of(context).push(MaterialPageRoute( - builder: (BuildContext context) => PlayerScreen())); - SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( - systemNavigationBarColor: settings.themeData - .scaffoldBackgroundColor, - )); - }, - leading: CachedImage( - width: 50, - height: 50, - url: AudioService.currentMediaItem.extras['thumb'] ?? - AudioService.currentMediaItem.artUri, - ), - title: Text( - AudioService.currentMediaItem.displayTitle, - overflow: TextOverflow.clip, - maxLines: 1, - ), - subtitle: Text( - AudioService.currentMediaItem.displaySubtitle ?? '', - overflow: TextOverflow.clip, - maxLines: 1, - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - PrevNextButton(iconSize, prev: true, hidePrev: true,), - PlayPauseButton(iconSize), - PrevNextButton(iconSize) - ], - ) + stream: Stream.periodic(Duration(milliseconds: 250)), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (AudioService.currentMediaItem == null) + return Container( + width: 0, + height: 0, + ); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + // For Android TV: indicate focus by grey + color: focusNode.hasFocus + ? Colors.black26 + : Theme.of(context).bottomAppBarColor, + child: ListTile( + dense: true, + focusNode: focusNode, + contentPadding: EdgeInsets.symmetric(horizontal: 8.0), + onTap: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (BuildContext context) => PlayerScreen())); + SystemChrome.setSystemUIOverlayStyle( + SystemUiOverlayStyle( + systemNavigationBarColor: + settings.themeData.scaffoldBackgroundColor, + )); + }, + leading: CachedImage( + width: 50, + height: 50, + url: AudioService.currentMediaItem.extras['thumb'] ?? + AudioService.currentMediaItem.artUri, + ), + title: Text( + AudioService.currentMediaItem.displayTitle, + overflow: TextOverflow.clip, + maxLines: 1, + ), + subtitle: Text( + AudioService.currentMediaItem.displaySubtitle ?? '', + overflow: TextOverflow.clip, + maxLines: 1, + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + PrevNextButton( + iconSize, + prev: true, + hidePrev: true, + ), + PlayPauseButton(iconSize), + PrevNextButton(iconSize) + ], + )), ), - ), - Container( - height: 3.0, - child: LinearProgressIndicator( - backgroundColor: Theme.of(context).primaryColor.withOpacity(0.1), - value: progress, - ), - ) - ], - ); - } - ), + Container( + height: 3.0, + child: LinearProgressIndicator( + backgroundColor: + Theme.of(context).primaryColor.withOpacity(0.1), + value: progress, + ), + ) + ], + ); + }), ); } } @@ -115,7 +127,8 @@ class PrevNextButton extends StatelessWidget { stream: AudioService.queueStream, builder: (context, _snapshot) { if (!prev) { - if (playerHelper.queueIndex == (AudioService.queue??[]).length - 1) { + if (playerHelper.queueIndex == + (AudioService.queue ?? []).length - 1) { return IconButton( icon: Icon(Icons.skip_next), iconSize: size, @@ -131,7 +144,10 @@ class PrevNextButton extends StatelessWidget { if (prev) { if (i == 0) { if (hidePrev) { - return Container(height: 0, width: 0,); + return Container( + height: 0, + width: 0, + ); } return IconButton( icon: Icon(Icons.skip_previous), @@ -151,51 +167,91 @@ class PrevNextButton extends StatelessWidget { } } - - -class PlayPauseButton extends StatelessWidget { - +class PlayPauseButton extends StatefulWidget { final double size; PlayPauseButton(this.size); @override - Widget build(BuildContext context) { + _PlayPauseButtonState createState() => _PlayPauseButtonState(); +} +class _PlayPauseButtonState extends State + with SingleTickerProviderStateMixin { + AnimationController _controller; + Animation _animation; + + @override + void initState() { + _controller = + AnimationController(vsync: this, duration: Duration(milliseconds: 250)); + _animation = Tween(begin: 0, end: 1) + .animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); + super.initState(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { return StreamBuilder( stream: AudioService.playbackStateStream, builder: (context, snapshot) { - //Playing - if (AudioService.playbackState?.playing??false) { + ////Playing + //if (AudioService.playbackState?.playing??false) { + // return IconButton( + // iconSize: widget.size, + // icon: Icon(Icons.pause), + // onPressed: () => AudioService.pause() + // ); + //} +// + ////Paused + //if ((!(AudioService.playbackState?.playing??false) && + // AudioService.playbackState.processingState == AudioProcessingState.ready) || + // //None state (stopped) + // AudioService.playbackState.processingState == AudioProcessingState.none) { + // return IconButton( + // iconSize: widget.size, + // icon: Icon(Icons.play_arrow), + // onPressed: () => AudioService.play() + // ); + //} + bool _playing = AudioService.playbackState?.playing ?? false; + if (_playing || + AudioService.playbackState?.processingState == + AudioProcessingState.ready || + AudioService.playbackState?.processingState == + AudioProcessingState.none) { + if (_playing) + _controller.forward(); + else + _controller.reverse(); return IconButton( - iconSize: this.size, - icon: Icon(Icons.pause), - onPressed: () => AudioService.pause() - ); - } - - //Paused - if ((!(AudioService.playbackState?.playing??false) && - AudioService.playbackState.processingState == AudioProcessingState.ready) || - //None state (stopped) - AudioService.playbackState.processingState == AudioProcessingState.none) { - return IconButton( - iconSize: this.size, - icon: Icon(Icons.play_arrow), - onPressed: () => AudioService.play() - ); + icon: AnimatedIcon( + icon: AnimatedIcons.play_pause, + progress: _animation, + ), + iconSize: widget.size, + onPressed: _playing + ? () => AudioService.pause() + : () => AudioService.play()); } switch (AudioService.playbackState.processingState) { - //Stopped/Error + //Stopped/Error case AudioProcessingState.error: case AudioProcessingState.none: case AudioProcessingState.stopped: - return Container(width: this.size, height: this.size); - //Loading, connecting, rewinding... + return Container(width: widget.size, height: widget.size); + //Loading, connecting, rewinding... default: return Container( - width: this.size, - height: this.size, + width: widget.size, + height: widget.size, child: CircularProgressIndicator(), ); }