تطبيق لتشغيل الملفات الصوتيه المحليه في Flutter
في هذا المقال سوف نقوم معكم بعمل تطبيق بسيط لتشغيل المقاطع الصوتيه الموجوده في داخل التطبيق الخاص بك بكل سهوله وهذا كله سوف يكون من خلال بعض المكتبات التي سوف نستعملها مع عمل انميشن لحركة الصورة في المقطع الصوتي حتى تضيف لمسة جماليه في تطبيقك وتحسن من مظهره بشكل اكبر وكل هذا سوف نتعرف عليه في مقالة اليوم بإذن الله بدون اي مشاكل ولهذا عليك باتباع كل تفاصيل المقاله حتى لا يحدث معك اي مشاكل اثناء تنفيذ الخطوات في البرنامج الخاص بك .
هناك العديد من الجوانب الحاسمة لتطوير تطبيقات الويب التي يجب التفكير فيها. الأداء المتسق هو أحد هذه العناصر. هذا يستدعي تقديم المعلومات بأسرع ما يمكن على موقع الويب أو التطبيق الخاص بك. سيترك المستخدمون موقعك وينتقلون إلى موقع آخر إذا استغرق تحميل الصفحة وقتًا طويلاً. الاتساق هو أحد أهم العناصر في العمل. يحتاج العملاء إلى أن يكونوا واثقين من أنه يمكنهم الاعتماد عليك لتقديم نفس مستوى الخدمة أو الجودة في كل مرة يتعاملون معك فيها. قد يكون بناء قاعدة مستهلكين متينة أمرًا صعبًا إذا كان أداؤك غير متسق.
Add packages
- assets_audio_player: ^3.0.5
- sleek_circular_slider: ^2.0.1
- palette_generator: ^0.3.3+2
ملئ model بالبيانات الخاصه بالمقطع الصوتي و جعل الصورة شفافه مع اختيار اكثر قيم موجوده في الصورة
هذا الجزء سوف نقوم به بعمل model للمقاطع الصوتيه ونضع لها الاسم والعنوان والمسار الخاص بالمقطع وايضا الصورة الخاصه بالمقطع الصوتي واسفلها سوف نقوم بعمل methode مسؤوله عن تنفيذ تحويل المده الزمنيه التي يستخدمها البرنامج الى صيغة يفهمها المستخدم مثل 1:52 و 2:40 وهكذا والدالة التي اسفلها تحصل على الوان الصورة لكي نضع صورة الغلاف كالوان وتكون النسبة الاكبر للالوان الاكثر كما تشاهدون في تطبيق انستقرام عندما يتم تحميل اي صورة تظهر تفاصيل الالوان اولا ثم الصورة .
utils.dart
// -------------- utils.dart
import 'package:assets_audio_player/assets_audio_player.dart';
import 'package:flutter/material.dart';
import 'package:palette_generator/palette_generator.dart';
const kPrimaryColor = Color(0xFFebbe8b);
// playlist songs
List<Audio> songs = [
Audio('assets/nf_Let_You_Down.mp3',
metas: Metas(
title: 'Let You Down',
artist: 'NF',
image: const MetasImage.asset(
'assets/1b7f41e39f3d6ac58798a500eb4a0e2901f4502dv2_hq.jpeg'))),
Audio('assets/lil_nas_x_industry_baby.mp3',
metas: Metas(
title: 'Industry Baby',
artist: 'Lil Nas X',
image: const MetasImage.asset('assets/81Uj3NtUuhL._SS500_.jpg'))),
Audio('assets/Beautiful.mp3',
metas: Metas(
title: 'Beautiful',
artist: 'Eminem',
image: const MetasImage.asset('assets/916WuJt833L._SS500_.jpg'))),
];
String durationFormat(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
return '$twoDigitMinutes:$twoDigitSeconds';
// for example => 03:09
}
// get song cover image colors
Future<PaletteGenerator> getImageColors(AssetsAudioPlayer player) async {
var paletteGenerator = await PaletteGenerator.fromImageProvider(
AssetImage(player.getCurrentAudioImage?.path ?? ''),
);
return paletteGenerator;
}
إنشاء صفحة لعرض المقطع الصوتي مع الصورة الخاصه به
هذه الصفحة تعبر عن تشغيل المقطع الصوتي وتوضيح المده الزمنيه المقطوعه والمده الزمنيه الباقيه وايضا يوفر انميشن بسيط بداخلها مع جعل الغلاف يكون عباره عن اكثر الالوان الموجوده في الصور وايضا الامر لا يكفي بذلك بل يكون شكل مختلف لطريقة توضيح المدة الزمنيه للمقطع مع امكانية ايقاف واستكمال المقطع الصوتي كما هو موضح .
PlayerPage.dart
import 'package:assets_audio_player/assets_audio_player.dart';
import 'package:flutter/material.dart';
import 'package:music_player/utils.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:sleek_circular_slider/sleek_circular_slider.dart';
class PlayerPage extends StatefulWidget {
const PlayerPage({required this.player, Key? key}) : super(key: key);
final AssetsAudioPlayer player;
@override
State<PlayerPage> createState() => _PlayerPageState();
}
class _PlayerPageState extends State<PlayerPage> {
Duration duration = Duration.zero;
Duration position = Duration.zero;
bool isPlaying = true;
@override
void initState() {
widget.player.isPlaying.listen((event) {
if (mounted) {
setState(() {
isPlaying = event;
});
}
});
widget.player.onReadyToPlay.listen((newDuration) {
if (mounted) {
setState(() {
duration = newDuration?.duration ?? Duration.zero;
});
}
});
widget.player.currentPosition.listen((newPosition) {
if (mounted) {
setState(() {
position = newPosition;
});
}
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.transparent,
leading: Padding(
padding: const EdgeInsets.only(left: 10),
child: IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(
Icons.keyboard_arrow_down,
size: 30,
color: Colors.white,
)),
),
),
extendBodyBehindAppBar: true,
body: Stack(
alignment: Alignment.center,
children: [
FutureBuilder<PaletteGenerator>(
future: getImageColors(widget.player),
builder: (context, snapshot) {
return Container(
color: snapshot.data?.mutedColor?.color,
);
},
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withOpacity(.7)
])),
),
),
Positioned(
height: MediaQuery.of(context).size.height / 1.5,
child: Column(
children: [
Text(
widget.player.getCurrentAudioTitle,
style: const TextStyle(
fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(
height: 5,
),
Text(
widget.player.getCurrentAudioArtist,
style: const TextStyle(fontSize: 20, color: Colors.white70),
),
SizedBox(
height: MediaQuery.of(context).size.height / 20,
),
IntrinsicHeight(
child: Row(
children: [
Text(
durationFormat(position),
style: const TextStyle(color: Colors.white70),
),
const VerticalDivider(
color: Colors.white54,
thickness: 2,
width: 25,
indent: 2,
endIndent: 2,
),
Text(
durationFormat(duration - position),
style: const TextStyle(color: kPrimaryColor),
)
],
),
),
],
),
),
Center(
child: SleekCircularSlider(
min: 0,
max: duration.inSeconds.toDouble(),
initialValue: position.inSeconds.toDouble(),
onChange: (value) async {
await widget.player.seek(Duration(seconds: value.toInt()));
},
innerWidget: (percentage) {
return Padding(
padding: const EdgeInsets.all(25.0),
child: CircleAvatar(
backgroundColor: Colors.grey,
backgroundImage: AssetImage(
widget.player.getCurrentAudioImage?.path ?? ''),
),
);
},
appearance: CircularSliderAppearance(
size: 330,
angleRange: 300,
startAngle: 300,
customColors: CustomSliderColors(
progressBarColor: kPrimaryColor,
dotColor: kPrimaryColor,
trackColor: Colors.grey.withOpacity(.4)),
customWidths: CustomSliderWidths(
trackWidth: 6, handlerSize: 10, progressBarWidth: 6)),
)),
Positioned(
top: MediaQuery.of(context).size.height / 1.3,
left: 0,
right: 0,
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
onPressed: () async {
await widget.player.previous();
},
icon: const Icon(
Icons.skip_previous_rounded,
size: 50,
color: Colors.white,
)),
IconButton(
onPressed: () async {
await widget.player.playOrPause();
},
padding: EdgeInsets.zero,
icon: isPlaying
? const Icon(
Icons.pause_circle,
size: 70,
color: Colors.white,
)
: const Icon(
Icons.play_circle,
size: 70,
color: Colors.white,
),
),
IconButton(
onPressed: () async {
await widget.player.next();
},
icon: const Icon(
Icons.skip_next_rounded,
size: 50,
color: Colors.white,
)),
],
),
),
),
],
),
);
}
}
إنشاء صفحة لعرض List بجميع المقاطع الصوتيه
في هذه الصفحة والتي تعد اخر صفحة لدينا في هذه المقاله وهيا تعبر عن قائمة بها جميع المقاطع الصوتيه وايضا اذا قمت بتشغيل مقطع وجعل بالاسفل سوف تلاحظ وجود انميشن على صورة الغلاف الخاصه بالعنصر مع تحريك لها بشكل بسيط كما هو موضح بالفيديو .
main.dart
import 'dart:math' as math;
import 'package:assets_audio_player/assets_audio_player.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:music_player/player_page.dart';
import 'package:music_player/utils.dart';
import 'package:palette_generator/palette_generator.dart';
class PlaylistPage extends StatefulWidget {
const PlaylistPage({Key? key}) : super(key: key);
@override
State<PlaylistPage> createState() => _PlaylistPageState();
}
class _PlaylistPageState extends State<PlaylistPage>
with SingleTickerProviderStateMixin {
final player = AssetsAudioPlayer();
bool isPlaying = true;
// define an animation controller for rotate the song cover image
late final AnimationController _animationController =
AnimationController(vsync: this, duration: const Duration(seconds: 3));
@override
void initState() {
openPlayer();
player.isPlaying.listen((event) {
if (mounted) {
setState(() {
isPlaying = event;
});
}
});
super.initState();
}
// define a playlist for player
void openPlayer() async {
await player.open(Playlist(audios: songs),
autoStart: false, showNotification: true, loopMode: LoopMode.playlist);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey.withOpacity(.2),
appBar: AppBar(
title: const Text(
'Music Player',
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
centerTitle: false,
backgroundColor: Colors.transparent,
),
body: Stack(
alignment: Alignment.bottomCenter,
children: [
SafeArea(
child: ListView.separated(
separatorBuilder: (context, index) {
return const Divider(
color: Colors.white30,
height: 0,
thickness: 1,
indent: 85,
);
},
itemCount: songs.length,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.only(top: 10),
child: ListTile(
title: Text(
songs[index].metas.title!,
style: const TextStyle(color: Colors.white),
),
subtitle: Text(
songs[index].metas.artist!,
style: const TextStyle(color: Colors.white70),
),
leading: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.asset(songs[index].metas.image!.path)),
onTap: () async {
await player.playlistPlayAtIndex(index);
setState(() {
player.getCurrentAudioImage;
player.getCurrentAudioTitle;
});
},
),
);
},
)),
player.getCurrentAudioImage == null
? const SizedBox.shrink()
: FutureBuilder<PaletteGenerator>(
future: getImageColors(player),
builder: (context, snapshot) {
return Container(
margin: const EdgeInsets.symmetric(
horizontal: 15, vertical: 50),
height: 75,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: const Alignment(0, 5),
colors: [
snapshot.data?.lightMutedColor?.color ??
Colors.grey,
snapshot.data?.mutedColor?.color ?? Colors.grey,
]),
borderRadius: BorderRadius.circular(20)),
child: ListTile(
leading: AnimatedBuilder(
// rotate the song cover image
animation: _animationController,
builder: (_, child) {
// if song is not playing
if (!isPlaying) {
_animationController.stop();
} else {
_animationController.forward();
_animationController.repeat();
}
return Transform.rotate(
angle: _animationController.value * 2 * math.pi,
child: child);
},
child: CircleAvatar(
radius: 30,
backgroundColor: Colors.grey,
backgroundImage: AssetImage(
player.getCurrentAudioImage?.path ?? '')),
),
onTap: () => Navigator.push(
context,
CupertinoPageRoute(
fullscreenDialog: true,
builder: (context) => PlayerPage(
player: player,
))),
title: Text(player.getCurrentAudioTitle),
subtitle: Text(player.getCurrentAudioArtist),
trailing: IconButton(
padding: EdgeInsets.zero,
onPressed: () async {
await player.playOrPause();
},
icon: isPlaying
? const Icon(Icons.pause)
: const Icon(Icons.play_arrow),
),
),
);
},
),
],
),
);
}
@override
void dispose() {
player.dispose();
super.dispose();
}
}