原文地址:http://www.reibang.com/p/5dc92e06b7f8
自從準(zhǔn)備畢業(yè)論文開始,就沒寫過博客了缩焦,關(guān)注量也明顯呈下滑趨勢(雖然本來就少)欲芹。到現(xiàn)在已經(jīng)入職一個多月了,抽空把之前做的一個項目整理一下嘱支,算是畢業(yè)后的第一篇博客吧。
關(guān)于Mp3播放器挣饥,網(wǎng)上有各種實現(xiàn)方法除师,但是對于歌詞的同步以及滑動更改播放進(jìn)度的講解卻少之又少,所以我這里重點放在歌詞的設(shè)計上(需要完整代碼的朋友扔枫,可以在評論中留下郵箱汛聚,我會盡快回復(fù)),關(guān)于Mp3的“播放\切歌\暫投碳觯”以及“隨機(jī)\順序\單曲”播放等常用功能應(yīng)該還是比較好做的倚舀。下面看看效果:
-
主界面如下圖:
-
右滑之后進(jìn)入歌詞界面:
-
點擊右上角那個大設(shè)置按鈕:
整個項目主要涉及到以下知識點:
- ViewPager
- Service與Activity通信
- Broadcast
- ContentResolver
- PreferenceActivity
- MediaPlayer
以上幾個知識點大家應(yīng)該比較熟悉,忍宋,四大組件全用上了痕貌,個人覺得這是個比較好的練手項目。下面從播放開始看吧糠排。
1舵稠、MP3播放器Service
作為播放器,固然是需要能夠支持后臺播放的入宦,所以在啟動播放之前哺徊,需要開啟service。為了方便Activity與Service通信云石,這里通過bindService方法開啟Service唉工,代碼如下:
bindService(new Intent(MainActivity.this, PlayService.class), connection, Context.BIND_AUTO_CREATE);
其中connection是Servive的一個回調(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) {
}
};
上面有個player汹忠,這個就是對播放器播放淋硝、暫停、切歌等操作的一個封裝類宽菜,下面來看看:
2谣膳、Mp3的播放、暫停铅乡、切歌
為了方便使用继谚,將Mp3的播放操作封裝到Mp3Player類中,在里面我實現(xiàn)了Mp3的各種常用操作阵幸,以及循環(huán)花履、單曲芽世、順序播放等常用播放模式,通過此類與Service通信诡壁,即可完成對MediaPlayer的操作济瓢。
3、MediaPlayer的使用
MediaPlayer的使用應(yīng)該還是很簡單的妹卿,如果沒有做過MediaPlayer開發(fā)的朋友旺矾,需要注意幾個問題:
- 在播放之前一定要先重置、準(zhǔn)備夺克。調(diào)用的順序為:reset箕宙、setDataSource、prepare铺纽、start柬帕。
- 由于播放的歌曲通常是在SD卡上,記得要申明權(quán)限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
- 因為涉及到搜索歌詞室囊、以及隨機(jī)播放的時候需要計算下一首歌雕崩,那么我們分別需要捕捉播放開始和播放結(jié)束的信號魁索,可以使用兩個監(jiān)聽器完成融撞,如下:
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));
}
});
這里我通過廣播的方式將“開始播放”和“結(jié)束播放”兩個信號傳遞出去。
4粗蔚、獲取歌曲列表
說了這么多尝偎,下面開始搜歌吧。這里用到Android的ContentProvider鹏控,Android系統(tǒng)會搜索手機(jī)里所有的音頻文件致扯,并放在MediaStore下面,我們要做的就是從這里面拿出想要的數(shù)據(jù)当辐。通過
context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
可以拿到列表的cursor抖僵,然后在當(dāng)中去逐條獲取信息即可。把每一個音頻文件視為一個對象缘揪,可以如下定義音頻對象:
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 List<MusicInfo> getMusicInfoList(Context context) {
Cursor cursor = context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
List<MusicInfo> list = 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;
}
這樣拿到一個list然后設(shè)置到ListView中就可以完成歌曲列表的顯示了。
5找筝、搜索歌詞
搜索歌詞的原理其實就是在當(dāng)前歌曲目錄下去搜索同名的.lrc文件蹈垢,然后從中讀入數(shù)據(jù)流進(jìn)行解析,歌詞的解析可以參考lrc歌詞的協(xié)議自行完成(需要完整代碼可以在下面留下您的郵箱)袖裕。
6曹抬、歌詞部分
接下來就是歌詞的同步與歌詞的滑動了,網(wǎng)上對于同步的實現(xiàn)大多是采用自定義一個TextView急鳄,然后再onDraw當(dāng)中去用Paint畫筆來畫出歌詞谤民。這樣做對于同步顯示來講非常容易堰酿,但是如果想讓他在切換歌詞的時候平滑移動以及拖拽歌詞改變播放進(jìn)度這都是比較麻煩的。因此這里我采用ListView來做歌詞张足,這樣平滑移動和滑動監(jiān)聽都比較方便胞锰。
由于需要將歌詞放在屏幕中央,所以需要提前計算出屏幕中央是ListView的第幾個Item兢榨,然后在前后依次留相應(yīng)數(shù)據(jù)的空白嗅榕。例如第五個item在中間,則在設(shè)置歌詞數(shù)據(jù)的時候需要在前后分別留5個空白(示意代碼吵聪,不建議這么寫):
public void setLrcList(List<Lrc> lrcList) {
//設(shè)置歌詞內(nèi)容
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 同步平滑更新歌詞
通過update方法封裝更新功能:
/**
* 更新歌詞內(nèi)容
*
* @param position 當(dāng)前歌曲播放的時間
*/
public void update(int position) {
if (!isTouching) {
adapter.notifyDataSetChanged();
isAutoScroll = true;
lvLrc.smoothScrollToPositionFromTop(adapter.update(position) - 4, 0, 1000); //減4是保證當(dāng)前這句歌詞能顯示在正中間
}
}
- 這里對ListView的滑動沒有用到smoothScrollToPosition(int position);原因是這個函數(shù)僅僅是保證position的那個item會顯示出來凌那,而我們想要的效果是讓他顯示到正中間,所以只能用smoothScrollToPositionFromTop吟逝,讓第前四句歌詞顯示在最頂端來實現(xiàn)效果帽蝶。
- adapter.update(position):這個方法的作用是獲取歌曲播放到position時間的時候是第幾句歌詞,從而讓他顯示在中間块攒,代碼如下:
public int update(int position) {
for (int i = 0; i < lrcList.size() - 1; i++) {
//判斷當(dāng)前播放時間是否在歌詞的第一句和最后一句歌詞時間內(nè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;
}
這類似一個順序查找算法,當(dāng)然朋友們可以采用二分查找等其他算法提高效率囱井。
這里實現(xiàn)的界面是一個ViewPager驹尼,第一頁是歌曲列表,右滑到第二頁是歌詞庞呕。效果見上圖
6.2 拖拽歌詞改變播放進(jìn)度
這部分主要是對歌詞布局新翎,即ListView的觸摸監(jiān)聽操作,采用listView.setOnTouchListener來實現(xiàn)住练,先來看看這部分代碼:
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的時候進(jìn)行操作地啰,計算出當(dāng)前播放的歌詞的時間字段,然后通過service控制播放進(jìn)度(resume中封裝了對service的操作)讲逛】髁撸可以看到,在ACTION_DOWN和ACTION_CANCEL中也做了操作盏混,主要是設(shè)置isTouching的值蔚鸥。這是為了防止在我們正在拖拽歌詞的過程中,由于歌詞同步作用導(dǎo)致當(dāng)前歌詞改變從而使歌詞的ListView自動滑動括饶。為了防止這個矛盾的出現(xiàn)株茶,在歌詞同步函數(shù)(update)中需要先檢查isTouch的值,然后決定是否要進(jìn)行自動同步(代碼見6.1)图焰。
7启盛、設(shè)置界面PreferenceActivity
設(shè)置界面幾乎是所有的App都要用到的,PreferenceActivity就是專門為設(shè)置界面打造的,而Android原生代碼中幾乎所有的設(shè)置界面也都是通過這個完成的僵闯。PreferenceActivity的使用方法網(wǎng)上有很多卧抗,他的使用與一般的布局類似,主要有以下幾種類型:
- ListPreference 列表項菜單
- EditTextPreference 編輯框菜單
- SwitchPreference 開關(guān)菜單
本項目中就使用了以上幾種菜單項鳖粟,其余的也大同小異社裆。我們可以對菜單項按功能進(jìn)行分組,每一組是一個PreferenceCategory向图,而所有的PreferenceCategory都屬于一個PreferenceScreen泳秀,這樣的層級關(guān)系非常明確,具體的菜單布局代碼如下:
<?xml version="1.0" encoding="utf-8"?>
<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="定時關(guān)機(jī)">
<EditTextPreference
android:summary="將在設(shè)置的分鐘數(shù)后關(guān)機(jī)"
android:title="請輸入關(guān)機(jī)時間" />
</PreferenceCategory>
<PreferenceCategory android:title="搖一搖切歌">
<SwitchPreference android:title="開啟搖晃切歌" />
</PreferenceCategory>
Activity的代碼也非常簡單:
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;
}
}
}
當(dāng)然榄攀,以上只是對設(shè)值界面進(jìn)行了顯示嗜傅,還需要完成相應(yīng)的邏輯和用戶設(shè)置的持久化,這個大家可以參考PreferenceActivity的具體用法檩赢,這里我就不展開講了吕嘀,需要完整開發(fā)源碼的,可以在下面留下郵箱贞瞒,我會及時給您回復(fù)的偶房。
好了,mp3播放器就講到這里军浆,主要是從邏輯結(jié)構(gòu)上做的梳理棕洋,然后針對部分細(xì)節(jié)進(jìn)行展開,并沒有將完整的代碼做一個串接瘾敢,主要還是考慮到關(guān)于Mp3的功能網(wǎng)上有很多資料拍冠,只是在歌詞那一塊應(yīng)該還是很空白的尿这。也希望我的這個歌詞方案能夠給大家?guī)硪恍┓奖愦氐郑瑫r大家有什么好的建議歡迎討論~
——超低空