MP3歌詞的同步與拖拽設(shè)計

原文地址: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)該還是比較好做的倚舀。下面看看效果:

  • 主界面如下圖:


    圖1 - 主界面.jpg
  • 右滑之后進(jìn)入歌詞界面:


    圖2 - 右滑進(jìn)入歌詞界面.jpg
  • 點擊右上角那個大設(shè)置按鈕:


    圖3 - 設(shè)置界面.jpg

整個項目主要涉及到以下知識點:

  • 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ā)的朋友旺矾,需要注意幾個問題:

  1. 在播放之前一定要先重置、準(zhǔn)備夺克。調(diào)用的順序為:reset箕宙、setDataSource、prepare铺纽、start柬帕。
  2. 由于播放的歌曲通常是在SD卡上,記得要申明權(quán)限:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

  1. 因為涉及到搜索歌詞室囊、以及隨機(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大家有什么好的建議歡迎討論~

                                                                      ——超低空
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市射众,隨后出現(xiàn)的幾起案子碟摆,更是在濱河造成了極大的恐慌,老刑警劉巖叨橱,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件典蜕,死亡現(xiàn)場離奇詭異,居然都是意外死亡罗洗,警方通過查閱死者的電腦和手機(jī)愉舔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來伙菜,“玉大人轩缤,你說我怎么就攤上這事。” “怎么了火的?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵壶愤,是天一觀的道長。 經(jīng)常有香客問我馏鹤,道長征椒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任湃累,我火速辦了婚禮勃救,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘治力。我一直安慰自己剪芥,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布琴许。 她就那樣靜靜地躺著税肪,像睡著了一般。 火紅的嫁衣襯著肌膚如雪榜田。 梳的紋絲不亂的頭發(fā)上益兄,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天,我揣著相機(jī)與錄音箭券,去河邊找鬼净捅。 笑死,一個胖子當(dāng)著我的面吹牛辩块,可吹牛的內(nèi)容都是我干的蛔六。 我是一名探鬼主播,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼废亭,長吁一口氣:“原來是場噩夢啊……” “哼国章!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起豆村,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤液兽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后掌动,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體四啰,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年粗恢,在試婚紗的時候發(fā)現(xiàn)自己被綠了柑晒。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡眷射,死狀恐怖匙赞,靈堂內(nèi)的尸體忽然破棺而出恋追,到底是詐尸還是另有隱情,我是刑警寧澤罚屋,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布苦囱,位于F島的核電站,受9級特大地震影響脾猛,放射性物質(zhì)發(fā)生泄漏撕彤。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一猛拴、第九天 我趴在偏房一處隱蔽的房頂上張望羹铅。 院中可真熱鬧,春花似錦愉昆、人聲如沸职员。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽焊切。三九已至,卻和暖如春芳室,著一層夾襖步出監(jiān)牢的瞬間专肪,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工堪侯, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留嚎尤,地道東北人。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓伍宦,卻偏偏與公主長得像芽死,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子次洼,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,490評論 2 348

推薦閱讀更多精彩內(nèi)容