原文地址:http://blog.csdn.net/mc_hust/article/details/51534901
自從準備畢業(yè)論文開始,就沒寫過博客了,關注量也明顯呈下滑趨勢(雖然本來就少)。到現(xiàn)在已入職1個多月了,抽空把之前做的1個項目整理1下,算是畢業(yè)后的第1篇博客吧。
關于Mp3播放器,網(wǎng)上有各種實現(xiàn)方法,但是對歌詞的同步和滑動更改播放進度的講授卻少之又少,所以我這里重點放在歌詞的設計上(需要完全代碼的朋友,可以在評論中留下郵箱,我會盡快回復),關于Mp3的“播放\切歌\暫停”和“隨機\順序\單曲”播放等經(jīng)常使用功能應當還是比較好做的。下面看看效果:
- 主界面以下圖:
- 右滑以后進入歌詞界面:
- 點擊右上角那個大設置按鈕:
全部項目主要觸及到以下知識點:
- ViewPager
- Service與Activity通訊
- Broadcast
- ContentResolver
- PreferenceActivity
- MediaPlayer
以上幾個知識點大家應當比較熟習,,4大組件全用上了,個人覺得這是個比較好的練手項目。下面從播放開始看吧。
1、MP3播放器Service
作為播放器,固然是需要能夠支持后臺播放的,所以在啟動播放之前,需要開啟service。為了方便Activity與Service通訊,這里通過bindService方法開啟Service,代碼以下:
bindService(new Intent(MainActivity.this, PlayService.class), connection, Context.BIND_AUTO_CREATE);
其中connection是Servive的1個回調(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,這個就是對播放器播放、暫停、切歌等操作的1個封裝類,下面來看看:
2、Mp3的播放、暫停、切歌
為了方便使用,將Mp3的播放操作封裝到Mp3Player類中,在里面我實現(xiàn)了Mp3的各種經(jīng)常使用操作,和循環(huán)、單曲、順序播放等經(jīng)常使用播放模式,通過此類與Service通訊,便可完成對MediaPlayer的操作。
MediaPlayer的使用應當還是很簡單的,如果沒有做過MediaPlayer開發(fā)的朋友,需要注意幾個問題:
1. 在播放之前1定要先重置、準備。調(diào)用的順序為:reset、setDataSource、prepare、start。
2. 由于播放的歌曲通常是在SD卡上,記得要申明權限:
3. 由于觸及到搜索歌詞、和隨機播放的時候需要計算下1首歌,那末我們分別需要捕捉播放開始和播放結束的信號,可使用兩個監(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));
}
});
這里我通過廣播的方式將“開始播放”和“結束播放”兩個信號傳遞出去。
4、獲得歌曲列表
說了這么多,下面開始搜歌吧。這里用到Android的ContentProvider,Android系統(tǒng)會搜索手機里所有的音頻文件,并放在MediaStore下面,我們要做的就是從這里面拿出想要的數(shù)據(jù)。通過
context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER)
可以拿到列表的cursor,然后在當中去逐條獲得信息便可。把每個音頻文件視為1個對象,可以以下定義音頻對象:
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個list然后設置到ListView中就能夠完成歌曲列表的顯示了。
5、搜索歌詞
搜索歌詞的原理其實就是在當前歌曲目錄下去搜索同名的.lrc文件,然后從中讀入數(shù)據(jù)流進行解析,歌詞的解析可以參考lrc歌詞的協(xié)議自行完成(需要完全代碼可以在下面留下您的郵箱)。
6、歌詞部份
接下來就是歌詞的同步與歌詞的滑動了,網(wǎng)上對同步的實現(xiàn)大多是采取自定義1個TextView,然后再onDraw當中去用Paint畫筆來畫出歌詞。這樣做對同步顯示來說非常容易,但是如果想讓他在切換歌詞的時候平滑移動和拖拽歌詞改變播放進度這都是比較麻煩的。因此這里我采取ListView來做歌詞,這樣平滑移動和滑動監(jiān)聽都比較方便。
由于需要將歌詞放在屏幕中央,所以需要提早計算出屏幕中央是ListView的第幾個Item,然后在前后順次留相應數(shù)據(jù)的空白。例如第5個item在中間,則在設置歌詞數(shù)據(jù)的時候需要在前后分別留5個空白(示意代碼,不建議這么寫):
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 同步平滑更新歌詞
通過update方法封裝更新功能:
/**
* 更新歌詞內(nèi)容
*
* @param position 當前歌曲播放的時間
*/ public void update(int position) { if (!isTouching) {
adapter.notifyDataSetChanged();
isAutoScroll = true;
lvLrc.smoothScrollToPositionFromTop(adapter.update(position) - 4, 0, 1000); }
}
-
這里對ListView的滑動沒有用到smoothScrollToPosition(int position);緣由是這個函數(shù)僅僅是保證position的那個item會顯示出來,而我們想要的效果是讓他顯示到正中間,所以只能用smoothScrollToPositionFromTop,讓第前4句歌詞顯示在最頂端來實現(xiàn)效果。
-
adapter.update(position):這個方法的作用是獲得歌曲播放到position時間的時候是第幾句歌詞,從而讓他顯示在中間,代碼以下:
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個順序查找算法,固然朋友們可以采取2分查找等其他算法提高效力。
這里實現(xiàn)的界面是1個ViewPager,第1頁是歌曲列表,右滑到第2頁是歌詞。效果見上圖
6.2 拖拽歌詞改變播放進度
這部份主要是對歌詞布局,即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的時候進行操作,計算出當前播放的歌詞的時間字段,然后通過service控制播放進度(resume中封裝了對service的操作)。可以看到,在ACTION_DOWN和ACTION_CANCEL中也做了操作,主要是設置isTouching的值。這是為了避免在我們正在拖拽歌詞的進程中,由于歌詞同步作用致使當前歌詞改變從而使歌詞的ListView自動滑動。為了避免這個矛盾的出現(xiàn),在歌詞同步函數(shù)(update)中需要先檢查isTouch的值,然后決定是不是要進行自動同步(代碼見6.1)。
7、設置界面PreferenceActivity
設置界面幾近是所有的App都要用到的,PreferenceActivity就是專門為設置界面打造的,而Android原生代碼中幾近所有的設置界面也都是通過這個完成的。PreferenceActivity的使用方法網(wǎng)上有很多,他的使用與1般的布局類似,主要有以下幾種類型:
* ListPreference 列表項菜單
* EditTextPreference 編輯框菜單
* SwitchPreference 開關菜單
本項目中就使用了以上幾種菜單項,其余的也大同小異。我們可以對菜單項按功能進行分組,每組是1個PreferenceCategory,而所有的PreferenceCategory都屬于1個PreferenceScreen,這樣的層級關系非常明確,具體的菜單布局代碼以下:
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" android:title="設置"> <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="歌詞設置"> <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="定時關機"> <EditTextPreference android:summary="將在設置的分鐘數(shù)后關機" android:title="請輸入關機時間" /> PreferenceCategory> <PreferenceCategory android:title="搖1搖切歌"> <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;
}
}
}
固然,以上只是對設值界面進行了顯示,還需要完成相應的邏輯和用戶設置的持久化,這個大家可以參考PreferenceActivity的具體用法,這里我就不展開講了,需要完全開起源碼的,可以在下面留下郵箱,我會及時給您回復的。
好了,mp3播放器就講到這里,主要是從邏輯結構上做的梳理,然后針對部份細節(jié)進行展開,并未將完全的代碼做1個串接,主要還是斟酌到關于Mp3的功能網(wǎng)上有很多資料,只是在歌詞那1塊應當還是很空白的。也希望我的這個歌詞方案能夠給大家?guī)?些方便,同時大家有甚么好的建議歡迎討論~
——超低空