原文地址:http://www.jianshu.com/p/5dc92e06b7f8
自从准备毕业论文开始,就没写过博客了,关注量也明显呈下滑趋势(虽然本来就少)。到现在已经入职一个多月了,抽空把之前做的一个项目整理一下,算是毕业后的第一篇博客吧。
关于Mp3播放器,网上有各种实现方法,但是对于歌词的同步以及滑动更改播放进度的讲解却少之又少,所以我这里重点放在歌词的设计上(需要完整代码的朋友,可以在评论中留下邮箱,我会尽快回复),关于Mp3的“播放\切歌\暂停”以及“随机\顺序\单曲”播放等常用功能应该还是比较好做的。下面看看效果:
-
主界面如下图:
-
右滑之后进入歌词界面:
-
点击右上角那个大设置按钮:
整个项目主要涉及到以下知识点:
- 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开发的朋友,需要注意几个问题:
- 在播放之前一定要先重置、准备。调用的顺序为:reset、setDataSource、prepare、start。
- 由于播放的歌曲通常是在SD卡上,记得要申明权限:
- 因为涉及到搜索歌词、以及随机播放的时候需要计算下一首歌,那么我们分别需要捕捉播放开始和播放结束的信号,可以使用两个监听器完成,如下:
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 getMusicInfoList(Context context) {
Cursor cursor = context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
List 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 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,这样的层级关系非常明确,具体的菜单布局代码如下:
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的功能网上有很多资料,只是在歌词那一块应该还是很空白的。也希望我的这个歌词方案能够给大家带来一些方便,同时大家有什么好的建议欢迎讨论~
——超低空