国内最全IT社区平台 联系我们 | 收藏本站
华晨云阿里云优惠2
您当前位置:首页 > php开源 > 综合技术 > MP3歌词的同步与拖拽设计

MP3歌词的同步与拖拽设计

来源:程序员人生   发布时间:2016-06-24 17:34:50 阅读次数:2508次

原文地址:http://blog.csdn.net/mc_hust/article/details/51534901

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


关于Mp3播放器,网上有各种实现方法,但是对歌词的同步和滑动更改播放进度的讲授却少之又少,所以我这里重点放在歌词的设计上(需要完全代码的朋友,可以在评论中留下邮箱,我会尽快回复),关于Mp3的“播放\切歌\暂停”和“随机\顺序\单曲”播放等经常使用功能应当还是比较好做的。下面看看效果: 
- 主界面以下图:
图1 - 主界面.jpg
- 右滑以后进入歌词界面:
图2 - 右滑进入歌词界面.jpg
- 点击右上角那个大设置按钮:
图3 - 设置界面.jpg


全部项目主要触及到以下知识点:
- 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个回调方法,在里面获得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类中,在里面我实现了Mp3的各种经常使用操作,和循环、单曲、顺序播放等经常使用播放模式,通过此类与Service通讯,便可完成对MediaPlayer的操作。


3、MediaPlayer的使用

MediaPlayer的使用应当还是很简单的,如果没有做过MediaPlayer开发的朋友,需要注意几个问题:
1. 在播放之前1定要先重置、准备。调用的顺序为:reset、setDataSource、prepare、start。
2. 由于播放的歌曲通常是在SD卡上,记得要申明权限:



3. 由于触及到搜索歌词、和随机播放的时候需要计算下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,然后在当中去逐条获得信息便可。把每个音频文件视为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中获得数据以后填写到上面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文件,然后从中读入数据流进行解析,歌词的解析可以参考lrc歌词的协议自行完成(需要完全代码可以在下面留下您的邮箱)。


6、歌词部份

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

由于需要将歌词放在屏幕中央,所以需要提早计算出屏幕中央是ListView的第几个Item,然后在前后顺次留相应数据的空白。例如第5个item在中间,则在设置歌词数据的时候需要在前后分别留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方法封装更新功能:

/** * 更新歌词内容 * * @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,让第前4句歌词显示在最顶端来实现效果。
  • adapter.update(position):这个方法的作用是获得歌曲播放到position时间的时候是第几句歌词,从而让他显示在中间,代码以下:
public int update(int position) { for (int i = 0; i < lrcList.size() - 1; i++) { //判断当前播放时间是不是在歌词的第1句和最后1句歌词时间内 if (position >= lrcList.get(i).getLrcTime() && position < lrcList.get(i + 1).getLrcTime() || position < lrcList.get(0).getLrcTime()) { index = i; break; } //如果时间超过了最后1句歌词,则停留在最后1句歌词 else if (position > lrcList.get(lrcList.size() - 1).getLrcTime()) { index = lrcList.size() - 1; } } return index; }

这类似1个顺序查找算法,固然朋友们可以采取2分查找等其他算法提高效力。

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

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的使用方法网上有很多,他的使用与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="单曲循环" 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="摇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播放器就讲到这里,主要是从逻辑结构上做的梳理,然后针对部份细节进行展开,并未将完全的代码做1个串接,主要还是斟酌到关于Mp3的功能网上有很多资料,只是在歌词那1块应当还是很空白的。也希望我的这个歌词方案能够给大家带来1些方便,同时大家有甚么好的建议欢迎讨论~

——超低空

生活不易,码农辛苦
如果您觉得本网站对您的学习有所帮助,可以手机扫描二维码进行捐赠
程序员人生
------分隔线----------------------------
分享到:
------分隔线----------------------------
关闭
程序员人生