diff --git a/example/lib/main.dart b/example/lib/main.dart index 5431f79..7a16d43 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,18 +1,24 @@ import 'package:flutter/material.dart'; -import 'package:miniplayer/miniplayer.dart'; +import 'screens/audio_screen.dart'; +import 'widgets/player.dart'; +import 'utils.dart'; + +ValueNotifier currentlyPlaying = ValueNotifier(null); + +const double playerMinHeight = 70; +const double playerMaxHeight = 370; +const miniplayerPercentageDeclaration = 0.2; void main() => runApp(MyApp()); -final _navigatorKey = GlobalKey(); - class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - title: 'Miniplayer example', - debugShowCheckedModeBanner: false, + title: 'Miniplayer Demo', theme: ThemeData( - primaryColor: Color(0xFFFAFAFA), + primaryColor: Colors.grey[50], + visualDensity: VisualDensity.adaptivePlatformDensity, ), home: MyHomePage(), ); @@ -27,111 +33,59 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { @override Widget build(BuildContext context) { - return MiniplayerWillPopScope( - onWillPop: () async { - final NavigatorState navigator = _navigatorKey.currentState; - if (!navigator.canPop()) return true; - navigator.pop(); - - return false; - }, - child: Scaffold( - body: Stack( - children: [ - Navigator( - key: _navigatorKey, - onGenerateRoute: (RouteSettings settings) { - return MaterialPageRoute( - settings: settings, - builder: (BuildContext context) => FirstScreen(), - ); - }, - ), - Miniplayer( - minHeight: 70, - maxHeight: 370, - builder: (height, percentage) { - return Center( - child: Text('$height, $percentage'), - ); - }, - ), - ], - ), - bottomNavigationBar: BottomNavigationBar( + return Scaffold( + body: Stack( + children: [ + Column( + children: [ + AppBar(title: Text('Miniplayer Demo')), + Expanded( + child: AudioUi( + onTap: (audioObject) => currentlyPlaying.value = audioObject, + ), + ), + ], + ), + ValueListenableBuilder( + valueListenable: currentlyPlaying, + builder: + (BuildContext context, AudioObject audioObject, Widget child) => + audioObject != null + ? DetailedPlayer(audioObject: audioObject) + : Container(), + ), + ], + ), + bottomNavigationBar: ValueListenableBuilder( + valueListenable: playerExpandProgress, + child: BottomNavigationBar( currentIndex: 0, - fixedColor: Colors.green, - items: [ + selectedItemColor: Colors.blue, + items: [ + BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Feed'), BottomNavigationBarItem( - icon: Icon(Icons.home), - title: Text('Home'), - ), - BottomNavigationBarItem( - icon: Icon(Icons.mail), - title: Text('Messages'), - ), - BottomNavigationBarItem( - icon: Icon(Icons.person), title: Text('Profile')) + icon: Icon(Icons.library_books), label: 'Library'), ], ), + builder: (BuildContext context, double height, Widget child) { + final value = percentageFromValueInRange( + min: playerMinHeight, max: playerMaxHeight, value: height); + + if (value == null) return child; + var opacity = 1 - value; + if (opacity < 0) opacity = 0; + if (opacity > 1) opacity = 1; + + return SizedBox( + height: + kBottomNavigationBarHeight - kBottomNavigationBarHeight * value, + child: Transform.translate( + offset: Offset(0.0, kBottomNavigationBarHeight * value * 0.5), + child: Opacity(opacity: opacity, child: child), + ), + ); + }, ), ); } } - -class FirstScreen extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text('Demo: FirstScreen')), - body: Container( - constraints: BoxConstraints.expand(), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - RaisedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => SecondScreen()), - ); - }, - child: const Text('Open SecondScreen', - style: TextStyle(fontSize: 20)), - ), - RaisedButton( - onPressed: () { - Navigator.of(context, rootNavigator: true).push( - MaterialPageRoute(builder: (context) => ThirdScreen()), - ); - }, - child: const Text('Open ThirdScreen with root Navigator', - style: TextStyle(fontSize: 20)), - ), - ], - ), - ), - ); - } -} - -class SecondScreen extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text('Demo: SecondScreen')), - body: Center(child: Text('SecondScreen')), - ); - } -} - -class ThirdScreen extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text('Demo: ThirdScreen')), - body: Center(child: Text('ThirdScreen')), - ); - } -} \ No newline at end of file diff --git a/example/lib/screens/audio_screen.dart b/example/lib/screens/audio_screen.dart new file mode 100644 index 0000000..787ae58 --- /dev/null +++ b/example/lib/screens/audio_screen.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import '../widgets/audio_list_tile.dart'; + +import '../utils.dart'; + +typedef OnTap(final AudioObject audioObject); + +const Set audioExamples = { + AudioObject('Salt & Pepper', 'Dope Lemon', + 'https://m.media-amazon.com/images/I/81UYWMG47EL._SS500_.jpg'), + AudioObject('Losing It', 'FISHER', + 'https://m.media-amazon.com/images/I/9135KRo8Q7L._SS500_.jpg'), + AudioObject('American Kids', 'Kenny Chesney', + 'https://cdn.playbuzz.com/cdn/7ce5041b-f9e8-4058-8886-134d05e33bd7/5c553d94-4aa2-485c-8a3f-9f496e4e4619.jpg'), + AudioObject('Wake Me Up', 'Avicii', + 'https://upload.wikimedia.org/wikipedia/en/d/da/Avicii_Wake_Me_Up_Official_Single_Cover.png'), + AudioObject('Missing You', 'Mesto', + 'https://img.discogs.com/EcqkrmOCbBguE3ns-HrzNmZP4eM=/fit-in/600x600/filters:strip_icc():format(jpeg):mode_rgb():quality(90)/discogs-images/R-12539198-1537229070-5497.jpeg.jpg'), + AudioObject('Drop it dirty', 'Tavengo', + 'https://images.shazam.com/coverart/t416659652-b1392404277_s400.jpg'), + AudioObject('Cigarettes', 'Tash Sultana', + 'https://m.media-amazon.com/images/I/91vBpel766L._SS500_.jpg'), + AudioObject('Ego Death', 'Ty Dolla \$ign, Kanye West, FKA Twigs, Skrillex', + 'https://static.stereogum.com/uploads/2020/06/Ego-Death-1593566496.jpg'), +}; + +class AudioUi extends StatelessWidget { + final OnTap onTap; + + const AudioUi({Key key, @required this.onTap}) : super(key: key); + + @override + Widget build(BuildContext context) { + return ListView( + padding: const EdgeInsets.all(0), + children: [ + Padding( + padding: const EdgeInsets.only(left: 10, bottom: 6, top: 15), + child: Text('Your Library:'), + ), + for (AudioObject a in audioExamples) + AudioListTile(audioObject: a, onTap: () => onTap(a)) + ], + ); + } +} diff --git a/example/lib/utils.dart b/example/lib/utils.dart new file mode 100644 index 0000000..bcc1219 --- /dev/null +++ b/example/lib/utils.dart @@ -0,0 +1,16 @@ +import 'package:flutter/foundation.dart'; + +class AudioObject { + final String title, subtitle, img; + + const AudioObject(this.title, this.subtitle, this.img); +} + +double valueFromPercentageInRange( + {@required final double min, max, percentage}) { + return percentage * (max - min) + min; +} + +double percentageFromValueInRange({@required final double min, max, value}) { + return (value - min) / (max - min); +} diff --git a/example/lib/widgets/audio_list_tile.dart b/example/lib/widgets/audio_list_tile.dart new file mode 100644 index 0000000..649db37 --- /dev/null +++ b/example/lib/widgets/audio_list_tile.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; + +import '../utils.dart'; + +typedef OnTap(AudioObject audioObject); + +class AudioListTile extends StatelessWidget { + final AudioObject audioObject; + final Function onTap; + + const AudioListTile( + {Key key, @required this.audioObject, @required this.onTap}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: ClipRRect( + borderRadius: BorderRadius.circular(4), + child: Image.network( + audioObject.img, + width: 52, + height: 52, + fit: BoxFit.cover, + ), + ), + title: Text(audioObject.title), + subtitle: Text( + audioObject.subtitle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + trailing: IconButton( + icon: Icon(Icons.play_arrow_outlined), + onPressed: onTap, + ), + ); + } +} diff --git a/example/lib/widgets/player.dart b/example/lib/widgets/player.dart new file mode 100644 index 0000000..6bf7951 --- /dev/null +++ b/example/lib/widgets/player.dart @@ -0,0 +1,197 @@ +import 'package:flutter/material.dart'; +import 'package:miniplayer/miniplayer.dart'; +import 'package:example/main.dart'; + +import '../utils.dart'; + +final ValueNotifier playerExpandProgress = + ValueNotifier(playerMinHeight); + +final MiniplayerController controller = MiniplayerController(); + +class DetailedPlayer extends StatelessWidget { + final AudioObject audioObject; + + const DetailedPlayer({Key key, @required this.audioObject}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Miniplayer( + valueNotifier: playerExpandProgress, + minHeight: playerMinHeight, + maxHeight: playerMaxHeight, + controller: controller, + elevation: 4, + onDismissed: () => currentlyPlaying.value = null, + curve: Curves.easeOut, + builder: (height, percentage) { + final bool miniplayer = percentage < miniplayerPercentageDeclaration; + final double width = MediaQuery.of(context).size.width; + final maxImgSize = width * 0.4; + + final img = Image.network(audioObject.img); + final text = Text(audioObject.title); + const buttonPlay = IconButton( + icon: Icon(Icons.pause), + onPressed: onTap, + ); + final progressIndicator = LinearProgressIndicator(value: 0.3); + + //Declare additional widgets (eg. SkipButton) and variables + if (!miniplayer) { + var percentageExpandedPlayer = percentageFromValueInRange( + min: playerMaxHeight * miniplayerPercentageDeclaration + + playerMinHeight, + max: playerMaxHeight, + value: height); + if (percentageExpandedPlayer < 0) percentageExpandedPlayer = 0; + final paddingVertical = valueFromPercentageInRange( + min: 0, max: 10, percentage: percentageExpandedPlayer); + final double heightWithoutPadding = height - paddingVertical * 2; + final double imageSize = heightWithoutPadding > maxImgSize + ? maxImgSize + : heightWithoutPadding; + final paddingLeft = valueFromPercentageInRange( + min: 0, + max: width - imageSize, + percentage: percentageExpandedPlayer, + ) / + 2; + + const buttonSkipForward = IconButton( + icon: Icon(Icons.forward_30), + iconSize: 33, + onPressed: onTap, + ); + const buttonSkipBackwards = IconButton( + icon: Icon(Icons.replay_10), + iconSize: 33, + onPressed: onTap, + ); + const buttonPlayExpanded = IconButton( + icon: Icon(Icons.pause_circle_filled), + iconSize: 50, + onPressed: onTap, + ); + + return Column( + children: [ + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: EdgeInsets.only( + left: paddingLeft, + top: paddingVertical, + bottom: paddingVertical), + child: SizedBox( + height: imageSize, + child: img, + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 33), + child: Opacity( + opacity: percentageExpandedPlayer, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + text, + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + buttonSkipBackwards, + buttonPlayExpanded, + buttonSkipForward + ], + ), + progressIndicator, + Container(), + Container(), + ], + ), + ), + ), + ), + ], + ); + } + + //Miniplayer + final percentageMiniplayer = percentageFromValueInRange( + min: playerMinHeight, + max: playerMaxHeight * miniplayerPercentageDeclaration + + playerMinHeight, + value: height); + + final elementOpacity = 1 - 1 * percentageMiniplayer; + final progressIndicatorHeight = 4 - 4 * percentageMiniplayer; + + return Column( + children: [ + Expanded( + child: Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(maxHeight: maxImgSize), + child: img, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 10), + child: Opacity( + opacity: elementOpacity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Text(audioObject.title, + style: Theme.of(context) + .textTheme + .bodyText2 + .copyWith(fontSize: 16)), + Text( + audioObject.subtitle, + style: Theme.of(context) + .textTheme + .bodyText2 + .copyWith( + color: Colors.black.withOpacity(0.55)), + ), + ], + ), + ), + ), + ), + IconButton( + icon: Icon(Icons.fullscreen), + onPressed: () { + controller.animateToHeight(state: PanelState.MAX); + }), + Padding( + padding: const EdgeInsets.only(right: 3), + child: Opacity( + opacity: elementOpacity, + child: buttonPlay, + ), + ), + ], + ), + ), + SizedBox( + height: progressIndicatorHeight, + child: Opacity( + opacity: elementOpacity, + child: progressIndicator, + ), + ), + ], + ); + }, + ); + } +} + +void onTap() {}