基于Android實(shí)現(xiàn)一個(gè)簡易音樂播放器
1、簡介
一個(gè)簡易的音樂APP,主要練習(xí)對四大組件的應(yīng)用。感興趣的可以看看。
播放界面如下:

歌曲列表界面如下:

項(xiàng)目結(jié)構(gòu)如下:


接下來將對代碼做詳細(xì)介紹:
2、Music: 音頻對象
public class Music {
private String name;//歌曲的名稱
private String author;//歌曲的作者(歌手)
private long time;//歌曲的時(shí)長
private String id;//歌曲的唯一Id
private String url;//歌曲的地址
}
特殊說明: 由于本APP沒有使用數(shù)據(jù)庫而是使用 List 去存儲(chǔ)對象信息,所以沒找到合適的屬性值去唯一代表一個(gè)音頻。此id用的是 name+author進(jìn)行字符串拼接而成。
這種做法很有可能會(huì)發(fā)生 id 碰撞。如有嚴(yán)格需求,請自行解決。
3、BaseActivity:
自定義Activity去繼承AppCompatActivity。此Class主要用來存放一些全局都要訪問的東西。
public class BaseActivity extends AppCompatActivity {
//用來存放音頻對象。
public static List<Music> musicList = null;
//用來標(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è)相對布局,沒什么好講的。
后面會(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 。代碼很長,分模塊講解。
屬性:
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();
//注冊廣播
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);//剛開始不允許操作
}
5.1.1、requestPermissionByHand(): 因?yàn)橐x取音頻文件,第一步肯定要先進(jìn)行授權(quán)。代碼就是很標(biāo)準(zhǔn)的權(quán)限獲取流程。
public void requestPermissionByHand() {
//檢查有沒有這個(gè)權(quán)限
int checkWriteStoragePermission = ContextCompat.checkSelfPermission(
MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE);
//如果沒有被授予
if (checkWriteStoragePermission != PackageManager.PERMISSION_GRANTED) {
//請求權(quán)限,此處可以同時(shí)申請多個(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è)為 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:
// 如果請求被取消了,那么結(jié)果數(shù)組就是空的
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 權(quán)限被授予了
initMusicList();//初始化數(shù)據(jù)
IS_PERMISSION = true;
} else {
//拒絕了權(quán)限請求,彈出提示,然后退出程序。
ToastUtil.toast(MainActivity.this, "請前往設(shè)置授予權(quán)限");
}
break;
default:
break;
}
}
==注意:==當(dāng)我們安裝完應(yīng)用后第一次啟動(dòng)時(shí)如果拒絕了權(quán)限請求。那么再次啟動(dòng)應(yīng)用時(shí),它會(huì)默認(rèn)為禁止此權(quán)限,且 ActivityCompat.requestPermissions()將不會(huì)再彈出權(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í)它仍在后臺進(jìn)行(才疏學(xué)淺,沒找到徹底殺死進(jìn)程的方法)。這個(gè)時(shí)候我們?nèi)ナ謩?dòng)授權(quán)結(jié)束后,再次打開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)從后臺進(jìn)入時(shí),判斷應(yīng)用是否已經(jīng)有權(quán)限了 ,沒有就去申請
requestPermissionByHand();
}
}
為什么不放在 onResume()里面呢? 這個(gè)主要是會(huì)出現(xiàn)重復(fù)授權(quán)請求的情況(可以自己思考一下哈)。
仔細(xì)留意可以看到,我們在授權(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)容提供者,可以通過去去訪問一些數(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í)長
long duration = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media.DURATION));
//是音樂并且時(shí)長大于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í)長,專輯,圖標(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)閉
}
//主要是這一部分
//這一部分是可有可無,上面一部分是讀取的本地的音頻文件
//這一部分主要是 將兩個(gè)音頻文件塞進(jìn)了app內(nèi)部,進(jìn)行測試系統(tǒng)功能,可刪除
//在上面系統(tǒng)結(jié)構(gòu)圖中可以看到 ,我在 /res/raw 下放了兩首 MP3
// 由于沒找到具體去直接遍歷的操作,所以這里使用了暴力去解決,即把文件名設(shè)置成有規(guī)律的,如:m1,m2這樣。
// 如果有好方法可以提出來。
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í)長
String duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
//歌名,歌手,時(shí)長,專輯,圖標(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();
注冊廣播: 這里采用的是 本地廣播 + 動(dòng)態(tài)注冊
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():
剛開始,seekBar處于不可點(diǎn)擊狀態(tài)。本應(yīng)用啟動(dòng)時(shí)是不會(huì)主動(dòng)播放歌曲的,也就是處于 暫無歌曲狀態(tài)。seekBar此時(shí)應(yīng)處于不可用狀態(tài)(因?yàn)橛斜O(jiān)聽點(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 {
//用來控制音樂的播放與暫停。系統(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() {
//還沒有播放器的時(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é)下來就是,只要有 mediaPlay的存在,就把當(dāng)前歌曲播放的具體時(shí)長 以廣播的形式發(fā)送,由MainActivity進(jìn)行捕獲與響應(yīng)
}
}
@Override
public IBinder onBind(Intent intent) {
return new MusicBinder();
}
//用來綁定服務(wù),這樣可以通過Activity 與服務(wù)進(jìn)行交互了
public class MusicBinder extends Binder {
public void play(String url){//String path
Uri uri= Uri.parse(url);
try{
//重置音樂播放器
mediaPlayer.reset();
//加載多媒體文件
mediaPlayer=MediaPlayer.create(getApplicationContext(),uri);
mediaPlayer.start();//播放音樂
createTimer();//添加計(jì)時(shí)器
}catch(Exception e){
e.printStackTrace();
}
}
//下面的暫停繼續(xù)和退出方法全部調(diào)用的是MediaPlayer自帶的方法
public void pausePlay(){
mediaPlayer.pause();//暫停播放音樂
}
public void continuePlay(){
mediaPlayer.start();//繼續(xù)播放音樂
}
public void seekTo(int progress){
mediaPlayer.seekTo(progress);//設(shè)置音樂的播放位置
}
//播放下一首
public void nextPlay(){
//當(dāng)前的下標(biāo)加1,
BaseActivity.currentOrder +=1;
//確定下一首歌的坐標(biāo)
if(BaseActivity.currentOrder == BaseActivity.musicList.size()) BaseActivity.currentOrder = 0;
//獲取下一首歌的對象
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();//停止播放音樂
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é)來說:MusicReceiver 就復(fù)雜監(jiān)聽音樂的播放,動(dòng)態(tài)的去更新 界面上時(shí)間及進(jìn)度條的顯示。
if (format(currentPosition).equals(format(seekBar.getMax())) && Flag == 1) {//如果進(jìn)度條已經(jīng)到頭了
handleEnd();
}
==提示:==這里簡單的提一下,為什么要判斷 format 之后的 字符串 而不是直接比較 currentPosition和seekBar.getMax()。
因?yàn)槲覀兘邮艿氖菑V播,且廣播一秒才發(fā)一次,再加上傳播產(chǎn)生的時(shí)間,在 ms 時(shí)間級內(nèi), currentPosition和seekBar.getMax()。大概不不會(huì)出現(xiàn)相等。所以這里比較的是格式化后的 s 級內(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());
我們在這里傳了當(dāng)前正在播放歌曲的 id 。因?yàn)槲覀円獙@個(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());
//檢測是否是正在播放的歌曲
//對于正在播放的歌曲要加綠處理。
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)前頁面(主要就是為了更新 綠色的正在播放)。這里我先是用了notifyItemRangeChanged()去測試,但是發(fā)現(xiàn)如果一直待在這個(gè)界面,有綠色狀態(tài)的會(huì)變的不唯一,也是DeBug很久,沒解決,就用了這種 重置適配器 的暴力方法(大數(shù)據(jù)時(shí)不可?。H绻袆e的方法,還請多多指教。
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 返回來的。
if (intent != null && currentOrder != -1) {
//從歌曲列表返回來時(shí),更新正在播放的音頻對象
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, "請先前往授權(quán)");
}
break;
btn_start: 情況最多的點(diǎn)擊
case R.id.btn_start:
/*
*三種情況會(huì)觸發(fā)。
* 1、剛進(jìn)入界面,還沒有選擇任何歌曲
* 2、歌曲播放中,點(diǎn)擊按鈕
* 3、選歌界面返回后,觸發(fā)
*/
//1、剛進(jìn)入界面,沒有選擇任何歌曲
if (currentOrder == -1) {
startFirstMusic();//選中第一首歌進(jìn)行播放
break;
}
//如果二者不相等,說明發(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è)地方要判斷下 是還沒有播放,還是繼續(xù)播放
// play()是會(huì)從頭開始重新播放的,所以不能亂用
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) { //與開始按鈕一樣,最開始的時(shí)候,點(diǎn)擊三個(gè)中的任意一個(gè),都會(huì)選中第一首歌進(jìn)行播放
startFirstMusic();
return;
}
if (Flag == 0) { //如果此時(shí)處于暫停狀態(tài)
Flag = 1; //更新狀態(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) { //如果沒有授權(quán),點(diǎn)擊任何一個(gè)按鈕,都會(huì)彈出提示,然后什么也不干
ToastUtil.toast(MainActivity.this, "請先前往授權(quán)");
return;
}
if (BaseActivity.musicList.isEmpty()) { //授權(quán)了,但是沒有歌曲,也是彈出提示,然后啥也不干
ToastUtil.toast(MainActivity.this, "暫無曲目");
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è)簡易音樂播放器的詳細(xì)內(nèi)容,更多關(guān)于Android音樂播放器的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解Andorid開發(fā)中反射機(jī)制是怎么一回事
反射機(jī)制是在運(yùn)行狀態(tài)中,對于任何一個(gè)類,都可以知道這個(gè)類的所有屬性和方法,對于任何一個(gè)對象,都可以調(diào)用它所有的方法和屬性,修改部分類型信息,這種動(dòng)態(tài)獲取信息以及動(dòng)態(tài)調(diào)用對象方法的功能稱為Java的反射機(jī)制2022-11-11
Android.mk文件中添加第三方j(luò)ar文件的方法
這篇文章主要介紹了Android.mk文件中添加第三方j(luò)ar文件及引用第三方j(luò)ar包的方法,需要的朋友可以參考下2018-01-01
Android編程實(shí)現(xiàn)的EditText彈出打開和關(guān)閉工具類
這篇文章主要介紹了Android編程實(shí)現(xiàn)的EditText彈出打開和關(guān)閉工具類,涉及Android輸入框EditText彈出打開和關(guān)閉功能簡單實(shí)現(xiàn)技巧,需要的朋友可以參考下2018-02-02
Android獲取當(dāng)前位置的經(jīng)緯度數(shù)據(jù)
這篇文章主要介紹了Android獲取當(dāng)前位置的經(jīng)緯度數(shù)據(jù)的相關(guān)資料,需要的朋友可以參考下2016-02-02
淺談Android安全風(fēng)險(xiǎn)與防范措施
這篇文章主要介紹了淺談Android安全風(fēng)險(xiǎn)與防范措施,對安全感興趣的同學(xué)可以參考下2021-04-04
Android編程實(shí)現(xiàn)使用handler在子線程中更新UI示例
這篇文章主要介紹了Android編程實(shí)現(xiàn)使用handler在子線程中更新UI,涉及Android線程與界面布局相關(guān)操作技巧,需要的朋友可以參考下2017-09-09
Android開發(fā)之自帶下載器DownloadManager的使用示例代碼
本篇文章主要介紹了Android開發(fā)之自帶下載器DownloadManager的使用示例代碼,Android自帶的DownloadManager是一個(gè)很好的下載文件的工具,有興趣的可以了解一下。2017-03-03

