طريقة عرض ملفّات الوسائط المتعدّدة في ListView لتشغيلها من خلال MediaPlayer

مقدّمة

يمكن من خلال MediaPlayer class الموجودة في مكتبة نظام Android؛ أن يتمكَّن المبرمج من تشغيل الصوتيّات ومقاطع الفيديو بأنواعها المختلفة بحسب ما يدعمه النظام. سنتعرَّف في هذا المقال على طريقة استخدام ListView widget لعرض الصوتيّات المتاحة، بحيث يمكن للمستخدم اختيار أحدها من القائمة ليتمّ بعد ذلك تشغيلها بواسطة الMediaPlayer class، كما سنقوم بتزويد التطبيق بأزرار لتشغيل الصوتيّات وإيقافها وقوفاً مؤقّتاً (أي Pause) أو وقوفاً تامّاً (Stop).

 

تصميم واجهة التطبيق

سيحتوي التطبيق على ListView widget بالإضافة إلى ثلاثة أزرار: Play، Pause، Stop. يمكن عرض جميع هذه العناصر بالطريقة التي يرغبها المبرمج، ولكن بالنّسبة لي، فقد قمت بوضعها في RelativeLayout، بحيث تعلوها الListView، وتدنوها LinearLayout أفقيّة تحتوي الثلاث أزرار خاصّتنا. ما يلي طريقة ترتيب عناصر الواجهة:

بحيث تظهر الواجهة كالآتي:

يمكن استخدام الكود التالي للحصول على الواجهة:

	  <?xml version="1.0" encoding="utf-8"?>
	  <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
	  xmlns:app="http://schemas.android.com/apk/res-auto"
	  xmlns:tools="http://schemas.android.com/tools"
	  android:layout_width="match_parent"
	  android:layout_height="match_parent"
	  tools:context=".MainActivity">

	  <ListView
	  android:id="@+id/mylistview"
	  android:layout_width="match_parent"
	  android:layout_height="wrap_content"
	  android:layout_above="@+id/currentlyPlaying"
	  android:layout_alignParentTop="true" />

	  <TextView
	  android:id="@+id/currentlyPlaying"
	  android:layout_width="match_parent"
	  android:layout_height="wrap_content"
	  android:layout_above="@+id/mylinearlayout"
	  android:text="Playing:"
	  android:textAlignment="viewStart"
	  android:textSize="30sp"
	  android:textStyle="bold" />

	  <LinearLayout
	  android:id="@+id/mylinearlayout"
	  android:layout_width="match_parent"
	  android:layout_height="wrap_content"
	  android:layout_alignParentBottom="true"
	  android:orientation="horizontal">

	  <Button
	  android:id="@+id/button_play"
	  android:layout_width="wrap_content"
	  android:layout_height="wrap_content"
	  android:layout_weight="1"
	  android:text="Play" />

	  <Button
	  android:id="@+id/button_pause"
	  android:layout_width="wrap_content"
	  android:layout_height="wrap_content"
	  android:layout_weight="1"
	  android:text="Pause" />

	  <Button
	  android:id="@+id/button_stop"
	  android:layout_width="wrap_content"
	  android:layout_height="wrap_content"
	  android:layout_weight="1"
	  android:text="Stop" />
	  </LinearLayout>
	  </RelativeLayout>
	  

 

برمجة الواجهة

