MP3歌词的同步与拖拽设计

原文地址:http://www.jianshu.com/p/5dc92e06b7f8


自从准备毕业论文开始,就没写过博客了,关注量也明显呈下滑趋势(虽然本来就少)。到现在已经入职一个多月了,抽空把之前做的一个项目整理一下,算是毕业后的第一篇博客吧。


关于Mp3播放器,网上有各种实现方法,但是对于歌词的同步以及滑动更改播放进度的讲解却少之又少,所以我这里重点放在歌词的设计上(需要完整代码的朋友,可以在评论中留下邮箱,我会尽快回复),关于Mp3的“播放\切歌\暂停”以及“随机\顺序\单曲”播放等常用功能应该还是比较好做的。下面看看效果:

  • 主界面如下图:


    图1 - 主界面.jpg
  • 右滑之后进入歌词界面:


    图2 - 右滑进入歌词界面.jpg
  • 点击右上角那个大设置按钮:


    图3 - 设置界面.jpg

整个项目主要涉及到以下知识点:

  • ViewPager
  • Service与Activity通信
  • Broadcast
  • ContentResolver
  • PreferenceActivity
  • MediaPlayer
    以上几个知识点大家应该比较熟悉,,四大组件全用上了,个人觉得这是个比较好的练手项目。下面从播放开始看吧。

1、MP3播放器Service

作为播放器,固然是需要能够支持后台播放的,所以在启动播放之前,需要开启service。为了方便Activity与Service通信,这里通过bindService方法开启Service,代码如下:

bindService(new Intent(MainActivity.this, PlayService.class), connection, Context.BIND_AUTO_CREATE);

其中connection是Servive的一个回调方法,在里面获取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类中,在里面我实现了Mp3的各种常用操作,以及循环、单曲、顺序播放等常用播放模式,通过此类与Service通信,即可完成对MediaPlayer的操作。


3、MediaPlayer的使用

MediaPlayer的使用应该还是很简单的,如果没有做过MediaPlayer开发的朋友,需要注意几个问题:

  1. 在播放之前一定要先重置、准备。调用的顺序为:reset、setDataSource、prepare、start。
  2. 由于播放的歌曲通常是在SD卡上,记得要申明权限:

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

  1. 因为涉及到搜索歌词、以及随机播放的时候需要计算下一首歌,那么我们分别需要捕捉播放开始和播放结束的信号,可以使用两个监听器完成,如下:
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系统会搜索手机里所有的音频文件,并放在MediaStore下面,我们要做的就是从这里面拿出想要的数据。通过

context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER);

可以拿到列表的cursor,然后在当中去逐条获取信息即可。把每一个音频文件视为一个对象,可以如下定义音频对象:

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中获取数据之后填写到上面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然后设置到ListView中就可以完成歌曲列表的显示了。


5、搜索歌词

搜索歌词的原理其实就是在当前歌曲目录下去搜索同名的.lrc文件,然后从中读入数据流进行解析,歌词的解析可以参考lrc歌词的协议自行完成(需要完整代码可以在下面留下您的邮箱)。


6、歌词部分

接下来就是歌词的同步与歌词的滑动了,网上对于同步的实现大多是采用自定义一个TextView,然后再onDraw当中去用Paint画笔来画出歌词。这样做对于同步显示来讲非常容易,但是如果想让他在切换歌词的时候平滑移动以及拖拽歌词改变播放进度这都是比较麻烦的。因此这里我采用ListView来做歌词,这样平滑移动和滑动监听都比较方便。

由于需要将歌词放在屏幕中央,所以需要提前计算出屏幕中央是ListView的第几个Item,然后在前后依次留相应数据的空白。例如第五个item在中间,则在设置歌词数据的时候需要在前后分别留5个空白(示意代码,不建议这么写):

public void setLrcList(List<Lrc> lrcList) {
    //设置歌词内容
    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方法封装更新功能:

/**
 * 更新歌词内容
 *
 * @param position 当前歌曲播放的时间
 */
public void update(int position) {
    if (!isTouching) {
        adapter.notifyDataSetChanged();
        isAutoScroll = true;
        lvLrc.smoothScrollToPositionFromTop(adapter.update(position) - 4, 0, 1000);   //减4是保证当前这句歌词能显示在正中间
    }
}
  • 这里对ListView的滑动没有用到smoothScrollToPosition(int position);原因是这个函数仅仅是保证position的那个item会显示出来,而我们想要的效果是让他显示到正中间,所以只能用smoothScrollToPositionFromTop,让第前四句歌词显示在最顶端来实现效果。
  • 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;
}

这类似一个顺序查找算法,当然朋友们可以采用二分查找等其他算法提高效率。

这里实现的界面是一个ViewPager,第一页是歌曲列表,右滑到第二页是歌词。效果见上图

6.2 拖拽歌词改变播放进度

这部分主要是对歌词布局,即ListView的触摸监听操作,采用listView.setOnTouchListener来实现,先来看看这部分代码:

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自动滑动。为了防止这个矛盾的出现,在歌词同步函数(update)中需要先检查isTouch的值,然后决定是否要进行自动同步(代码见6.1)。


7、设置界面PreferenceActivity

设置界面几乎是所有的App都要用到的,PreferenceActivity就是专门为设置界面打造的,而Android原生代码中几乎所有的设置界面也都是通过这个完成的。PreferenceActivity的使用方法网上有很多,他的使用与一般的布局类似,主要有以下几种类型:

  • ListPreference 列表项菜单
  • EditTextPreference 编辑框菜单
  • SwitchPreference 开关菜单
    本项目中就使用了以上几种菜单项,其余的也大同小异。我们可以对菜单项按功能进行分组,每一组是一个PreferenceCategory,而所有的PreferenceCategory都属于一个PreferenceScreen,这样的层级关系非常明确,具体的菜单布局代码如下:
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"    android:title="设置">
    <PreferenceCategory android:title="播放模式">
        <ListPreference
            android:defaultValue="单曲循环"
            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="将在设置的分钟数后关机"
            android:title="请输入关机时间" />
    </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;
        }
    }
}

当然,以上只是对设值界面进行了显示,还需要完成相应的逻辑和用户设置的持久化,这个大家可以参考PreferenceActivity的具体用法,这里我就不展开讲了,需要完整开发源码的,可以在下面留下邮箱,我会及时给您回复的。


好了,mp3播放器就讲到这里,主要是从逻辑结构上做的梳理,然后针对部分细节进行展开,并没有将完整的代码做一个串接,主要还是考虑到关于Mp3的功能网上有很多资料,只是在歌词那一块应该还是很空白的。也希望我的这个歌词方案能够给大家带来一些方便,同时大家有什么好的建议欢迎讨论~

                                                                      ——超低空
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354

推荐阅读更多精彩内容