در قسمت قبلی از سری مباجث آموزش برنامه نویسی اندروید، کار را با Activity ها شروع کردیم (اینجا).همچنین درباره چرخه زندگی Activity ها صحبت کردیم و امروز میخواهیم در قالب یک پروژه عملی و کاربردی آن را بررسی کنیم. پروژه ای که انتخاب کردیم یک نرم افزار پخش آهنگ ساده و زیبا (Music Player)است که هم کدهای جاوای آن را بررسی می کنیم و هم طراحی layout آن را. پس با ما همراه باشید تا قدم به قدم با یکدیگر پیش برویم.
نویسنده: احسان قربان نژاد
لینک دانلود سورس کامل پروژه: اینجا
برای شروع سورس کد کامل پروژه را از لینک بالا میتوانید دریافت کنید. ما فرض میکنیم می خواهید از صفر شروع کنید. برای این کار، ابتدا یک پروژه اندروید بسازید (در مباحث قبلی به آن پرداخته شده است) . پروژه ما در محیط Eclipse نوشته شده است. شما میتوانید در Android Studio آن را بنویسید چرا که دستورات همان دستورات هستند.
شروع طراحی
می دانیم که با ساخت یک پروژه دو فایل MainActiviyty.java و activity_main.xml ایجاد میگردند (یکی در پوشه src و دیگری در پوشه res/layout ) برای شروع به سراغ آرایش صفحه موزیک پلیرمان می رویم (یعنی activity_main.xml ) . صفحه ای که می خواهیم طراحی کنیم باید به شکل زیر در آید. برای شروع کدهای زیر را در فایل activity_main.xml کپی کنید)
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="10dp" > <RelativeLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" > <ImageView android:id="@+id/songImage" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#eee" android:scaleType="centerCrop" /> <ImageButton android:id="@+id/playBtn" android:layout_width="160dp" android:layout_height="160dp" android:layout_centerInParent="true" android:background="@drawable/play_selector" android:tag="pause" /> <TextView android:id="@+id/title" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="#2d2d2d" android:gravity="center" android:padding="10dp" android:textColor="#fff" android:textSize="22sp" android:textStyle="bold" /> </RelativeLayout> <SeekBar android:id="@+id/seekBar" android:layout_width="match_parent" android:layout_height="wrap_content" android:progressDrawable="@drawable/red_scrubber_progress" android:thumb="@drawable/normal_scrobler" /> </LinearLayout>
در آرایش صفحه ما از SeekBar برای کنترل پخش موسیقی، ImageView برای نمایش تصویر کاور آهنگ، ImageButton برای دکمه play وTextView برای نمایش عنوان ترانه استفاده کرده ایم. دقت کنید که سه تگ عکس، دکمه و تکست، هر سه در لایه ای از نوع RelativeLayout قرار گرفته اند تا بتوانیم آنها را روی هم بچینیم. همچنین این RelativeLayout را به همراه SeekBar در لایه ای از نوع LinearLayout قرار داده ایم تا زیر هم با یک نسبت قرار گیرند. ( به صفت layout_weight در LinearLayout دقت کنید. دقت کنید که ما درطراحی این layout از یکسری drawable استفاده کرده ایم و آن هارا با دستور @drawable آدرس داده ایم. بعضی از آنها عکس با فرمت .png هستند و بعضی از آنها فایل با پسوند xml. که در پوشه ای که خودمان با نام drawable ساخته ایم، قرار دارد. کل این پوشه را از داخل سورس کامل پروژه کپی کنید و به پوشه res پروژه خودتان اضافه کنید. این پوشه حاوی چندین تصویر و همچنین ۳ فایل xml است که این سه فایل در زیر آورده شده اند:
play_selector.xml
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android" > <item android:state_pressed="true" android:drawable="@drawable/play_press" /> <item android:drawable="@drawable/play" /> </selector>
pause_selector.xml
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android" > <item android:state_pressed="true" android:drawable="@drawable/pause_press" /> <item android:drawable="@drawable/pause" /> </selector>
ما برای دکمه play و pause یک ImageButton مد نظر قرار داده ایم و برای background آن بجای آدرس به یک drawable از نوع عکس، یک selector در نظر گرفته ایم. در سلکتور ها رفتار کلیک را میتوانیم توضیف کنیم. سلکتور ها میگویند که در هنگام press شدن یک View چه تغییر شکلی رو آن اعمال شود. مثلا ما در این پروژه میخواهیم وقتی کاربر دکمه را لمس کرد، قبل از برداشته شدن انگشتش، عکس آیکن مان تغییر رنگ دهد. به همین منظور بود که در بالا از هر آیکن دو نوع با رنگهای متفاوت قرار دادیم. ساختار فایل های selector را دربالا میینید. علاوه بر selector، نیاز داریم تا شکل seekBar را به شیوه ای که خودمان دوست داریم طراحی کنیم. این کار را، هم برای شستی (thumb) و هم برای نوار پس زمینه seekBar انجام داده ایم که در زیر آن را می بینید. برای درگیر نشدن با جزییات طراحی، کافیست پوشه drawable را از سورس کامل پروژه به داخل برنامه تان کپی کنید. تمرکز ما در این آموزش در برنامه نویسی در داخل MainActivity.java است.
red_scrubber_progress.xml
<?xml version="1.0" encoding="utf-8"?> <layer-list xmlns:android="http://schemas.android.com/apk/res/android" > <item android:id="@android:id/background" android:drawable="@drawable/red_scrubber_track_holo_light"/> <item android:id="@android:id/secondaryProgress"> <scale android:drawable="@drawable/red_scrubber_secondary_holo" android:scaleWidth="100%" /> </item> <item android:id="@android:id/progress"> <scale android:drawable="@drawable/red_scrubber_primary_holo" android:scaleWidth="100%" /> </item> </layer-list>
MainActivit.java
حال که طراحی مان به اتمام رسید، وقت آن است المان های آن را در سمت جاوا دستکاری کنیم. کد کامل کلاس MainActivity.java در انتهای پست قرار داده شده است. ما میخواهیم این کلاس راT مرحله به مرحله بسازیم و کامل کنیم. فایل MainActivity.java پروژه تان، را باز کنید. کارمان را با متد OnCreate شروع میکنیم. اما قبل از رفتن به داخل onCreate المان های مورد نیاز را قبل از آن (در بدنه کلاس)، بصورت زیر تعریف کنید:
package ir.ehsanet.sample.musicplayer; import android.app.Activity; import android.media.MediaPlayer; import android.media.MediaPlayer.OnCompletionListener; import android.os.Bundle; import android.os.Handler; import android.view.View; import android.view.View.OnClickListener; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; import android.widget.TextView; public class MainActivity extends Activity { ImageButton imgBtn; TextView titleTextView; MediaPlayer mp; SeekBar seekbar; Handler handler; Runnable runnable; ImageView songImageView; @Override protected void onCreate(Bundle savedInstanceState) { // ... } }
حال باید در اولین قدم، در داخل onCreate المان ها را find کنیم.
super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); imgBtn = (ImageButton) findViewById(R.id.playBtn); seekbar = (SeekBar) findViewById(R.id.seekBar); songImageView = (ImageView) findViewById(R.id.songImage); titleTextView = (TextView) findViewById(R.id.title);
سپس عنوان و تصویر آهنگ را set میکنیم. برای اینکار از دستورات زیر استفاده میکنیم:
titleTextView.setText("Ashoobam - Chartar"); songImageView.setImageResource(R.drawable.song_image);
ساخت یک MediaPlayer
حال نوبت تعیین آهنگی که باید پخش شود می رسد. برای اینکار از کلاس MediaPlayer استفاده میکنیم و متد Create را روی آن صدا میزنیم. این متد در خروجی خود، یک شیء از نوع MediaPlayer به ما میدهد که کافیست متد start() را روی آن صدا بزنیم. قبل از آن یادمان باشد باید یک آهنگ در پوشه res/raw قرار دهیم. اگر پوشه raw را ندارید آن را بسازید و یک فایل mp3داخل آن قرار دهید. دقت کنید که اسم فایل تان تنها از حروف کوچک، آندرلاین و اعداد تشکیل شده باشد . حروف بزرگ یا فاصله و یا سایر علامت ها کار را خراب میکند. حال می توانید در جاوا همانطور که در جلسه قبلی بیان شد، آهنگتان را با R.raw.file_name آدرسدهی کنید. یادمان باشد بطور کلی هرگاه بخواهیم چیزی را از پوشه res در جاوا آدرس دهی کنیم از R استفاده کنیم. گفتیم که برای پخش یک موسیقی یا صدا در اندروید باید از کلاس MediaPlaer استفاده کنیم. برای اینکار از کدهای زیر استفاده مینکیم.
mp = MediaPlayer.create(MainActivity.this, R.raw.music);
نکته) ما در این پروژه آهنگ را از پوشه اپ خودمان یعنی raw می خوانیم. اگر میخواهید آهنگ ها را از روی حافظه sdcard بخوانید کمی ساخت MediaPlayer تان تفاوت خواهد کرد که در آین آموزش به آن پرداخته نمی شود.
بروزرسانی موقعیت SeekBar
ما میخواهیم SeekBar را در حین پخش آهنگ، آپدیت کنیم تا همراه آهنگ جلو برود. برای این کار از کلاس Handler استفاده میکنیم. یکی از کارهایی که با handler ها میتوان کرد، انجام کاری پس از چند ثانیه است. ما فعلا یک handler و یک کار از نوع Runnable تعریف کرده ایم.
handler = new Handler(); runnable = new Runnable() { @Override public void run() { updateSeekbar(); } };
در داخل متد run از Runnable متد updateSeekBar را صدا زده ایم. این متد را که بصورت زیر است، را در خارج از OnCreate ( والبته در داخل کلاسMainActivty اضافه کنید)
public void updateSeekbar() { //find current progress position of mediaplayer float progress = ((float) mp.getCurrentPosition() / mp.getDuration()); //set this progress to seekbar seekbar.setProgress((int) (progress * 100)); //run handler again after 1 second handler.postDelayed(runnable, 1000); }
در این متد ابتدا پیشرفت آهنگ مجاسبه میشود، سپس بر روی SeekBar مقدار آن set می شود. دقت کنید که در پایان این متد با دستور handler.postDelayed(runnable,1000) دوباره خودش،یعنی updateSeekbar ، صدا زده زده میشود اما با تاخیر ۱۰۰۰ میلی ثانیه ای یعنی یک ثانیه.اگر کمی به آن فکر کنید متوجه علت کار خواهید شد. دلیل آن است که SeekBar هر یک ثانیه باید آپدیت شود (جلو برود با آهنگ) و ما مسئول آپدیت کردن آن هستیم. پس چه بهتر که هر یک ثانیه خودش را صدا بزنیم.
اما فرآیند pause و play کردن به چه صورت است؟
تا اینجا ما فقط همه چیز را تعریف کردیم. برای این اینکه آهنگ شروع به پخش شود باید روی شی ای که از کلاس MediaPlayer ساختیم ( به نام mp ) ، متد start را صدا بزنیم. اما این کار را وقتی میکنیم که بدانیم روی دکمه play کلیک شده است. اما Pause کردن چی؟ ما در دو زمان، آهنگ را باید pause میکنیم. یکی هنگامی که کاربر روی دکمه play قبلی که حالا تبدیل به دکمه pause شده کلیک کند. و دیگری هنگامی که یک اتفاق مخصوصی که خارج از اپ ما بیوفتد. مثلا یک نفر با ما تماس برقرارکند (که آهنگ دیگر پخش نشود) و یا کاربر از اپ به هرصورتی خارج شود (بی آنکه روی دکمه Pause کلیک کند. مثلا از اپ ما بپرد به یک اپ دیگر). یادمان باشد در این دو مورد، با اینکه از اپ خارج میشویم، آهنگ قطع نمیشود و ما مسئول قطع کردن آن هستیم. برای اینکه بخواهیم روی click یک المان کاری انجام دهیم باید برای آن المان، onClickListener بنویسیم و با دستور setOnClickListener ، آن را به المان نسبت بدهیم. کدهای زیر را در داخل OnCrete و در انتهای آن اضافه میکنیم:
imgBtn.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { String s = (String) imgBtn.getTag(); if (s.equals("pause")) { play(); } else { pause(); } } });
(دقت کنید اگر کدها را مرجله به مرحله به پروژه تان اضافه می کنید، شاید زیر دستورات خطوط قرمزی ظاهر شود مبنی بر اینکه دستوراتی ناشناخته در حال استفاده است. برای حل این مشکل، هر قطعه کدی که از جای دیگر، به برنامه تان اضافه میکنید ، یک بار ctrl + shift + o را فشار دهید تا کلاس های لازم به برنامه تان Import شود.)
در بالا، چون ما تنها یک دکمه داریم ابتدا باید تشخیص دهیم که وضعیت دکمه در حال حاضر play است یا pause. برای این کار از دستورات setTag(…) و getTag() استفاده کرده ایم. از این دستورات برای نگهداری دیتا ( از هر نوعی) روی هر المانی استفاده میشود ( مثلا در اینجا المان ImageButton) در این دستورات ما از دو متد play() و pause() استفاده کردیم. دو متد را در خارج از OnCreate ( والبته در داخل کلاسMainActivty) اضافه کنید.
public void pause() { mp.pause(); imgBtn.setTag("pause"); // next state should be 'pause' imgBtn.setBackgroundResource(R.drawable.play_selector); // stop seekbar handler.removeCallbacks(runnable); } public void play() { mp.start(); imgBtn.setTag("play"); // next state should be 'pause' imgBtn.setBackgroundResource(R.drawable.pause_selector); //start seekbar updateSeekbar(); }
نکته: دستور handler.removeCallbacks(runnable) در داخل متد () pause ، چرخه فراخوانی updateSekkbar را می شکند و آن را متوقف میکند. برای دوباره راه انداختن آپدیت یک ثانیه ای مان کافیست () updateSekkbar را در () play صدا بزنیم. تا اینجا کار تمام است. اگر برنامه را تست کنید، به درستی کار میکند. اما ما هنوز فکری به حال Pause کردن اتوماتیک و بروز رسانی seekBar نکرده ایم.
حالت دوم pause
این حالت وقتی روی می دهد که کاربر یا دکمه back را بزند، یا به صفحه دیگری از اپ خودمان برود، یا یک نفر زنگ بزند، یا اپ دیگری روی اپ ما باز شود. در این جالت ما نیاز داریم درباره چرخه حیات Activity ها اطلاع داشته باشم که ما این موضوع را در مطلب قبلی از سری آموزش های اندروید بررسی کردیم. برای این هدف،(..) onPause را در داخل Activity مان بازنویسی میکنیم.(برای زمانی که کاربر ناخودآگاه یا خودآگاه از Activity خارج میشود) همچنین متد (..) onResume برای وقتی که کابر دوباره برمی گردد. دو متد زیر را به کلاس Activity تان اضافه کنید.
@Override protected void onPause() { super.onPause(); mp.pause(); imgBtn.setBackgroundResource(R.drawable.play_selector); } @Override protected void onResume() { super.onResume(); String s = (String) imgBtn.getTag(); // check if its not first onResume which is invoked // note that onResum invoked always after onCreate()-> onStart()-> // and also after we come back to our application (Activity) // if s equals to 'pause' it means we used setTag('pause') before // it means that we now should start the player again. if (!s.equals("pause")) { mp.start(); imgBtn.setBackgroundResource(R.drawable.pause_selector); } }
کنترل SeekBar
برای کنترل seekbar توسط کاربر (عقب جلو کشیدن شستی آن برای جابجایی موقعیت آهنگ) دستورات زیر را به داخل OnCreate در ادامه دستوراتی که قبلا گفتیم اضافه کنید.
seekbar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { @Override public void onStopTrackingTouch(SeekBar seekBar) { long now = (long) ((float) seekbar.getProgress() / 100 * mp.getDuration()); mp.seekTo((int) now); handler.postDelayed(runnable, 1000); } @Override public void onStartTrackingTouch(SeekBar seekBar) { handler.removeCallbacks(runnable); } @Override public void onProgressChanged(SeekBar seekBar, int progress,boolean fromUser) { } });
همچنین برای اینکه فرآیند آپدیت seekBar که هریک ثانیه اتفاق می افتاد و تغییر شکل آیکون pause به play در صورت رسیدن به آخر آهنگ اتفاق بیوفتد، دستور زیر را در ادامه قبلی ها، به داخل OnCreate اضافه کنید.
mp.setOnCompletionListener(new OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { pause(); } });
فایل نهایی MainActivity.java
کلاس MainActivity ما آماده است. در زیر میتوانید همه کدهای آن را با هم مشاهده کنید.
package ir.ehsanet.sample.musicplayer; import android.app.Activity; import android.media.MediaPlayer; import android.media.MediaPlayer.OnCompletionListener; import android.os.Bundle; import android.os.Handler; import android.view.View; import android.view.View.OnClickListener; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; import android.widget.TextView; public class MainActivity extends Activity { ImageButton imgBtn; TextView titleTextView; MediaPlayer mp; SeekBar seekbar; Handler handler; Runnable runnable; ImageView songImageView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); imgBtn = (ImageButton) findViewById(R.id.playBtn); seekbar = (SeekBar) findViewById(R.id.seekBar); songImageView = (ImageView) findViewById(R.id.songImage); titleTextView = (TextView) findViewById(R.id.title); titleTextView.setText("Ashoobam - Chartar"); songImageView.setImageResource(R.drawable.song_image); mp = MediaPlayer.create(MainActivity.this, R.raw.music); handler = new Handler(); runnable = new Runnable() { @Override public void run() { updateSeekbar(); } }; seekbar.setOnSeekBarChangeListener(new OnSeekBarChangeListener() { @Override public void onStopTrackingTouch(SeekBar seekBar) { long now = (long) ((float) seekbar.getProgress() / 100 * mp .getDuration()); mp.seekTo((int) now); handler.postDelayed(runnable, 1000); } @Override public void onStartTrackingTouch(SeekBar seekBar) { handler.removeCallbacks(runnable); } @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { } }); mp.setOnCompletionListener(new OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { pause(); } }); imgBtn.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { String s = (String) imgBtn.getTag(); if (s.equals("pause")) { play(); } else { pause(); } } }); } @Override protected void onPause() { super.onPause(); mp.pause(); imgBtn.setBackgroundResource(R.drawable.play_selector); } @Override protected void onResume() { super.onResume(); String s = (String) imgBtn.getTag(); // check if its not first onResume which is invoked // note that onResum invoked always after onCreate()-> onStart()-> // and also after we come back to our application (Activity) // if s equals to 'pause' it means we used setTag('pause') before // it means that we now should start the player again. if (!s.equals("pause")) { mp.start(); imgBtn.setBackgroundResource(R.drawable.pause_selector); } } public void updateSeekbar() { //find current progress position of mediaplayer float progress = ((float) mp.getCurrentPosition() / mp.getDuration()); //set this progress to seekbar seekbar.setProgress((int) (progress * 100)); //run handler again after 1 second handler.postDelayed(runnable, 1000); } public void pause() { mp.pause(); imgBtn.setTag("pause"); // next state should be 'pause' imgBtn.setBackgroundResource(R.drawable.play_selector); // stop seekbar handler.removeCallbacks(runnable); } public void play() { mp.start(); imgBtn.setTag("play"); // next state should be 'pause' imgBtn.setBackgroundResource(R.drawable.pause_selector); //start seekbar updateSeekbar(); } }
نکته ۱) دقت کنید هرجا می بینید خط قرمزی زیر دستوری کشیده شده است، ممکن است به این خاطر باشد که کلاس های مربوط به آن import نشده باشند. با خیال راحت یکبار ctrl + shift + o را فشار دهید تا کلاس هایی که پروژه تان نیاز دارد همی باهم import شوند. نکته ۲) اگر فایل R را همچنان برنامه تان نمیشناسد، چند دلیل دارد که در پست های بعدی آن را بررسی کنیم. اما مهمترین دلیل آن یک اشکال در پوشه res تان است. یا نام فایل های drawable تان از حروف بزرگ،غیرمجاز و یا فاصله تشکیل شده است. ویا در فایل activity_main.xml جایی اشتباه کرده اید و تگ ها را درست ننوشتید.
عالی! ممنون بسیار زیاد.
متشکرم دوست عزیزم
خیلی خیلی عالیه!
آموزش هاتون رو ادامه بدین! من که همینطور منتظر بعدی ها هستم!
خدا قوت!!!!
سپاس.
سلام استاد
لطفا اموزش دیتابیسم بذارین. ممنون
حاجی واقعا مرسی
من که هیچ چیزی از جاوا و اندروید و برنامه نویسی نمیدونستم خیلی خیلی راحت مطالبی رو گفتید رو خوندمو یاد گرفتم.
خوندن آموزش جاوا منو خیلی مشتاق کرد که اندروید رو هم یاد بگیرم.
به قول مادربزرگم ” ایشالا دست به خاک میزنی طلا بشه واست”
واقعا مرسی
ممنون دوست خوبم.نظر لطف شماست.
سلام تنها فایده ای که برام داشت یک نکته پیش پاافتاده اما مهم بود اونم اینکه توی دوتا تابع مختلف میتونیم هی همدیگه رو صدا کنیم
وگرنه هر کاری کردم سیکبار آپدیت نشد که نشد
نمیدونم شاید اشکال از منه
به هر حال ممنون
فکر میکنم خیلی چهشی مطالب رو سنگین کردین من و تو ظراحی قالبش که بعضی جاهارو متوجه نشدم. کد ها هم که یهو خیلی پیشرفته شد. تازه من جاوارو خوندم. اما احساس میکنم مطالب خیلی غیر عادی سنگین شد.
بازم ممنون
خیلی ممنون از زحماتتون ولی کاش چنتا آهنگ بود که امکان جلو دادن و عقب دادن هم داشت اونموقع کامل میشد
ممنونم توضیحاتتونم کامل بود علاوه بر کد.
خیلی راضی ام:)
سلام خسته نباشید …
ببخشید میخوام بدونم چجوری در یک اکتیویتی با زدن یک دکمه به عقب برگردیم یا به اکتیویتی قبلی
سلام
تابع onBackPressed() را صدا بزنید
اگر بخواهیم به جای اینکه از raw بگیره موزیک رو از اینترنت بگیره چیکار باید بکنیم؟؟؟ ضروریه، لطفا جواب بدید.ممنون
سلام
تشکر بابت آموزش های خوبتون.
یه سوال داشتم. من میخوام از OnCreate استفاده کنم. از چه کلیدهایی باید استفاده کنم که وقتی O رو تایپ میکنم، لیست همه دستورات بیاد و لازم نباشه که کل متد رو تایپ کنم؟
خیلی ممنون
جوابو پیدا کردم
Ctrl+Space
تشکر از آموزش عالیتون
فقط اینکه واقعا به آموزش کار کردن و پخش موسیقی در پس زمینه هنگام بسته شدن برنامه هم نیاز دارم اگر این اموزشم قرار بدید و با سرویس ها آشنا بشیم خیلی لطف کرده اید….
باز هم ممنون
سلام
باتشکر از آموزش های خوبتون
قسمت هندلر درست کار نمیکنه متاسفانه
سلام وقت بخیر
آیا این امکان وجود داره ک یه handler رو pause و resume کنیم .
سپاس