إن قمت بتصميم الواجهة بنفسك دون اللجوء للكود أعلاه، فلا تنسى تسمية كل عنصر من العناصر بid محدَّد لنتمكَّن من الوصول إليه برمجيّاً. سنقوم أوّلاً بتعريف وصلات لعناصر الواجهة التي سنحتاج للتعامل معها برمجيّاً، وسنستخدم لذلك دالّة ()findViewById. العناصر التي نحتاج التعامل معها هي الlistView بالإضافة للأزرار الثلاثة، بالتالي، سنقوم بالإشارة إلى كلّ عنصر عند بدء التطبيق كما يلي:

	  public class MainActivity extends AppCompatActivity {
	  @Override
	  protected void onCreate(Bundle savedInstanceState) {
	  super.onCreate(savedInstanceState);
	  setContentView(R.layout.activity_main);

	  Button playbutton_ref = findViewById(R.id.button_play);
	  Button pausebutton_ref = findViewById(R.id.button_pause);
	  Button stopbutton_ref = findViewById(R.id.button_stop);

	  ListView mylistview_ref = findViewById(R.id.mylistview);
	  
 

برمجة ال listView

للاستفادة من الlistView widget والتمكُّن من إضافة العناصر إليها وحذفها، يجب علينا ربطها بما يُسمّى ListAdapter، والتي تكون مرتبطة بArrayList من نوع String تحتوي جميع أسماء الصوتيّات المعروضة للمستخدم. لنتمكَّن من إنشاء ListAdapter، يجب علينا أوّلاً تصميم كيف سيبدو كلّ عُنصر في القائمة، بحيث سنحدِّد كيفيّة ظهوره من خلال ملفّ Layout. لذا، سنقوم بإنشاء ملفّ جديد في مجلَّد “layout” سنسمّيه “listview_item.xml”، وسنضع فيه الكود التالي:

XHTML
	  <?xml version="1.0" encoding="utf-8"?>

	  <TextView xmlns:android="http://schemas.android.com/apk/res/android"
	  android:id="@+id/label"
	  android:layout_width="fill_parent"
	  android:layout_height="fill_parent"
	  android:padding="10dip"
	  android:textSize="18sp"></TextView>
	  

الآن ننتقل مجدّداً لكود الجافا في ملفّ “MainActivity.java” ونقوم بإنشاء ArrayList من نوع String، وستكون صلاحيّات الوصول إليها private، إذ لن نستخدمها خارج الMainActivity class:

Java
	  
		public class MainActivity extends AppCompatActivity {
		private List<String> myMedia = new ArrayList<>();

		@Override
		protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
	  

لائحة المصفوفات myMedia ستحتوي أسماء ملفّات الصوتيّات المتاحة، والتي ستظهر في الListView widget للمستخدم. الآن سنقوم بتعريف ListAdapter جديدة ونربطها بالLayout (الذي قمنا بإنشائه لشكل عناصر اللائحة) بالإضافة إلى لائحة myMedia والتي ستحتوي عناصر الListView، فسنقوم بتعريفها كالتالي في دالّة onCreate:

	  ListAdapter myMediaAdapter = new ArrayAdapter<String>(this, R.layout.listview_item, myMedia);
	  
	  
	  

 

الآن ما علينا سوى تزويد الlistView widget بالListAdapter الذي ستعتمد عليه في معرفة العناصر التي ستقوم بعرضها، فنقوم باستخدام دالّة setAdapter() لذلك:

	  
	  mylistview_ref.setAdapter(myMediaAdapter);
	

الآن، عند إضافة أي نصّ للائحة myMedia، فسيظهر تلقائيّاً في الListView خاصّتنا.

 

عرض الصوتيّات المُراد تشغيلها

سنقوم بإضافة الصوتيّات التي نرغب بعرضها للمستخدم في مجلَّد “/res/raw”، لذا، فسنقوم بإنشاء مجلَّد يحمل الاسم “raw” في مجلَّد “res” والموجود في المجلَّد الرئيسي للمشروع، بعد ذلك سنقوم بإضافة ملفّاتنا الصوتيّة (بصيغة mp3) إليه. بما أنّه لا توجد طريقة مباشرة للوصول إلى جميع ملفّات مجلَّد “/res/raw” برمجيّاً، فسنقوم بإنشاء مصفوفة من نوع String نضع فيها أسماء ملفّاتنا، فمثلاً لو كان لدينا ثلاث ملفّات ألا وهي: sound1.mp3، وsound2.mp3، وsound3.mp3، فسنقوم بتعريف المصفوفة كالتالي:

	  
	  String []files = {"sound1", "sound2", "sound3"};
	

وبعدها ننسخ هذه المصفوفة إلى لائحة myMedia كالتالي:

	  
	  myMedia.addAll(Arrays.asList(files));
	  
	  
	  

 

تشغيل الصوتيّات باستخدام MediaPlayer

سنقوم الآن بتعريف كائن من نوع MediaPlayer سنسمّيه myPlayer، وسنجعل صلاحيّات الوصول إليه هي private، كما سنحتاج إلى متغيِّرين من نوع int، الأوّل لتخزين حالة الMediaPlayer، وسنسمّيه mplayer_status، أمّا الآخر فسيكون لتخزين موقع الملفّ الذي تمَّ اختياره في اللائحة، وسنسمّيه selected_media، وسنجعل قيمتهما الأوّليّة -1، وستتبيَّن لنا أهميّة كِلا المتغيِّرين لاحقاً. بذلك، سيكون لدينا ثلاث تعريفات في MainActivity class كالتالي:

		public class MainActivity extends AppCompatActivity {
		private List<String> myMedia = new ArrayList<>();

		private MediaPlayer myPlayer;
		private int mplayer_status = -1;
		private int selected_media = -1;

	  

برمجة حالات مشغِّل الصوتيّات

سيتم اختيار الملفّ الصوتي فور النقر عليه من الlistView، وسيتم تشغيله تلقائيّاً، لذا سنقوم باستخدام ()setOnItemClickListener لمعرفة العنصر الذي تمَّ اختياره. بحسب آليّة عمل MediaPlayer class، فإنّه يجب حجز كائن جديد (()new MediaPlayer) في كلّ مرّة نريد تشغيل ملفّ معيَّن، وبعد ذلك، نقوم بتحديد مكان تواجد الملفّ الصوتي باستخدام دالّة ()setDataSource، وبعدها يجب استدعاء دالّة ()prepare لتحضير الملفّ للتشغيل، أخيراً نقوم باستدعاء دالّة ()start. سنقوم بوضع القيمة 1 في المتغيِّر mplayer_status والتي تدلّ على أنَّه يوجد ملفّ قيد التشغيل حاليّاً، وذلك لنتمكَّن من تتبُّع حالة myPlayer. وبذلك، فقد أصبح كود ()setOnItemClickListener كالتالي:

	  mylistview_ref.setOnItemClickListener(new AdapterView.OnItemClickListener() {
	  @Override
	  public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
	  try {
	  myPlayer = new MediaPlayer();

	  mplayer_status = 1;
	  myPlayer.setDataSource(getApplicationContext(), Uri.parse("android.resource://" + getPackageName() + "/raw/" + myMedia.get(position)));
	  myPlayer.prepare();
	  myPlayer.start();
	  } catch (IOException e) {
	  e.printStackTrace();
	  }
	  }
	  });
	

لاحظ أنَّ صيغة الوصول لأيّ ملفّ في مجلَّدات تطبيق الآندرويد الداخليّة هي “///:android.resource” متبوعةً باسم حزمة التطبيق (Package name)، ومن ثمَّ اسم الملفّ، وبما أنَّ ملفّاتنا موجودة في مجلَّد raw، لذا فستكون الصيغة كما هو ظاهر في السطر الثامن من الكود أعلاه.

 

توجد لدينا مشكلة في الكود الذي كتبناه، وهي أنَّ دالّة ()prepare لا يجب استدعاؤها سوى مرّة واحدة بعد اختيار الملفّ الصوتي، وإلّا فسينهار برنامجنا ويتمّ الخروج منه، فماذا لو قام المستخدم بالضغط على الملفّ ذاته مرّتين؟ لحلّ هذه المشكلة، سنقوم بتخزين موقع العنصر (في اللائحة) للملفّ الذي تمَّ اختياره في كل مرّة ينقر فيها المستخدم على ملفّ، وفي حال كان نفس الملفّ الذي تمَّ اختياره في المرّة الأولى، فلن نقوم بفعل أي شيء حينها، وسنستخدم لذلك المتغيِّر selected_media الذي قمنا بتعريفه مُسبقاً، واستخدام جملة if للتأكُّد من الحالة:

 

	  mylistview_ref.setOnItemClickListener(new AdapterView.OnItemClickListener() {
	  @Override
	  public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
	  try {
	  if(selected_media != position) {
	  myPlayer = new MediaPlayer();

	  mplayer_status = 1;
	  myPlayer.setDataSource(getApplicationContext(), Uri.parse("android.resource://" + getPackageName() + "/raw/" + myMedia.get(position)));
	  myPlayer.prepare();
	  myPlayer.start();
	  }

	  selected_media = position;
	  } catch (IOException e) {
	  e.printStackTrace();
	  }
	  }
	  });
	

يُفضلَّ استدعاء دالّة ()release قبل إنشاء كائن جديد (انتبه إلى أنّه من الممكن أن يكون myPlayer ليس معرّفاً بالأصل، وذلك في أوّل مرّة يتم تشغيل ملفّ، ففي حال استدعاء أي دالّة منه فسينهار التطبيق):

	  mylistview_ref.setOnItemClickListener(new AdapterView.OnItemClickListener() {
	  @Override
	  public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
	  try {
	  if(selected_media != position) {
	  if(myPlayer != null) myPlayer.release();
	  myPlayer = new MediaPlayer();

	  mplayer_status = 1;
	  myPlayer.setDataSource(getApplicationContext(), Uri.parse("android.resource://" + getPackageName() + "/raw/" + myMedia.get(position)));
	  myPlayer.prepare();
	  myPlayer.start();
	  }

	  selected_media = position;
	  } catch (IOException e) {
	  e.printStackTrace();
	  }
	  }
	  });
	

الآن تبقّى لنا برمجة الازرار: Play، وPause، وStop، ونقوم بذلك كالتالي:

	  playbutton_ref.setOnClickListener(new View.OnClickListener() {
	  @Override
	  public void onClick(View v) {
	  try {
	  if(mplayer_status == -1) myPlayer.prepare();
	  } catch (IOException e) {
	  e.printStackTrace();
	  }
	  myPlayer.start();
	  mplayer_status = 1;
	  }
	  });

	  pausebutton_ref.setOnClickListener(new View.OnClickListener() {
	  @Override
	  public void onClick(View v) {
	  myPlayer.pause();
	  mplayer_status = 0;
	  }
	  });

	  stopbutton_ref.setOnClickListener(new View.OnClickListener() {
	  @Override
	  public void onClick(View v) {
	  myPlayer.stop();
	  mplayer_status = -1;
	  }
	  });
	

لاحظ أنَّ القيمة -1 تُمثِّل حالة وقوف الMediaPlayer عن التشغيل وقوفاً تامّاً، والحالة 1 تُمثِّل العكس، أمّا القيمة 0 فهي تُمثِّل حالة كون المُشغِل في حالة الوقوف المؤقَّت (Paused).

الكود النهائي

	  public class MainActivity extends AppCompatActivity {
	  private List<String> myMedia = new ArrayList<>();
	  private List<String> myMediaPaths = new ArrayList<>();

	  private MediaPlayer myPlayer;
	  private int mplayer_status = -1;
	  private int selected_media = -1;

	  @Override
	  protected void onCreate(Bundle savedInstanceState) {
	  super.onCreate(savedInstanceState);
	  setContentView(R.layout.activity_main);

	  Button playbutton_ref = findViewById(R.id.button_play);
	  Button pausebutton_ref = findViewById(R.id.button_pause);
	  Button stopbutton_ref = findViewById(R.id.button_stop);

	  ListView mylistview_ref = findViewById(R.id.mylistview);

	  ListAdapter myMediaAdapter = new ArrayAdapter<String>(this, R.layout.listview_item, myMedia);
	  mylistview_ref.setAdapter(myMediaAdapter);

	  String []files = {"sound1", "sound2", "sound3"};

	  myMedia.addAll(Arrays.asList(files));

	  mylistview_ref.setOnItemClickListener(new AdapterView.OnItemClickListener() {
	  @Override
	  public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
	  try {
	  if(selected_media != position) {
	  if(myPlayer != null) myPlayer.release();
	  myPlayer = new MediaPlayer();


	  mplayer_status = 1;
	  myPlayer.setDataSource(getApplicationContext(), Uri.parse("android.resource://" + getPackageName() + "/raw/" + myMedia.get(position)));
	  myPlayer.prepare();
	  myPlayer.start();
	  }

	  selected_media = position;
	  } catch (IOException e) {
	  e.printStackTrace();
	  }
	  }
	  });

	  //Events
	  playbutton_ref.setOnClickListener(new View.OnClickListener() {
	  @Override
	  public void onClick(View v) {
	  try {
	  if(mplayer_status == -1) myPlayer.prepare();
	  } catch (IOException e) {
	  e.printStackTrace();
	  }
	  myPlayer.start();
	  mplayer_status = 1;
	  }
	  });

	  pausebutton_ref.setOnClickListener(new View.OnClickListener() {
	  @Override
	  public void onClick(View v) {
	  myPlayer.pause();
	  mplayer_status = 0;
	  }
	  });

	  stopbutton_ref.setOnClickListener(new View.OnClickListener() {
	  @Override
	  public void onClick(View v) {
	  myPlayer.stop();
	  mplayer_status = -1;
	  }
	  });


	  }
	  }
	

