原文地址:http://blog.csdn.net/mc_hust/article/details/51534901
自從準(zhǔn)備畢業(yè)論文開(kāi)始,就沒(méi)寫過(guò)博客了,關(guān)注量也明顯呈下滑趨勢(shì)(雖然本來(lái)就少)。到現(xiàn)在已入職1個(gè)多月了,抽空把之前做的1個(gè)項(xiàng)目整理1下,算是畢業(yè)后的第1篇博客吧。
關(guān)于Mp3播放器,網(wǎng)上有各種實(shí)現(xiàn)方法,但是對(duì)歌詞的同步和滑動(dòng)更改播放進(jìn)度的講授卻少之又少,所以我這里重點(diǎn)放在歌詞的設(shè)計(jì)上(需要完全代碼的朋友,可以在評(píng)論中留下郵箱,我會(huì)盡快回復(fù)),關(guān)于Mp3的“播放\切歌\暫停”和“隨機(jī)\順序\單曲”播放等經(jīng)常使用功能應(yīng)當(dāng)還是比較好做的。下面看看效果:
- 主界面以下圖:
- 右滑以后進(jìn)入歌詞界面:
- 點(diǎn)擊右上角那個(gè)大設(shè)置按鈕:
全部項(xiàng)目主要觸及到以下知識(shí)點(diǎn):
- ViewPager
- Service與Activity通訊
- Broadcast
- ContentResolver
- PreferenceActivity
- MediaPlayer
以上幾個(gè)知識(shí)點(diǎn)大家應(yīng)當(dāng)比較熟習(xí),,4大組件全用上了,個(gè)人覺(jué)得這是個(gè)比較好的練手項(xiàng)目。下面從播放開(kāi)始看吧。
1、MP3播放器Service
作為播放器,固然是需要能夠支持后臺(tái)播放的,所以在啟動(dòng)播放之前,需要開(kāi)啟service。為了方便Activity與Service通訊,這里通過(guò)bindService方法開(kāi)啟Service,代碼以下:
bindService(new Intent(MainActivity.this, PlayService.class), connection, Context.BIND_AUTO_CREATE);
其中connection是Servive的1個(gè)回調(diào)方法,在里面獲得Mp3Binder:
private ServiceConnection connection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) {
PlayService.Mp3Binder binder = (PlayService.Mp3Binder) service;
player = new Mp3Player(binder.getService(), musicInfos);
} @Override public void onServiceDisconnected(ComponentName name) {
}
};
上面有個(gè)player,這個(gè)就是對(duì)播放器播放、暫停、切歌等操作的1個(gè)封裝類,下面來(lái)看看:
2、Mp3的播放、暫停、切歌
為了方便使用,將Mp3的播放操作封裝到Mp3Player類中,在里面我實(shí)現(xiàn)了Mp3的各種經(jīng)常使用操作,和循環(huán)、單曲、順序播放等經(jīng)常使用播放模式,通過(guò)此類與Service通訊,便可完成對(duì)MediaPlayer的操作。
MediaPlayer的使用應(yīng)當(dāng)還是很簡(jiǎn)單的,如果沒(méi)有做過(guò)MediaPlayer開(kāi)發(fā)的朋友,需要注意幾個(gè)問(wèn)題:
1. 在播放之前1定要先重置、準(zhǔn)備。調(diào)用的順序?yàn)椋簉eset、setDataSource、prepare、start。
2. 由于播放的歌曲通常是在SD卡上,記得要申明權(quán)限:
3. 由于觸及到搜索歌詞、和隨機(jī)播放的時(shí)候需要計(jì)算下1首歌,那末我們分別需要捕捉播放開(kāi)始和播放結(jié)束的信號(hào),可使用兩個(gè)監(jiān)聽(tīng)器完成,以下:
mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) {
sendBroadcast(new Intent(MainActivity.Mp3Receiver.ACTION_NEW));
}
});
mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) {
sendBroadcast(new Intent(MainActivity.Mp3Receiver.ACTION_END));
}
});
這里我通過(guò)廣播的方式將“開(kāi)始播放”和“結(jié)束播放”兩個(gè)信號(hào)傳遞出去。
4、獲得歌曲列表
說(shuō)了這么多,下面開(kāi)始搜歌吧。這里用到Android的ContentProvider,Android系統(tǒng)會(huì)搜索手機(jī)里所有的音頻文件,并放在MediaStore下面,我們要做的就是從這里面拿出想要的數(shù)據(jù)。通過(guò)
context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER)
可以拿到列表的cursor,然后在當(dāng)中去逐條獲得信息便可。把每個(gè)音頻文件視為1個(gè)對(duì)象,可以以下定義音頻對(duì)象:
class MusicInfo {
long id; String title; String artist; String duration;
int durationInSeconds;
long size; String data;
long albumId;
@Override public boolean equals(Object o) { data = data.replace("file://", ""); return data.equals(((MusicInfo) o).data);
}
}
這樣從Cursor中獲得數(shù)據(jù)以后填寫到上面MusicInfo中就能夠了,代碼示意以下:
private static ListgetMusicInfoList(Context context) {
Cursor cursor = context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER) Listlist = new ArrayList<>() int count = cursor.getCount() while (count-- > 0) {
cursor.moveToNext() if (0 == cursor.getInt(cursor.getColumnIndex(MediaStore.Audio.Media.IS_MUSIC))) {
continue }
MusicInfo info = new MusicInfo() info.id = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media._ID)) info.artist = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST)) long durationSeconds = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media.DURATION)) / 1000 info.durationInSeconds = (int) durationSeconds info.duration = durationSeconds % 60 < 10 ? durationSeconds / 60 + ":0" + durationSeconds % 60 : durationSeconds / 60 + ":" + durationSeconds % 60 info.size = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media.SIZE)) info.title = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.TITLE)) info.data = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.DATA)) info.albumId = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM_ID)) list.add(info) }
return list }
這樣拿到1個(gè)list然后設(shè)置到ListView中就能夠完成歌曲列表的顯示了。
5、搜索歌詞
搜索歌詞的原理其實(shí)就是在當(dāng)前歌曲目錄下去搜索同名的.lrc文件,然后從中讀入數(shù)據(jù)流進(jìn)行解析,歌詞的解析可以參考lrc歌詞的協(xié)議自行完成(需要完全代碼可以在下面留下您的郵箱)。
6、歌詞部份
接下來(lái)就是歌詞的同步與歌詞的滑動(dòng)了,網(wǎng)上對(duì)同步的實(shí)現(xiàn)大多是采取自定義1個(gè)TextView,然后再onDraw當(dāng)中去用Paint畫(huà)筆來(lái)畫(huà)出歌詞。這樣做對(duì)同步顯示來(lái)說(shuō)非常容易,但是如果想讓他在切換歌詞的時(shí)候平滑移動(dòng)和拖拽歌詞改變播放進(jìn)度這都是比較麻煩的。因此這里我采取ListView來(lái)做歌詞,這樣平滑移動(dòng)和滑動(dòng)監(jiān)聽(tīng)都比較方便。
由于需要將歌詞放在屏幕中央,所以需要提早計(jì)算出屏幕中央是ListView的第幾個(gè)Item,然后在前后順次留相應(yīng)數(shù)據(jù)的空白。例如第5個(gè)item在中間,則在設(shè)置歌詞數(shù)據(jù)的時(shí)候需要在前后分別留5個(gè)空白(示意代碼,不建議這么寫):
public void setLrcList(ListlrcList) { this.lrcList = lrcList; lrcList.add(new Lrc());
lrcList.add(new Lrc());
lrcList.add(new Lrc());
lrcList.add(new Lrc());
lrcList.add(new Lrc());
lrcList.add(new Lrc()); lrcList.add(0, new Lrc());
lrcList.add(0, new Lrc());
lrcList.add(0, new Lrc());
lrcList.add(0, new Lrc());
lrcList.add(0, new Lrc());
lrcList.add(0, new Lrc());}
6.1 同步平滑更新歌詞
通過(guò)update方法封裝更新功能:
/**
* 更新歌詞內(nèi)容
*
* @param position 當(dāng)前歌曲播放的時(shí)間
*/ public void update(int position) { if (!isTouching) {
adapter.notifyDataSetChanged();
isAutoScroll = true;
lvLrc.smoothScrollToPositionFromTop(adapter.update(position) - 4, 0, 1000); }
}
-
這里對(duì)ListView的滑動(dòng)沒(méi)有用到smoothScrollToPosition(int position);緣由是這個(gè)函數(shù)僅僅是保證position的那個(gè)item會(huì)顯示出來(lái),而我們想要的效果是讓他顯示到正中間,所以只能用smoothScrollToPositionFromTop,讓第前4句歌詞顯示在最頂端來(lái)實(shí)現(xiàn)效果。
-
adapter.update(position):這個(gè)方法的作用是獲得歌曲播放到position時(shí)間的時(shí)候是第幾句歌詞,從而讓他顯示在中間,代碼以下:
public int update(int position) { for (int i = 0; i < lrcList.size() - 1; i++) { if (position >= lrcList.get(i).getLrcTime() && position < lrcList.get(i + 1).getLrcTime() || position < lrcList.get(0).getLrcTime()) {
index = i; break;
} else if (position > lrcList.get(lrcList.size() - 1).getLrcTime()) {
index = lrcList.size() - 1;
}
} return index;
}
這類似1個(gè)順序查找算法,固然朋友們可以采取2分查找等其他算法提高效力。
這里實(shí)現(xiàn)的界面是1個(gè)ViewPager,第1頁(yè)是歌曲列表,右滑到第2頁(yè)是歌詞。效果見(jiàn)上圖
6.2 拖拽歌詞改變播放進(jìn)度
這部份主要是對(duì)歌詞布局,即ListView的觸摸監(jiān)聽(tīng)操作,采取listView.setOnTouchListener來(lái)實(shí)現(xiàn),先來(lái)看看這部份代碼:
lvLrc.setOnTouchListener(new View.OnTouchListener() {
@Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN:
isTouching = true; break; case MotionEvent.ACTION_UP: int time = lrcList.get(lvLrc.getFirstVisiblePosition() + 5).getLrcTime();
((MainActivity) activity).resume(time / 1000);
isTouching = false; break; case MotionEvent.ACTION_CANCEL:
isTouching = false; break;
} return false;
}
});
主要是在ACTION_UP的時(shí)候進(jìn)行操作,計(jì)算出當(dāng)前播放的歌詞的時(shí)間字段,然后通過(guò)service控制播放進(jìn)度(resume中封裝了對(duì)service的操作)。可以看到,在ACTION_DOWN和ACTION_CANCEL中也做了操作,主要是設(shè)置isTouching的值。這是為了避免在我們正在拖拽歌詞的進(jìn)程中,由于歌詞同步作用致使當(dāng)前歌詞改變從而使歌詞的ListView自動(dòng)滑動(dòng)。為了避免這個(gè)矛盾的出現(xiàn),在歌詞同步函數(shù)(update)中需要先檢查isTouch的值,然后決定是不是要進(jìn)行自動(dòng)同步(代碼見(jiàn)6.1)。
7、設(shè)置界面PreferenceActivity
設(shè)置界面幾近是所有的App都要用到的,PreferenceActivity就是專門為設(shè)置界面打造的,而Android原生代碼中幾近所有的設(shè)置界面也都是通過(guò)這個(gè)完成的。PreferenceActivity的使用方法網(wǎng)上有很多,他的使用與1般的布局類似,主要有以下幾種類型:
* ListPreference 列表項(xiàng)菜單
* EditTextPreference 編輯框菜單
* SwitchPreference 開(kāi)關(guān)菜單
本項(xiàng)目中就使用了以上幾種菜單項(xiàng),其余的也大同小異。我們可以對(duì)菜單項(xiàng)按功能進(jìn)行分組,每組是1個(gè)PreferenceCategory,而所有的PreferenceCategory都屬于1個(gè)PreferenceScreen,這樣的層級(jí)關(guān)系非常明確,具體的菜單布局代碼以下:
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" android:title="設(shè)置"> <PreferenceCategory android:title="播放模式"> <ListPreference android:defaultValue="單曲循環(huán)" android:entries="@array/play_mode" android:entryValues="@array/play_mode_value" android:key="@string/key_play_mode" android:title="選擇播放模式" /> PreferenceCategory> <PreferenceCategory android:title="歌詞設(shè)置"> <ListPreference android:entries="@array/lrc_color" android:entryValues="@array/lrc_color_value" android:key="@string/key_lrc_color" android:title="歌詞色彩" /> <ListPreference android:entries="@array/lrc_size" android:entryValues="@array/lrc_size_value" android:key="@string/key_lrc_size" android:title="歌詞大小" /> PreferenceCategory> <PreferenceCategory android:title="定時(shí)關(guān)機(jī)"> <EditTextPreference android:summary="將在設(shè)置的分鐘數(shù)后關(guān)機(jī)" android:title="請(qǐng)輸入關(guān)機(jī)時(shí)間" /> PreferenceCategory> <PreferenceCategory android:title="搖1搖切歌"> <SwitchPreference android:title="開(kāi)啟搖晃切歌" /> PreferenceCategory>
Activity的代碼也非常簡(jiǎn)單:
package com.example.machao10.mp3; import android.preference.EditTextPreference; import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceActivity; import android.preference.SwitchPreference; import android.support.v7.app.AppCompatActivity; import android.os.Bundle;public class SettingsActivity extends PreferenceActivity { ListPreference listPlayMode, listLrcSize, listLrcColor, listRing, listNotification, listSms;
EditTextPreference etAutoShutdown;
SwitchPreference switchShake; private void initPreference() {
listPlayMode = (ListPreference) findPreference(getString(R.string.key_play_mode));
SettingsChangeListener listener = new SettingsChangeListener();
listPlayMode.setOnPreferenceChangeListener(listener);
} @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.settings);
initPreference();
}
class SettingsChangeListener implements Preference.OnPreferenceChangeListener { @Override public boolean onPreferenceChange(Preference preference, Object newValue) {
String key = preference.getKey(); return true;
}
}
}
固然,以上只是對(duì)設(shè)值界面進(jìn)行了顯示,還需要完成相應(yīng)的邏輯和用戶設(shè)置的持久化,這個(gè)大家可以參考PreferenceActivity的具體用法,這里我就不展開(kāi)講了,需要完全開(kāi)起源碼的,可以在下面留下郵箱,我會(huì)及時(shí)給您回復(fù)的。
好了,mp3播放器就講到這里,主要是從邏輯結(jié)構(gòu)上做的梳理,然后針對(duì)部份細(xì)節(jié)進(jìn)行展開(kāi),并未將完全的代碼做1個(gè)串接,主要還是斟酌到關(guān)于Mp3的功能網(wǎng)上有很多資料,只是在歌詞那1塊應(yīng)當(dāng)還是很空白的。也希望我的這個(gè)歌詞方案能夠給大家?guī)?lái)1些方便,同時(shí)大家有甚么好的建議歡迎討論~
——超低空