Android自定义音乐播放器
一:首先介绍用了哪些Android的知识点:
1 MediaPlayer工具来播放音乐
2 Handle。因为存在定时任务(歌词切换,动画,歌词进度条变换等)需要由Handle来处理Ui相关内容
3 动态权限申请(该应用程序读取本地歌曲,并且设置音质相关属性)且这两个权限在Android6.0后都需要动态申请
4 手势控制 (左划和右划需要满足一定条件后可以进行切歌)
5 Service服务 (启动Service 绑定Service 前台Service)
6 BroadcastReceiver 广播,Service与Activity,Activity与Activity存在动态交互,需要广播实现
7 基本的重写View能力和Intent交互数据的能力
8 Animation动画 图片旋转 歌词更新
二:实现过程(简要步骤,下面会详细讲解)
1 先编写MusicInfo工具类。因为我们是从手机内存中去读取音乐的相关信息,那么读出的数据该存储到MusicInfo工具类集合中。
2 先申请权限,然后再去手机内存将音乐及其相关的信息读出来 ,用一个ListView容器去装载所有的本地音乐
PS:到了这里基本的音乐信息列表已经有了 这也是我们的主界面(音乐列表界面) 即: 展示音乐/歌曲列表;
3 这时候先不方去实现播放这类的功能,我们先去处理歌词
这里说明一下。一个歌词文件(.Lrc)里面内容格式如下
(张卫健--真英雄)
可以发现他由时间戳和歌词内容两部分组成。有了这个信息后。编写LrcContent工具类,用于记录歌词内容和歌词时间。然后去手机里面寻找歌曲对应的歌词文件,将其编码,读出,装载为LrcContent集合。
4 编写Service类,Service主要用来处理:音乐播放,前台服务。在播放状态改变的时候与播放音乐的Activity进行通信。该Service由主界面启动,后面的Activity绑定即可
5 编写播放音乐的Activity类(MusicPlay)。当我们从主界面(音乐列表界面)点击了一首具体的音乐时,就会调转到该Activity,所有首先,主界面Activity需要传递一些信息给该Activity。
(1) MusicInfo工具类集合。即手机中所有的音乐信息
(2)当前点击的歌曲,传递位置(position)即可
好,现在我们播放音乐的Activity有了所有的音乐信息,还有当前需要播放的歌曲位置。因为需要前台服务,所以我们把音乐播放的控制权交给Service,我们去绑定服务,然后把所有的音乐信息,还有当前需要播放的歌曲位置都传递给Serivce,Service来控制播放音乐。
6 好了,现在我们的程序可以播放音乐了,我们再来一步步完善细节,歌词同步,该功能自定义View实现,最后显现在播放音乐的Activity中。注意Mediaplayer有一个重要属性:
mediaPlayer.getCurrentPosition()。该方法会返回当前播放时间,不过返回的时候时毫秒(重)。
自定义View(LrcView 显示歌词),该View中除了传统的自定义View需要的OnDraw之类的方法外,还需要获取第三步中的LrcContent集合,有了这个我们就有了所有的歌词内容和歌词相应的时间,那么同步如何实现呢?音乐最终该View要显示在播放音乐的Activity(MusicPlay)中,我们去MusicPlay的布局文件申明该View,然后在MusicPlay中编写一个定时器,可以设定每一秒启动一次,定时器发送消息,在Handle中接受消息,处理消息。Handle中我们需要:获取歌曲当前播放时间,根据当前播放时间去LrcContent集合中寻求匹配的歌词。用invalidate()方法,通知自定义View重绘,来同步更新歌词
7 我们的音乐播放器还差一个重要的东西,音乐控制器部分。这部分需要来控制播放上一首,下一首,播放/暂停,音乐进度拖动,音量设置。
该部分不难,所以再这里不详细讲。
三:效果图 因为完整录制的GIF太大,传不上来所以分批处理
四:代码精讲 代码量也不很大,但是全贴出来挨着讲又影响阅读。所以部分节选和重要知识点一并讲解。
PS:源码中含有大量System.out.println("XXXX");语句。个人比较偏爱的一种测试方式。。应该不干扰阅读,忘见谅
1 权限获取,我们要做的第一件事就是去内存读取音乐相关信息,那么我们就需要获得相关的权限,从Android6.0开始部分权限不仅需要在AndroidManifest.xml文件中声明,还需要在运行程序的时候动态获取.这里以读取存储权限为例:
首先在AndroidManifest.xml中定义
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
在Java业务代码中先判断是否已经有权限,有权限就不再申请,没有就申请权限
//首先检查自身是否已经拥有相关权限,拥有则不再重复申请
int check = checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) ;
//没有相关权限
if (check != PackageManager.PERMISSION_GRANTED)
{
//申请权限 STORGE_REQUEST = 1
ActivityCompat.requestPermissions(this , new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE} ,STORGE_REQUEST);
}else {
//已有权限的情况下可以直接初始化程序
init();
}
当我们申请权限后,去判断用户是否给与了相关权限,如果赋予了就可以做我们的事情了
/*
申请权限处理结果
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode)
{
case STORGE_REQUEST :
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED)
{
//完成程序的初始化
init();
System.out.println("程序申请权限成功,完成初始化") ;
}
else {
System.out.println("程序没有获得相关权限,请处理");
}
break ;
}
}
关于权限这部分:有兴趣的可以看下参考下这篇博客
2 有了权限,我们就可以从手机中去读取音乐相关信息了,这里讲解下如何去读取信息
(1) 我们需要先创建一个存储音乐信息MusicInfo工具类
注意:代码没贴完,还有属性的get和set方法没列出
public class MusicInfo implements Serializable {
private int _id = - 1; //音乐标识码
private int duration = -1 ; //音乐时常
private String artist = null ; //音乐作者
private String musicName= null ; //音乐名字
private String album = null ; //音乐文件专辑
private String title = null ; //音乐文件标题
private int size ; //音乐文件的大小 返回byte大小
private String data ; //获取文件的完整路径
private String album_id ; //实际存储为音乐专辑团片
}
(2)读取音乐信息。
Android中的音乐信息存储再手机数据库中,那么我们就需要去访问数据库。这里介绍下如何访问数据库
首先要知道基本数据库查询操作,返回Cursor对象
public final Cursor query(Uri uri , //查询路径
String[] projection , //查询指定的列
String selection, //查询条件
String[] selectionArgs, //查询参数
String sortOrder } //查询结果的排序方式
那么就有了我们查询音乐的操作。
//申明ContentResolver对象,用于访问系统数据库 private ContentResolver contentResolver ; //用于装载MusicInfo对象 private ListmusicInfos ;
//获取系统的ContentResolver contentResolver = getContentResolver() ; //从数据库中获取指定列的信息 mCursor = contentResolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI , new String[] {MediaStore.Audio.Media._ID , MediaStore.Audio.Media.TITLE , MediaStore.Audio.Media.ALBUM , MediaStore.Audio.Media.ARTIST , MediaStore.Audio.Media.DURATION , MediaStore.Audio.Media.DISPLAY_NAME , MediaStore.Audio.Media.SIZE , MediaStore.Audio.Media.DATA , MediaStore.Audio.Media.ALBUM_ID } , null ,null ,null) ;
我们已经查询出了所有的音乐信息,并且存储在mCursor对象中,现在我们需要把音乐信息装载到musicInfos工具类集合中。
但是注意,音乐专辑图片需要特殊处理:我们这是只查询出了音乐专辑id MediaStore.Audio.Media.ALBUM_ID 我们需要根据音乐专辑id再去查询音乐专辑图片,用该图片来作为音乐图片。
/* 获取本地音乐专辑的图片 */ private String getAlbumArt(int album_id) { String UriAlbum = "content://media/external/audio/albums" ; String projecttion[] = new String[] {"album_art"} ; Cursor cursor = contentResolver.query(Uri.parse(UriAlbum + File.separator +Integer.toString(album_id)) , projecttion , null , null , null); String album = null ; if (cursor.getCount() > 0 && cursor.getColumnCount() > 0) { cursor.moveToNext() ; album = cursor.getString(0) ; } //关闭资源数据 cursor.close(); return album ; }
这时还可能出现一个问题,可能存在本地专辑音乐图片不存在,所以我们要进行判断,没有专辑图片就使用默认的图片。好了,现在我们就可以装载音乐信息了
musicInfos = new ArrayList<>() ; for (int i = 0 ; i < mCursor.getCount() ; i++) { Mapmap = new HashMap<>() ; MusicInfo musicInfo = new MusicInfo() ; //列表移动 mCursor.moveToNext() ; //将数据装载到List 中 musicInfo.set_id(mCursor.getInt(0)); musicInfo.setTitle(mCursor.getString(1)); musicInfo.setAlbum(mCursor.getString(2)); musicInfo.setArtist(mCursor.getString(3)); musicInfo.setDuration(mCursor.getInt(4)); musicInfo.setMusicName(mCursor.getString(5)); musicInfo.setSize(mCursor.getInt(6)); musicInfo.setData(mCursor.getString(7)); //将数据装载到List //获取本地音乐专辑图片 String MusicImage = getAlbumArt(mCursor.getInt(8)) ; //判断本地专辑的图片是否为空 if (MusicImage == null) { //为空,用默认图片 map.put("image" , String.valueOf(R.mipmap.timg)) ; musicInfo.setAlbum_id(String.valueOf(R.mipmap.timg)); }else { //不为空,设定专辑图片为音乐显示的图片 map.put("image" , MusicImage) ; musicInfo.setAlbum_id(MusicImage); } // musicInfo.setAlbum_id(mCursor.getInt(8)); musicInfos.add(musicInfo) ;
3 MediaPlayer重要方法:MediaPlayer用来指定音乐文件和播放相关动作,所以我们需要掌握与音乐播放相关的方法。
方法: getCurrentPosition()
解释:返回 Int, 得到当前播放位置 毫秒单位时间
方法: getDuration()
解释:返回 Int,得到文件的时间
方法:isLooping()
解释:返回 boolean ,是否循环播放
方法:isPlaying()
解释:返回 boolean,是否正在播放
方法:pause()
解释:无返回值 ,暂停
方法:release()
解释:无返回值,释放 MediaPlayer 对象
方法:reset()
解释:无返回值,重置 MediaPlayer 对象
方法:seekTo(int msec)
解释:无返回值,指定播放的位置(以毫秒为单位的时间)
方法:setDataSource(String path)
解释:无返回值,设置多媒体数据来源【根据 路径】
方法:setLooping(boolean looping)
解释:无返回值,设置是否循环播放
方法:start()
解释:无返回值,开始播放
方法:stop()
解释:无返回值,停止播放
方法:setVolume(float leftVolume, float rightVolume)
解释:无返回值,设置音量
事件:setOnCompletionListener(MediaPlayer.OnCompletionListener listener)
解释:监听事件,网络流媒体播放结束监听
事件:setOnBufferingUpdateListener(MediaPlayer.OnBufferingUpdateListener listener)
解释:监听事件,网络流媒体的缓冲监听
事件:setOnErrorListener(MediaPlayer.OnErrorListener listener)
解释:监听事件,设置错误信息监听
4 歌词处理 说下思路把:首先根据歌曲去内存寻找对应的歌词文件,找到后解码为歌词内容和歌词时间,并且把歌词内容和个时间装载到歌词信息类集合中。然后提供一个外部访问的方法用于向外部输出歌词信息
// 注:LrcContent是歌词处理类 其中包含两个参数,歌词时间和歌词内容
public class LrcProcess {
//所有需要处理的歌词对象
private List lrcList;
//一个歌词对象
private LrcContent mLrcContent;
/*
构造函数完成对象的初始化
*/
public LrcProcess() {
lrcList = new ArrayList<>();
}
/*
*从内存中读取歌词文件,并转换为String对象输出
*/
public String readLrc(String path)
{
//用StringBuild来存储歌词内容
StringBuilder sb = new StringBuilder() ;
//获取歌词文件 因为传入的文件为MusicInfo类中的Data内容,其文件为mp3,需要更换为lrc文件
File f = new File(path.replace(".mp3" , ".lrc")) ;
try{
//通过文件流对象来获取文件内容并且导入歌词内容对象集合中(lrcList)
FileInputStream inputStream = new FileInputStream(f) ;
InputStreamReader streamReader = new InputStreamReader(inputStream , "utf-8") ;
BufferedReader bufferedReader = new BufferedReader(streamReader) ;
String tempStr = "" ;
while((tempStr = bufferedReader.readLine()) != null)
{
//实现字符替换
tempStr = tempStr.replace("[" , "") ;
tempStr = tempStr.replace("]" , "@") ;
//根据@分号对文件分离
String[] splitData = tempStr.split("@") ;
// System.out.println("THE TEMP STR IS " + splitData[0]) ;
if (splitData.length > 1)
{
//新建歌词内容对象
mLrcContent = new LrcContent();
//设置歌词文本内容
mLrcContent.setLrcStr(splitData[1]);
//设置歌词时间
int lrcTime = timeToStr(splitData[0]) ;
mLrcContent.setLrcTime(lrcTime);
//添加到列表
lrcList.add(mLrcContent);
// System.out.println("录入歌词成功") ;
}else {
// System.out.println("录入歌词失败") ;
}
}
}catch (FileNotFoundException e)
{
sb.append("木有歌词文件,赶紧去下载!...");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
sb.append("木有读取到歌词哦!");
} catch (IOException e) {
e.printStackTrace();
sb.append("木有读取到歌词哦!");
}
return sb.toString() ;
}
/*
* 对歌词文件lrc中的时间内容进行转码
* [00:02.32]陈奕迅 时间分别代表分,秒,毫秒
* [00:03.43]好久不见
*/
public int timeToStr(String timeStr)
{
timeStr = timeStr.replace(":" , ".") ;
timeStr = timeStr.replace("." , "@") ;
String []splitTime = timeStr.split("@") ;
//分离出分, 秒, 毫秒
int minute = Integer.parseInt(splitTime[0]) ;
int second = Integer.parseInt(splitTime[1]) ;
int millisSecond = Integer.parseInt(splitTime[2]) ;
int time = (minute * 60 + second) * 1000 + millisSecond ;
return time ;
}
/*
提供一个外界方法歌词对象集合的方法
*/
public List getLrcList()
{
return lrcList ;
}
}
//实例化歌词处理对象 Activity中调用方法
//musicInfosList为封装好的所有音乐信息类,get(position)可以获取当前音乐信息,getData获取音乐路径
LrcProcess mLrcProcess = new LrcProcess() ;
mLrcProcess.readLrc(musicInfosList.get(position).getData()) ;
这里再补一个内容:通常经常下载的歌曲都是没有歌词文件的,所以我们需要自己去下载歌词文件,注意该程序中你需要把歌词文件和音乐文件放在一个地方,并且歌词文件和音乐文件前缀相同只是将.mp3改为了.lrc。我用的是网易云音乐,这里顺便推荐一个网易云下载歌词的网址,大神们各种推荐供你选择
知乎:网易云音乐怎么下载歌词?
5 歌曲播放界面
看图分析:
(1)标题动画
这两个TextView的布局代码就不再列出了。动画效果主要是继承了Animation类自定实现。
public class TextAnimation extends Animation { private float currentX ; //指定X private float currentY ; //指定Y //定义持续时间 private int duration ; //设定Camera private Camera camera = new Camera() ; public TextAnimation(float x , float y ,int duration) { currentX = x ; currentY = y ; this.duration = duration ; System.out.println("THE X IS " + currentX + "\nTHE Y IS " + currentY) ; } @Override public void initialize(int width, int height, int parentWidth, int parentHeight) { super.initialize(width, height, parentWidth, parentHeight); //设置持续时间 setDuration(duration); //设置动画结束后保留 setFillAfter(true); //设置变换速度 setInterpolator(new AccelerateDecelerateInterpolator()); //减速 //setInterpolator(new AccelerateInterpolator()); //加速 默认情况 //setInterpolator(new LinearInterpolator()); //匀速 } @Override protected void applyTransformation(float interpolatedTime, Transformation t) { super.applyTransformation(interpolatedTime, t); /* 保存 */ camera.save(); //根据interpolatedTime来控制X Y Z上的偏移 camera.translate(10.0f - 10.0f * interpolatedTime , 30.0f - 30.0f * interpolatedTime , 80.0f - 80.0f * interpolatedTime); //根据interpolatedTime在X轴做角度变换 camera.rotateX(360 * interpolatedTime); //根据interpolatedTime在Y轴做角度变换 camera.rotateY(360 * interpolatedTime); //获取Transformation参数封装的matrix对象 camera.getMatrix(t.getMatrix()); t.getMatrix().preTranslate(-currentX / 4 , -currentY / 4) ; t.getMatrix().postTranslate(currentX , currentY) ; /* 如果存在保存的状态,就恢复 */ camera.restore(); } }
/* 获取并且设置音乐的标题和歌手。并添加动画来显示 */
MusicArtist = (TextView) findViewById(R.id.MusicArtist) ; MusicName = (TextView) findViewById(R.id.MusicName) ; MusicName.setText(musicInfosList.get(position).getTitle()); MusicArtist.setText(musicInfosList.get(position).getArtist()); //动画效果 MusicArtist.setAnimation(new TextAnimation(0 , 0 , 2000)); MusicName.setAnimation(new TextAnimation(0 , 0 , 2000));
1,圆形图片
需要用到Shader
public class XCRoundImageView extends android.support.v7.widget.AppCompatImageView{ private Paint mPaintBitmap = new Paint(Paint.ANTI_ALIAS_FLAG); private Bitmap mRawBitmap; private BitmapShader mShader; private Matrix mMatrix = new Matrix(); public XCRoundImageView(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onDraw(Canvas canvas) { //获取原生Bitmap位图 Bitmap rawBitmap = getBitmap(getDrawable()); if (rawBitmap != null){ //获取图片宽和高 int viewWidth = getWidth(); int viewHeight = getHeight(); //由于要变换为圆形,在变换过程中取边长相对小的为基准 int viewMinSize = Math.min(viewWidth, viewHeight); float dstWidth = viewMinSize; float dstHeight = viewMinSize; //如果是第一次绘制 if (mShader == null || !rawBitmap.equals(mRawBitmap)){ mRawBitmap = rawBitmap; /* BitmapShader是Shader的子类,可以通过Paint.setShader(Shader shader)进行设置、 这里我们只关注BitmapShader,构造方法: mBitmapShader = new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP); 参数1:bitmap 参数2,参数3:TileMode; TileMode的取值有三种: CLAMP 拉伸 REPEAT 重复 MIRROR 镜像 如果大家给电脑屏幕设置屏保的时候,如果图片太小,可以选择重复、拉伸、镜像; 重复:就是横向、纵向不断重复这个bitmap 镜像:横向不断翻转重复,纵向不断翻转重复; 拉伸:这个和电脑屏保的模式应该有些不同,这个拉伸的是图片最后的那一个像素;横向的最后一个横行像素,不断的重复,纵项的那一列像素,不断的重复; */ mShader = new BitmapShader(mRawBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); } if (mShader != null){ /* void setLocalMatrix(Matrix localM); 设置shader的本地矩阵,如果localM为空将重置shader的本地矩阵。 */ mMatrix.setScale(dstWidth / rawBitmap.getWidth(), dstHeight / rawBitmap.getHeight()); mShader.setLocalMatrix(mMatrix); } mPaintBitmap.setShader(mShader); float radius = viewMinSize / 2.0f; canvas.drawCircle(radius, radius, radius, mPaintBitmap); } else { super.onDraw(canvas); } } private Bitmap getBitmap(Drawable drawable){ if (drawable instanceof BitmapDrawable){ return ((BitmapDrawable)drawable).getBitmap(); } else if (drawable instanceof ColorDrawable){ Rect rect = drawable.getBounds(); int width = rect.right - rect.left; int height = rect.bottom - rect.top; int color = ((ColorDrawable)drawable).getColor(); Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); canvas.drawARGB(Color.alpha(color), Color.red(color), Color.green(color), Color.blue(color)); return bitmap; } else { return null; } } }
PS:作者当时也是搬运的,只是补充了部分注释。内容可能有点复杂,需要多实践,建议参考资料文档
首先定义旋转动画 :image_rotate.xml
然后在Java业务代码中设定定时器来启动动画
/* 该Timer用于实现:音乐播放界面图片旋转动画 */ MyHandle2 handle2 = new MyHandle2() ; new Timer().schedule(new TimerTask() { @Override public void run() { handle2.sendEmptyMessage(0x112) ; } }, 0 ,8000);
最后再Handle中接收消息,并且启动动画
public class MyHandle2 extends Handler { @Override public void handleMessage(Message msg) { if ((msg.what == 0x112)) { //设置图片旋转 MusicImage.setAnimation(AnimationUtils.loadAnimation(MusicPlay.this , R.anim.image_rotate)); } } }
五:下载地址github
https://github.com/547291213/MusicPlayer
六:参考资料
七:总结
第一次写这么长文的博客,也没想过邀功。不过真心希望您再阅读了这篇博文,并且有所建议和收获后留下您宝贵的评论。谢谢。
PS:转载请注明 https://blog.csdn.net/qq_29989087/article/details/80206290