طُرُق تحسين التطبيق

البحث في مجلّدات الجهاز عن الصوتيّات

إنَّ طريقة استخدام مجلَّد “/res/raw” لن تكون طريقة عمليّة في الواقع، وإنّما ستكون الملفّات موجودة في ذاكرة الجهاز، لذا، سنقوم باستخدام طريقة للبحث عن جميع الملفّات التي تحمل صيغة “mp3.” في جهاز المستخدم. ولكن قبل ذلك، يجب علينا تفعيل أذونات الوصول لذاكرة المستخدم، فنقوم بإضافة السطريين التاليين في الملفّ “AndroidManifest.xml”:

	  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
	  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
	  
	

سنقوم الآن بتعريف دالّة جديدة نسمّيها ()getMediaFiles، وهي دالّة سنستخدم فيها أسلوب الاستدعاء الذاتي (Recursive function) للتنقُّل بين المجلّدات الرئيسيّة والفرعيّة والبحث عن الملفّات التي تنتهي بصيغة “mp3.”. ستأخذ الدالّة معاملاً واحداً من نوع File، وهذا المعامل هو فعليّاً عبارة عن عنوان المجلَّد المُراد البحث عن الملفّات بداخله (والذي سيكون عنوان الذاكرة External storage، والذي يُشير إلى “storage/emulated/0/” في معظم أجهزة الآندرويد):

	  private void getMediaFiles(File dir)
	  {
	  File []files = dir.listFiles();

	  if(files == null) return;

	  for(int i = 0; i < files.length; i++)
	  if (files[i].isDirectory())
	  getMediaFiles(files[i]);
	  else if(files[i].getName().endsWith(".mp3")) {
	  myMedia.add(files[i].getName());
	  }
	  }
	

 

