基于Android實(shí)現(xiàn)一個(gè)簡(jiǎn)易音樂(lè)播放器
1、簡(jiǎn)介
一個(gè)簡(jiǎn)易的音樂(lè)APP,主要練習(xí)對(duì)四大組件的應(yīng)用。感興趣的可以看看。
播放界面如下:
歌曲列表界面如下:
項(xiàng)目結(jié)構(gòu)如下:
接下來(lái)將對(duì)代碼做詳細(xì)介紹:
2、Music: 音頻對(duì)象
public class Music { private String name;//歌曲的名稱 private String author;//歌曲的作者(歌手) private long time;//歌曲的時(shí)長(zhǎng) private String id;//歌曲的唯一Id private String url;//歌曲的地址 }
特殊說(shuō)明: 由于本APP沒(méi)有使用數(shù)據(jù)庫(kù)而是使用 List 去存儲(chǔ)對(duì)象信息,所以沒(méi)找到合適的屬性值去唯一代表一個(gè)音頻。此id
用的是 name+author
進(jìn)行字符串拼接而成。
這種做法很有可能會(huì)發(fā)生 id 碰撞。如有嚴(yán)格需求,請(qǐng)自行解決。
3、BaseActivity:
自定義Activity去繼承AppCompatActivity。此Class主要用來(lái)存放一些全局都要訪問(wèn)的東西。
public class BaseActivity extends AppCompatActivity { //用來(lái)存放音頻對(duì)象。 public static List<Music> musicList = null; //用來(lái)標(biāo)志 當(dāng)前播放的是第幾首歌, 值代表在 musicList 中的下標(biāo)。 public static int currentOrder = -1; //不多解釋,就看成一個(gè)解析音頻文件的工具即可 protected MediaMetadataRetriever retriever; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); retriever = new MediaMetadataRetriever(); } @SuppressLint("Range") protected void initMusicList() { //此處是有代碼的,后面再具體講解 } }
4、activity_main.xml:
主界面,這里主要是用了一個(gè)相對(duì)布局,沒(méi)什么好講的。
后面會(huì)把整個(gè)項(xiàng)目代碼放到資源里,免費(fèi)使用。
<?xml version="1.0" encoding="utf-8"?> <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"> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" tools:ignore="UselessParent"> <LinearLayout android:id="@+id/title" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="70sp" android:layout_alignParentTop="true" > <TextView android:layout_width="0dp" android:layout_weight="5" android:layout_height="match_parent" android:layout_marginStart="5sp" android:text="@string/app_name" android:textSize="30sp" android:textColor="#1295DA" android:gravity="center|start"/> <ImageButton android:id="@+id/btn_list" android:layout_width="0dp" android:layout_weight="1" android:layout_height="match_parent" android:background="@drawable/list" android:scaleType="fitCenter"/> </LinearLayout> <ImageButton android:id="@+id/music" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/music" android:layout_marginTop="70sp" android:layout_centerInParent="true" android:layout_below="@+id/title" android:scaleType="fitCenter"/> <LinearLayout android:id="@+id/music_message" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="70sp" android:layout_below="@+id/music" android:orientation="vertical"> <TextView android:id="@+id/tv_music_name" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginStart="10sp" android:textSize="29sp" android:textColor="#000000" android:text="@string/default_music"/> <TextView android:id="@+id/tv_music_author" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginStart="10sp" android:textSize="25sp" android:text="@string/default_author"/> </LinearLayout> <SeekBar android:id="@+id/seekBar" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="60sp" android:layout_below="@+id/music_message" /> <RelativeLayout android:layout_below="@+id/seekBar" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:id="@+id/tv_now_time" android:layout_marginStart="10sp" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/default_music_time"/> <TextView android:id="@+id/tv_all_time" android:layout_marginEnd="15sp" android:layout_alignParentEnd="true" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/default_music_time"/> </RelativeLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="80sp" android:layout_alignParentBottom="true" android:layout_marginBottom="20sp" android:orientation="horizontal"> <ImageButton android:id="@+id/btn_last" android:layout_width="0dp" android:layout_height="match_parent" android:layout_marginEnd="1sp" android:layout_weight="1" android:background="@color/white" android:scaleType="fitCenter" android:src="@drawable/last" /> <ImageButton android:id="@+id/btn_start" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:background="@color/white" android:scaleType="fitCenter" android:src="@drawable/start" /> <ImageButton android:id="@+id/btn_next" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:background="@color/white" android:scaleType="fitCenter" android:src="@drawable/next" /> </LinearLayout> </RelativeLayout> </LinearLayout>
5、MainActivity:
主Activity 。代碼很長(zhǎng),分模塊講解。
屬性:
protected static String CURRENT_ID = "-1"; //當(dāng)前正在播放的歌曲id protected static Music currentMusic; protected static boolean isBind = false; protected ImageButton btn_list, btn_last, btn_start, btn_next; protected SeekBar seekBar; protected TextView tv_music_name, tv_music_author, tv_all_time, tv_now_time; protected static int Flag = 0; //當(dāng)前的狀態(tài) 1:正在播放 0:暫停 protected MusicService.MusicBinder musicBinder; protected MusicServiceConnection musicServiceConnection; public static LocalBroadcastManager localBroadcastManager; private static final int REQ_READ_EXTERNAL_STORAGE = 1; private static Boolean IS_PERMISSION = false; //是否授予權(quán)限
5.1、onCreate():
protected void onCreate(Bundle savedInstanceState) { ... //省略一些屬性賦值。 //獲取權(quán)限 requestPermissionByHand(); //注冊(cè)廣播 registerBroadCast(); //綁定服務(wù) startAndBindService();//啟動(dòng)服務(wù) //進(jìn)度條 seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { } @Override public void onStartTrackingTouch(SeekBar seekBar) { if (currentMusic == null) { ToastUtil.toast(MainActivity.this, "未播放歌曲"); } } @Override public void onStopTrackingTouch(SeekBar seekBar) { int progress = seekBar.getProgress(); tv_now_time.setText(format(progress)); musicBinder.seekTo(progress); } }); oprSeekBar(false);//剛開(kāi)始不允許操作 }
5.1.1、requestPermissionByHand()
: 因?yàn)橐x取音頻文件,第一步肯定要先進(jìn)行授權(quán)。代碼就是很標(biāo)準(zhǔn)的權(quán)限獲取流程。
public void requestPermissionByHand() { //檢查有沒(méi)有這個(gè)權(quán)限 int checkWriteStoragePermission = ContextCompat.checkSelfPermission( MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE); //如果沒(méi)有被授予 if (checkWriteStoragePermission != PackageManager.PERMISSION_GRANTED) { //請(qǐng)求權(quán)限,此處可以同時(shí)申請(qǐng)多個(gè)權(quán)限 ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, REQ_READ_EXTERNAL_STORAGE); //這里會(huì)根據(jù)授權(quán)的結(jié)果,去調(diào)用onRequestPermissionsResult 相應(yīng)的操作。 } else { //如果已經(jīng)有權(quán)限了,把這個(gè)標(biāo)識(shí)設(shè)為 true,后面講為什么。 IS_PERMISSION = true; initMusicList(); } } @Override public void onRequestPermissionsResult(int requestCode, final String[] permissions, int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); switch (requestCode) { case REQ_READ_EXTERNAL_STORAGE: // 如果請(qǐng)求被取消了,那么結(jié)果數(shù)組就是空的 if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // 權(quán)限被授予了 initMusicList();//初始化數(shù)據(jù) IS_PERMISSION = true; } else { //拒絕了權(quán)限請(qǐng)求,彈出提示,然后退出程序。 ToastUtil.toast(MainActivity.this, "請(qǐng)前往設(shè)置授予權(quán)限"); } break; default: break; } }
==注意:==當(dāng)我們安裝完應(yīng)用后第一次啟動(dòng)時(shí)如果拒絕了權(quán)限請(qǐng)求。那么再次啟動(dòng)應(yīng)用時(shí),它會(huì)默認(rèn)為禁止此權(quán)限,且 ActivityCompat.requestPermissions()
將不會(huì)再?gòu)棾鰴?quán)限授予框進(jìn)行選擇。如果想獲取權(quán)限,只能手動(dòng)去手機(jī)應(yīng)用設(shè)置處授權(quán)。
IS_PERMISSION
: 這玩意是干啥用的?
主要是考慮到下列情景:
如果第一次授權(quán)被拒絕了,程序雖然自動(dòng)結(jié)束了,但我發(fā)現(xiàn)其實(shí)它仍在后臺(tái)進(jìn)行(才疏學(xué)淺,沒(méi)找到徹底殺死進(jìn)程的方法)。這個(gè)時(shí)候我們?nèi)ナ謩?dòng)授權(quán)結(jié)束后,再次打開(kāi)APP(),其實(shí)是執(zhí)行了 onStop()->onRestart()->onResume()這樣一個(gè)流程(activity的生命周期)。那我們這時(shí)應(yīng)該再去判斷一次,是否授權(quán)。如果缺少這次判斷,那么應(yīng)用將會(huì)一直退出。(雖然我們手動(dòng)授權(quán)了,但是app自己不知道,必須告訴它一聲)。
@Override protected void onRestart() { super.onRestart(); if (!IS_PERMISSION) {//當(dāng)從后臺(tái)進(jìn)入時(shí),判斷應(yīng)用是否已經(jīng)有權(quán)限了 ,沒(méi)有就去申請(qǐng) requestPermissionByHand(); } }
為什么不放在 onResume()
里面呢? 這個(gè)主要是會(huì)出現(xiàn)重復(fù)授權(quán)請(qǐng)求的情況(可以自己思考一下哈)。
仔細(xì)留意可以看到,我們?cè)谑跈?quán)完成后,其實(shí)是去執(zhí)行了 BaeActivity.initMusicList()
方法。
5.1.2 initMusicList()
: 初始化音頻數(shù)據(jù)
@SuppressLint("Range") protected void initMusicList() { musicList = new ArrayList<>(); ContentResolver contentResolver = getContentResolver(); //系統(tǒng)提供的內(nèi)容提供者,可以通過(guò)去去訪問(wèn)一些數(shù)據(jù)。 Cursor cursor = null; //讀取sd卡 //這一部分直接用就行 try { cursor = contentResolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, null); if (cursor != null) { while (cursor.moveToNext()) { //是否是音頻 int isMusic = cursor.getInt(cursor.getColumnIndex(MediaStore.Audio.Media.IS_MUSIC)); //時(shí)長(zhǎng) long duration = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media.DURATION)); //是音樂(lè)并且時(shí)長(zhǎng)大于1分鐘 if (isMusic != 0 && duration >= 60 * 1000) { //歌名 String musicName = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE)); //歌手 String musicAuthor = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)); //文件路徑 String musicPath = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA)); //歌名,歌手,時(shí)長(zhǎng),專輯,圖標(biāo),文件路徑,sequence number of list in listview Music music = new Music(musicName, musicAuthor, duration, musicName + musicAuthor, musicPath); musicList.add(music); } } } } catch (Exception e) { e.printStackTrace(); } finally { if (cursor != null) cursor.close();//用完要關(guān)閉 } //主要是這一部分 //這一部分是可有可無(wú),上面一部分是讀取的本地的音頻文件 //這一部分主要是 將兩個(gè)音頻文件塞進(jìn)了app內(nèi)部,進(jìn)行測(cè)試系統(tǒng)功能,可刪除 //在上面系統(tǒng)結(jié)構(gòu)圖中可以看到 ,我在 /res/raw 下放了兩首 MP3 // 由于沒(méi)找到具體去直接遍歷的操作,所以這里使用了暴力去解決,即把文件名設(shè)置成有規(guī)律的,如:m1,m2這樣。 // 如果有好方法可以提出來(lái)。 try { for (int i = 1; i <= 2; i++) { Uri uri = Uri.parse("android.resource://" + getPackageName() + "/raw/m" + i); retriever.setDataSource(this,uri); String musicName = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE); if(musicName == null) musicName = "music"+i; //歌手 String musicAuthor = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST); if(musicAuthor == null) musicAuthor = "網(wǎng)絡(luò)歌手"; //文件路徑 String musicPath = "android.resource://" + getPackageName() + "/raw/m" + i; //時(shí)長(zhǎng) String duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); //歌名,歌手,時(shí)長(zhǎng),專輯,圖標(biāo),文件路徑,sequence number of list in listview Music music = new Music(musicName, musicAuthor, Long.parseLong(duration), musicName + musicAuthor, musicPath); musicList.add(music); } }catch (Exception e){ e.printStackTrace(); }finally { if(retriever != null) retriever.release(); } }
到這里 requestPermissionByHand()
就結(jié)束了,就是 授權(quán)+讀文件
5.1.3、registerBroadCast();
注冊(cè)廣播: 這里采用的是 本地廣播 + 動(dòng)態(tài)注冊(cè)
private void registerBroadCast() { localBroadcastManager = LocalBroadcastManager.getInstance(this); MusicReceiver musicReceiver = new MusicReceiver(); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction("com.xhy.musicRunning"); localBroadcastManager.registerReceiver(musicReceiver, intentFilter); }
class MusicReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Bundle bundle = intent.getBundleExtra("bundle"); int currentPosition = bundle.getInt("currentPosition"); seekBar.setProgress(currentPosition); tv_now_time.setText(format(currentPosition)); if (format(currentPosition).equals(format(seekBar.getMax())) && Flag == 1) { handleEnd(); } } }
ok,先到這里,后面再講 MusicReceiver
的操作。
5.1.4、startAndBindService()
private void startAndBindService() { Intent intent = new Intent(MainActivity.this, MusicService.class); musicServiceConnection = new MusicServiceConnection(); startService(intent); bindService(intent, musicServiceConnection, BIND_AUTO_CREATE); }
class MusicServiceConnection implements ServiceConnection { @Override public void onServiceConnected(ComponentName name, IBinder service) { musicBinder = (MusicService.MusicBinder) service; isBind = true; } @Override public void onServiceDisconnected(ComponentName name) {} }
這就是很標(biāo)準(zhǔn)的服務(wù)綁定流程。
5.1.5、seekBar.setOnSeekBarChangeListener()
這種都比較好理解,不多講。
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { } @Override public void onStartTrackingTouch(SeekBar seekBar) { if (currentMusic == null) { ToastUtil.toast(MainActivity.this, "未播放歌曲"); } } //主要看這個(gè) //當(dāng)我們滑動(dòng)或者點(diǎn)擊進(jìn)度條時(shí),會(huì)跟隨改變歌曲的進(jìn)度。 @Override public void onStopTrackingTouch(SeekBar seekBar) { int progress = seekBar.getProgress(); // progress就是代表當(dāng)前進(jìn)度條的數(shù)據(jù) tv_now_time.setText(format(progress)); //修改展示的當(dāng)前時(shí)間(歌曲的進(jìn)度) musicBinder.seekTo(progress); } });
format()
: 將 ms 轉(zhuǎn)化成 mm:ss
的格式
private String format(long time) { int minute = 0; int second = 0; minute = (int) (time / (1000 * 60)) % 60; second = (int) (time / 1000) % 60; return String.format("%02d", minute) + ":" + String.format("%02d", second); }
5.1.6、oprSeekBar()
:
剛開(kāi)始,seekBar
處于不可點(diǎn)擊狀態(tài)。本應(yīng)用啟動(dòng)時(shí)是不會(huì)主動(dòng)播放歌曲的,也就是處于 暫無(wú)歌曲
狀態(tài)。seekBar
此時(shí)應(yīng)處于不可用狀態(tài)(因?yàn)橛斜O(jiān)聽(tīng)點(diǎn)擊事件,會(huì)導(dǎo)致一些錯(cuò)誤)。
private void oprSeekBar(Boolean clickable) { //禁止拖動(dòng) seekBar.setClickable(clickable); seekBar.setEnabled(clickable); seekBar.setFocusable(clickable); }
onCreate()
到這里就暫時(shí)先結(jié)束,我們要先去看服務(wù)。
6、MusicService
public class MusicService extends Service { //用來(lái)控制音樂(lè)的播放與暫停。系統(tǒng)自帶的 protected MediaPlayer mediaPlayer; //定時(shí)器 protected Timer timer; //廣播管理器 //用的是 MainActivity中的 public static LocalBroadcastManager localBroadcastManager; public MusicService() { } @Override public void onCreate() { super.onCreate(); mediaPlayer = new MediaPlayer(); localBroadcastManager = MainActivity.localBroadcastManager; } private void createTimer() { if (timer == null) { timer = new Timer(); TimerTask timerTask = new TimerTask() { //定時(shí)任務(wù) @Override public void run() { //還沒(méi)有播放器的時(shí)候,就直接退出。 if(mediaPlayer == null) return; //當(dāng)前進(jìn)度, mediaPlayer 自帶API,獲取當(dāng)前音頻播放到哪里了 int currentPosition = mediaPlayer.getCurrentPosition(); //攜帶數(shù)據(jù) Bundle bundle=new Bundle(); bundle.putInt("currentPosition",currentPosition); Intent intent = new Intent(); intent.setAction("com.xhy.musicRunning"); intent.setClassName("com.xhy.musicplayer","MainActivity&MusicReceiver"); intent.putExtra("bundle",bundle); //發(fā)送廣播 localBroadcastManager.sendBroadcast(intent); } }; timer.schedule(timerTask,1,1000); // 1ms后,每1000ms執(zhí)行 一次 TimerTask; //總結(jié)下來(lái)就是,只要有 mediaPlay的存在,就把當(dāng)前歌曲播放的具體時(shí)長(zhǎng) 以廣播的形式發(fā)送,由MainActivity進(jìn)行捕獲與響應(yīng) } } @Override public IBinder onBind(Intent intent) { return new MusicBinder(); } //用來(lái)綁定服務(wù),這樣可以通過(guò)Activity 與服務(wù)進(jìn)行交互了 public class MusicBinder extends Binder { public void play(String url){//String path Uri uri= Uri.parse(url); try{ //重置音樂(lè)播放器 mediaPlayer.reset(); //加載多媒體文件 mediaPlayer=MediaPlayer.create(getApplicationContext(),uri); mediaPlayer.start();//播放音樂(lè) createTimer();//添加計(jì)時(shí)器 }catch(Exception e){ e.printStackTrace(); } } //下面的暫停繼續(xù)和退出方法全部調(diào)用的是MediaPlayer自帶的方法 public void pausePlay(){ mediaPlayer.pause();//暫停播放音樂(lè) } public void continuePlay(){ mediaPlayer.start();//繼續(xù)播放音樂(lè) } public void seekTo(int progress){ mediaPlayer.seekTo(progress);//設(shè)置音樂(lè)的播放位置 } //播放下一首 public void nextPlay(){ //當(dāng)前的下標(biāo)加1, BaseActivity.currentOrder +=1; //確定下一首歌的坐標(biāo) if(BaseActivity.currentOrder == BaseActivity.musicList.size()) BaseActivity.currentOrder = 0; //獲取下一首歌的對(duì)象 Music nextMusic = BaseActivity.musicList.get(BaseActivity.currentOrder); //播放 play(nextMusic.getUrl()); } //播放上一首 public void lastPlay(){ BaseActivity.currentOrder -=1; if(BaseActivity.currentOrder == -1) BaseActivity.currentOrder = 0; Music lastMusic = BaseActivity.musicList.get(BaseActivity.currentOrder); play(lastMusic.getUrl()); } } @Override public void onDestroy() { //當(dāng)服務(wù)被銷毀就 銷毀 mediaPlayer,釋放資源 super.onDestroy(); if(mediaPlayer==null) return; if(mediaPlayer.isPlaying()) mediaPlayer.stop();//停止播放音樂(lè) mediaPlayer.release();//釋放占用的資源 mediaPlayer=null;//將player置為空 if(timer != null) timer = null; } }
ok,此時(shí)我們回去看一下,廣播接收器干了什么。
class MusicReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Bundle bundle = intent.getBundleExtra("bundle"); int currentPosition = bundle.getInt("currentPosition"); seekBar.setProgress(currentPosition);//調(diào)整進(jìn)度條 tv_now_time.setText(format(currentPosition)); //設(shè)置當(dāng)前的播放時(shí)間 if (format(currentPosition).equals(format(seekBar.getMax())) && Flag == 1) {//如果進(jìn)度條已經(jīng)到頭了 handleEnd(); } } }
private void handleEnd() { //歌曲放完了,相當(dāng)于觸發(fā)一次下一首 Flag = 0;//先暫停這一首,然后執(zhí)行下一首 btn_start.setImageResource(R.drawable.start); ToastUtil.toast(MainActivity.this, "即將播放下一首"); //延遲2.5s,播放下一首 new Handler().postDelayed(new Runnable() { @Override public void run() { btn_next.performClick(); Log.d("TestRecycler", "發(fā)送消息"); //如果此時(shí)是在歌曲列表界面,發(fā)個(gè)消息 if (MusicListActivity.musicHandler != null) { Message message = new Message(); message.what = MusicListActivity.UPDATE_TEXT; MusicListActivity.musicHandler.sendMessage(message); } } }, 2500); }
總結(jié)來(lái)說(shuō):MusicReceiver
就復(fù)雜監(jiān)聽(tīng)音樂(lè)的播放,動(dòng)態(tài)的去更新 界面上時(shí)間及進(jìn)度條的顯示。
if (format(currentPosition).equals(format(seekBar.getMax())) && Flag == 1) {//如果進(jìn)度條已經(jīng)到頭了 handleEnd(); }
==提示:==這里簡(jiǎn)單的提一下,為什么要判斷 format 之后的 字符串 而不是直接比較 currentPosition
和seekBar.getMax()
。
因?yàn)槲覀兘邮艿氖菑V播,且廣播一秒才發(fā)一次,再加上傳播產(chǎn)生的時(shí)間,在 ms 時(shí)間級(jí)內(nèi), currentPosition和seekBar.getMax()。大概不不會(huì)出現(xiàn)相等。所以這里比較的是格式化后的 s 級(jí)內(nèi)。
MusicService
就到這里
7、MusicListActivity
歌曲列表界面。這里采用的是 RecyclerView
布局去展示。
public class MusicListActivity extends BaseActivity { protected ImageButton btn_back; public static Handler musicHandler; public static final int UPDATE_TEXT = 1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_music_list); RecyclerView recyclerView = findViewById(R.id.recycle_view); LinearLayoutManager layoutManager = new LinearLayoutManager(this); recyclerView.setLayoutManager(layoutManager); MusicAdapter musicAdapter = new MusicAdapter(musicList, currentOrder == -1 ? "-1":musicList.get(currentOrder).getId()); musicAdapter.setOnItemClickListener(new OnItemClickListener() { //給我們的 item 設(shè)置點(diǎn)擊事件,代表選中這首歌 @Override public void onItemClick(View view, int position) { Music music = musicList.get(position); if (music != null) { Intent intent = new Intent(MusicListActivity.this, MainActivity.class); currentOrder = position; //更新選中的小標(biāo), startActivity(intent); // 回到 MainActivity , } } }); recyclerView.setAdapter(musicAdapter); musicHandler = new Handler(new Handler.Callback() { @Override public boolean handleMessage(@NonNull Message msg) { if (msg.what == UPDATE_TEXT){ //刷新 recycler musicAdapter.setCurrentId(musicList.get(currentOrder).getId()); recyclerView.setAdapter(null); recyclerView.setAdapter(musicAdapter); } return true; } }); btn_back = findViewById(R.id.btn_back); btn_back.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { finish(); } }); } }
這里主要有兩個(gè)部分需要注意。
1、
MusicAdapter musicAdapter = new MusicAdapter(musicList, currentOrder == -1 ? "-1":musicList.get(currentOrder).getId());
我們?cè)谶@里傳了當(dāng)前正在播放歌曲的 id 。因?yàn)槲覀円獙?duì)這個(gè)做特殊處理。MusicAdapter
做的大部分都是標(biāo)準(zhǔn)的流程化處理
public class MusicAdapter extends RecyclerView.Adapter<MusicAdapter.ViewHolder> { protected List<Music> myMusicList; protected OnItemClickListener myItemListener; public String currentId; private static final String CHOOSE_COLOR = "#7FE67F"; public void setCurrentId(String id){ currentId = id; } public MusicAdapter(List<Music> musicList, String currentId) { myMusicList = musicList; this.currentId = currentId; } public void setOnItemClickListener(OnItemClickListener listener){ this.myItemListener = listener; } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.music_list, parent, false); return new ViewHolder(view,myItemListener); } //在這里 @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { Log.d("TestRecycler","會(huì)執(zhí)行幾次呢"); Music music = myMusicList.get(position); holder.musicName.setText(music.getName()); holder.musicAuthor.setText(music.getAuthor()); //檢測(cè)是否是正在播放的歌曲 //對(duì)于正在播放的歌曲要加綠處理。 if(currentId.equalsIgnoreCase(music.getId())){ Log.d("TestRecycler","匹配成功--"+music.getName()); holder.chooseFlag.setText("正在播放"); holder.musicName.setTextColor(Color.parseColor(CHOOSE_COLOR)); holder.musicAuthor.setTextColor(Color.parseColor(CHOOSE_COLOR)); holder.point.setTextColor(Color.parseColor(CHOOSE_COLOR)); holder.chooseFlag.setTextColor(Color.parseColor(CHOOSE_COLOR)); } } @Override public int getItemCount() { return myMusicList.size(); } class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { TextView musicName; TextView musicAuthor; TextView point; TextView chooseFlag; public ViewHolder(View view, OnItemClickListener onItemClickListener) { super(view); myItemListener = onItemClickListener; view.setOnClickListener(this); musicName = view.findViewById(R.id.tv_list_name); musicAuthor = view.findViewById(R.id.tv_list_author); point = view.findViewById(R.id.point); chooseFlag = view.findViewById(R.id.tv_choose); } @Override public void onClick(View v) { myItemListener.onItemClick(v,getPosition()); } } }
2、
musicHandler = new Handler(new Handler.Callback() { @Override public boolean handleMessage(@NonNull Message msg) { if (msg.what == UPDATE_TEXT){ //刷新 recycler musicAdapter.setCurrentId(musicList.get(currentOrder).getId()); recyclerView.setAdapter(null); recyclerView.setAdapter(musicAdapter); } return true; } });
不知道還記不記得,前面有個(gè)地方發(fā)了一個(gè)消息。當(dāng)歌曲播放完成后,如果我們正處于 MusicListActivity
界面。會(huì)發(fā)送一條消息。然后 MusicListActivity
就會(huì)接受這條消息,然后刷新當(dāng)前頁(yè)面(主要就是為了更新 綠色的正在播放)。這里我先是用了notifyItemRangeChanged()
去測(cè)試,但是發(fā)現(xiàn)如果一直待在這個(gè)界面,有綠色狀態(tài)的會(huì)變的不唯一,也是DeBug很久,沒(méi)解決,就用了這種 重置適配器 的暴力方法(大數(shù)據(jù)時(shí)不可?。?。如果有別的方法,還請(qǐng)多多指教。
private void handleEnd() { //歌曲放完了,相當(dāng)于觸發(fā)一次下一首 Flag = 0;//先暫停這一首,然后執(zhí)行下一首 btn_start.setImageResource(R.drawable.start); ToastUtil.toast(MainActivity.this, "即將播放下一首"); //延遲2.5s,播放下一首 new Handler().postDelayed(new Runnable() { @Override public void run() { btn_next.performClick(); Log.d("TestRecycler", "發(fā)送消息"); //如果此時(shí)是在歌曲列表界面,發(fā)個(gè)消息 if (MusicListActivity.musicHandler != null) { Message message = new Message(); message.what = MusicListActivity.UPDATE_TEXT; MusicListActivity.musicHandler.sendMessage(message); } } }, 2500); }
這個(gè)Activity功能較少。讓我們繼續(xù)回到MainActivity
8、onResume()
@Override protected void onResume() { super.onResume(); Intent intent = getIntent(); //這個(gè)判斷是為了區(qū)別時(shí)初始化還是從 MusicListActivity 返回來(lái)的。 if (intent != null && currentOrder != -1) { //從歌曲列表返回來(lái)時(shí),更新正在播放的音頻對(duì)象 currentMusic = musicList.get(currentOrder);//這個(gè)更新不會(huì)影響到播放,因?yàn)椴シ攀?mediaPlayer 控制的 //如果我們點(diǎn)擊的是正在播放的歌曲,那么我們就不會(huì)進(jìn)行任何操作 //如果歌曲不一樣,就會(huì)進(jìn)行更新 if (currentMusic != null && !CURRENT_ID.equalsIgnoreCase(currentMusic.getId())) { initMusicMessage();//更新展示界面 btn_start.performClick(); //這個(gè)意思是 觸發(fā)一次 btn_start的點(diǎn)擊事件。后面再講,這里主要是理清是否需要切歌的邏輯。 } } }
private void initMusicMessage() { //更新展示界面 currentMusic = musicList.get(currentOrder); seekBar.setMax((int) currentMusic.getTime()); seekBar.setProgress(0); tv_music_name.setText(currentMusic.getName()); tv_music_author.setText(currentMusic.getAuthor()); tv_all_time.setText(format(currentMusic.getTime())); tv_now_time.setText(R.string.default_music_time); }
9、點(diǎn)擊事件處理
堅(jiān)持住,就要結(jié)束了!
btn_list
: 點(diǎn)擊后跳轉(zhuǎn)到 歌曲列表。
case R.id.btn_list: //展示歌曲列表 if (IS_PERMISSION) { Intent intent = new Intent(this, MusicListActivity.class); startActivity(intent); } else { ToastUtil.toast(MainActivity.this, "請(qǐng)先前往授權(quán)"); } break;
btn_start
: 情況最多的點(diǎn)擊
case R.id.btn_start: /* *三種情況會(huì)觸發(fā)。 * 1、剛進(jìn)入界面,還沒(méi)有選擇任何歌曲 * 2、歌曲播放中,點(diǎn)擊按鈕 * 3、選歌界面返回后,觸發(fā) */ //1、剛進(jìn)入界面,沒(méi)有選擇任何歌曲 if (currentOrder == -1) { startFirstMusic();//選中第一首歌進(jìn)行播放 break; } //如果二者不相等,說(shuō)明發(fā)生了切歌 //什么時(shí)候不相等?還記的 onResume() 觸發(fā)了一次點(diǎn)擊事件不,就在這里 if (!CURRENT_ID.equalsIgnoreCase(currentMusic.getId())) { //在歌曲列表選擇了不同的歌曲 if (Flag == 0) { //如果是暫停裝填,則修改一下圖標(biāo) btn_start.setImageResource(R.drawable.pause); } CURRENT_ID = currentMusic.getId(); initMusicMessage();//初始化歌曲信息 musicBinder.play(currentMusic.getUrl());//播放 } else { //相等就是單純的暫停與播放 if (Flag == 1) { //處于播放狀態(tài),點(diǎn)擊后暫停 btn_start.setImageResource(R.drawable.start); musicBinder.pausePlay(); } else { btn_start.setImageResource(R.drawable.pause); //這個(gè)地方要判斷下 是還沒(méi)有播放,還是繼續(xù)播放 // play()是會(huì)從頭開(kāi)始重新播放的,所以不能亂用 if (seekBar.getProgress() == 0) { musicBinder.play(currentMusic.getUrl()); } else { musicBinder.continuePlay(); } } Flag = Flag == 1 ? 0 : 1; } break;
btn_next
、btn_last
二者差不多
case R.id.btn_last: nextAndLast(false); break; case R.id.btn_next: nextAndLast(true); break;
private void nextAndLast(Boolean nextFlag) { if (currentOrder == -1) { //與開(kāi)始按鈕一樣,最開(kāi)始的時(shí)候,點(diǎn)擊三個(gè)中的任意一個(gè),都會(huì)選中第一首歌進(jìn)行播放 startFirstMusic(); return; } if (Flag == 0) { //如果此時(shí)處于暫停狀態(tài) Flag = 1; //更新?tīng)顟B(tài) btn_start.setImageResource(R.drawable.pause); // 更新下圖標(biāo) } if (nextFlag) { musicBinder.nextPlay(); //執(zhí)行下一首 } else { musicBinder.lastPlay(); //執(zhí)行上一首 } initMusicMessage(); //更新界面 CURRENT_ID = currentMusic.getId(); //跟新 CURRNET_ID 的值,供后續(xù)使用 }
還有最后一個(gè)函數(shù)
private void startFirstMusic() { if (!IS_PERMISSION) { //如果沒(méi)有授權(quán),點(diǎn)擊任何一個(gè)按鈕,都會(huì)彈出提示,然后什么也不干 ToastUtil.toast(MainActivity.this, "請(qǐng)先前往授權(quán)"); return; } if (BaseActivity.musicList.isEmpty()) { //授權(quán)了,但是沒(méi)有歌曲,也是彈出提示,然后啥也不干 ToastUtil.toast(MainActivity.this, "暫無(wú)曲目"); return; } //有歌曲就播放第一首 currentOrder = 0; currentMusic = musicList.get(currentOrder); CURRENT_ID = currentMusic.getId(); initMusicMessage(); btn_start.setImageResource(R.drawable.pause); Flag = 1; musicBinder.play(musicList.get(currentOrder).getUrl()); oprSeekBar(true)//設(shè)置我們的進(jìn)度條可以進(jìn)行點(diǎn)擊、滑動(dòng)。 }
以上就是基于Android實(shí)現(xiàn)一個(gè)簡(jiǎn)易音樂(lè)播放器的詳細(xì)內(nèi)容,更多關(guān)于Android音樂(lè)播放器的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android實(shí)現(xiàn)點(diǎn)擊AlertDialog上按鈕時(shí)不關(guān)閉對(duì)話框的方法
這篇文章主要介紹了Android實(shí)現(xiàn)點(diǎn)擊AlertDialog上按鈕時(shí)不關(guān)閉對(duì)話框的方法,涉及設(shè)置監(jiān)聽(tīng)的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-02-02Android動(dòng)態(tài)時(shí)鐘壁紙開(kāi)發(fā)
這篇文章主要為大家詳細(xì)介紹了Android動(dòng)態(tài)時(shí)鐘壁紙開(kāi)發(fā)的相關(guān)資料,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-01-01Android 8.1 Launcher3實(shí)現(xiàn)動(dòng)態(tài)指針時(shí)鐘功能
這篇文章主要介紹了Android 8.1 Launcher3實(shí)現(xiàn)動(dòng)態(tài)指針時(shí)鐘功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-07-07Android handle-message的發(fā)送與處理案例詳解
這篇文章主要介紹了Android handle-message的發(fā)送與處理案例詳解,本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08詳解Android中Intent傳遞對(duì)象給Activity的方法
這篇文章主要介紹了Android中Intent傳遞對(duì)象給Activity的方法,文章中對(duì)Activity的生命周期等知識(shí)先作了簡(jiǎn)要的介紹,需要的朋友可以參考下2016-04-04Matrix的set,pre,post調(diào)用順序詳解
下面小編就為大家?guī)?lái)一篇Matrix的set,pre,post調(diào)用順序詳解。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-04-04關(guān)于Android 4.4相機(jī)預(yù)覽、錄像花屏的問(wèn)題的解決方法
這篇文章主要介紹了關(guān)于Android 4.4相機(jī)預(yù)覽、錄像花屏的問(wèn)題的解決方法,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友參考下吧2016-12-12Android App中使用Gallery制作幻燈片播放效果
這篇文章主要介紹了Android App中使用Gallery制作幻燈片播放效果,相冊(cè)應(yīng)用中的輪播功能也與本文中例子的原理類似,需要的朋友可以參考下2016-04-04