المشكلة هي أنَّ لائحة myMedia ستحتوي أسماء الملفّات فقط وليس العنوان كاملاً، وبالتالي لن نتمكَّن من الوصول إلى الملفّ لاحقاً لتشغيله إذا لم نكن نعرف عنوانه، لذلك، سنقوم بتعريف لائحة جديدة نسمّيها “myMediaPaths”، وهي تماماً من نفس نوع myMedia، إلّا أنّها ستحتوي على العنوان كاملاً:

	  public class MainActivity extends AppCompatActivity {
	  private List<String> myMedia = new ArrayList<>();
	  private List<String> myMediaPaths = new ArrayList<>();
	

وسنعدِّل على الدالّة ()getMediaFiles بحيث تضيف العناوين كاملة إلى هذه اللائحة، أمّا الأسماء المجرّدة (والتي سنظهرها للمستخدم) ففي لائحة myMedia:

	  private void getMediaFiles(File dir)
	  {
	  File []files = dir.listFiles();

	  if(files == null) return;

	  for(int i = 0; i < files.length; i++)
	  if (files[i].isDirectory())
	  getMediaFiles(files[i]);
	  else if(files[i].getName().endsWith(".mp3")) {
	  myMedia.add(files[i].getName());
	  myMediaPaths.add(files[i].getAbsolutePath());
	  }
	  }
	

الآن ما علينا سوى استدعاء الدالّة وإلغاء طريقة القراءة من مجلَّد “/res/raw/” التي استخدمناها فيما سبق:

	  getMediaFiles(new File(Environment.getExternalStorageDirectory().getPath()));

	  //String []files = {"sound1", "sound2", "sound3"};

	  //myMedia.addAll(Arrays.asList(files));
	

وأخيراً نقوم بالتعديل على معامل دالّة ()setDataSource في ()setOnItemClickListener المستخدمة لتحديد الملفّ المُراد تشغيله في الMediaPlayer ليصبح كالتالي:

	  myPlayer.setDataSource(myMediaPaths.get(position));
	  
	

إظهار اسم الملفّ قيد التشغيل

يمكننا إضافة TextView لتبيِّن اسم الملفّ قيد التشغيل حاليّاً، فيمكننا تحديد النصّ الظاهر في هذا الTextView عن طريق استخدام ()setText في كلّ مرّة يضغط المستخدم فيها على ملفّ، بحيث نسترجع اسم الملفّ (مجرّداً) من لائحة myMedia ونضع اسمه في الTextView. نقوم بإنشاء TextView جديد في الواجهة، بحيث يكون بين الأزرار وقائمة الصوتيّات:

سنقوم بتسمية عنصر الTextView ب”currentlyPlaying”، بحيث يصبح كود الواجهة كالتالي:

	  <?xml version="1.0" encoding="utf-8"?>
	  <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
	  xmlns:app="http://schemas.android.com/apk/res-auto"
	  xmlns:tools="http://schemas.android.com/tools"
	  android:layout_width="match_parent"
	  android:layout_height="match_parent"
	  tools:context=".MainActivity">

	  <ListView
	  android:id="@+id/mylistview"
	  android:layout_width="match_parent"
	  android:layout_height="wrap_content"
	  android:layout_above="@+id/currentlyPlaying"
	  android:layout_alignParentTop="true" />

	  <TextView
	  android:id="@+id/currentlyPlaying"
	  android:layout_width="match_parent"
	  android:layout_height="wrap_content"
	  android:layout_above="@+id/mylinearlayout"
	  android:text="Playing:"
	  android:textAlignment="viewStart"
	  android:textSize="30sp"
	  android:textStyle="bold" />

	  <LinearLayout
	  android:id="@+id/mylinearlayout"
	  android:layout_width="match_parent"
	  android:layout_height="wrap_content"
	  android:layout_alignParentBottom="true"
	  android:orientation="horizontal">

	  <Button
	  android:id="@+id/button_play"
	  android:layout_width="wrap_content"
	  android:layout_height="wrap_content"
	  android:layout_weight="1"
	  android:text="Play" />

	  <Button
	  android:id="@+id/button_pause"
	  android:layout_width="wrap_content"
	  android:layout_height="wrap_content"
	  android:layout_weight="1"
	  android:text="Pause" />

	  <Button
	  android:id="@+id/button_stop"
	  android:layout_width="wrap_content"
	  android:layout_height="wrap_content"
	  android:layout_weight="1"
	  android:text="Stop" />
	  </LinearLayout>
	  </RelativeLayout>
	

ونقوم بتعريفه برمجيّاً باستخدام ()findViewById كالتالي:

	  public class MainActivity extends AppCompatActivity {
	  private List<String> myMedia = new ArrayList<>();
	  private List<String> myMediaPaths = new ArrayList<>();

	  private MediaPlayer myPlayer;
	  private int mplayer_status = -1;
	  private int selected_media = -1;
	  TextView currentlyPlaying_ref;
	
ونقوم بإضافة السطر التالي في دالّة ()onCreate حتّى نتمكَّن من الوصول برمجيّاً إلى عنصر الTextView:
	  currentlyPlaying_ref = findViewById(R.id.currentlyPlaying);
	  
	

وأخيراً، نقوم بإضافة السطر التالي في دالّة ()mylistview_ref.setOnItemClickListener، بحيث يتم تغيير النصّ في كلّ مرّة يقوم المستخدم باختيار ملفّ صوتي جديد:

	  myPlayer.setDataSource(myMediaPaths.get(position));
	  currentlyPlaying_ref.setText("Playing: " + myMedia.get(position));