这次介绍一个多功能音乐播放器,记得是大二那年寒假写的,实现的主要功能就是音乐播放,带进度条控制,扫描本地音乐,上一曲下一曲,播放类型(单曲循环,顺序播放,随机播放),APP主题换肤,背景图更换等,功能都比较基础,基本上如果你不会的话,跟着我的思路,应该都是能实现的,预计会在以后加入歌词的功能。
在开始前,先放一张最后的效果图吧,我个人喜欢的风格,简约,美观。
目录
1.实现扫描本地音乐
2.音乐的播放与控制
3.关联进度条seekbar,自定义seekbar
4.单曲循环,顺序播放,随机播放的实现
5.设置喜爱音乐
6.播放列表背景图设置与保存
7.实现APP主题换肤的功能
正文
1.实现扫描本地音乐
这里为了将每个系统里面存放的音乐抽象出来,也是为了方便管理,先定义一个音乐类Song,代码如下
public class Song { /** * 歌手 */ private String singer; /** * 歌曲名 */ private String song; /** * 歌曲的地址 */ private String path; /** * 歌曲长度 */ private int duration; /** * 歌曲的大小 */ private long size; public String getSinger() { return singer; } public void setSinger(String singer) { this.singer = singer; } public String getSong() { return song; } public void setSong(String song) { this.song = song; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } public int getDuration() { return duration; } public void setDuration(int duration) { this.duration = duration; } public long getSize() { return size; } public void setSize(long size) { this.size = size; } }
然后我们再写一个工具类,这个工具类实现的功能就是扫描系统中的本地音乐,返回一个List<Song>集合,供我们使用,代码如下
public class MusicUtils { /** * 扫描系统里面的音频文件,返回一个list集合 */ public static List<Song> getMusicData(Context context) { List<Song> list = new ArrayList<>(); Cursor cursor = context.getContentResolver().query( MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null, MediaStore.Audio.AudioColumns.IS_MUSIC); if (cursor != null) { while (cursor.moveToNext()) { Song song = new Song(); song.setSong( cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME))); song.setSinger( cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST))); song.setPath(cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA))); song.setDuration( cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION))); song.setSize( cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE))); if (song.getSize() > 1000 * 800) {//过滤掉短音频 // 分离出歌曲名和歌手 if (song.getSong().contains("-")) { String[] str = song.getSong().split("-"); song.setSinger( str[0]); song.setSong( str[1]); } list.add(song); } } // 释放资源 cursor.close(); } return list; } //格式化时间 public static String formatTime(int time) { if (time / 1000 % 60 < 10) { return time / 1000 / 60 + ":0" + time / 1000 % 60; } else { return time / 1000 / 60 + ":" + time / 1000 % 60; } } }
然后,在布局里定义一个Listview,再给Listview写一个适配器,一般继承自BaseAdapter,adapter代码如下
public class MyAdapter extends BaseAdapter { private Context context; private List<Song> list; private int position_flag = 0; public MyAdapter(MainActivity mainActivity, List<Song> list) { this.context = mainActivity; this.list = list; } @Override public int getCount() { return list.size(); } @Override public Object getItem(int i) { return list.get(i); } @Override public long getItemId(int i) { return i; } @Override public View getView(int i, View view, ViewGroup viewGroup) { ViewHolder holder = null; if (view == null) { holder = new ViewHolder(); // 引入布局 view = View.inflate(context, R.layout.list_item, null); // 实例化对象 holder.song = (TextView) view.findViewById(R.id.item_mymusic_song); holder.singer = (TextView) view .findViewById(R.id.item_mymusic_singer); holder.duration = (TextView) view .findViewById(R.id.item_mymusic_duration); holder.position = (TextView) view .findViewById(R.id.item_mymusic_postion); view.setTag(holder); } else { holder = (ViewHolder) view.getTag(); } // 给控件赋值 String string_song = list.get(i).getSong(); if (string_song.length() >= 5 && string_song.substring(string_song.length() - 4, string_song.length()).equals(".mp3")) { holder.song.setText(string_song.substring(0, string_song.length() - 4).trim()); } else { holder.song.setText(string_song.trim()); } holder.singer.setText(list.get(i).getSinger().toString().trim()); // 时间转换为时分秒 int duration = list.get(i).getDuration(); String time = MusicUtils.formatTime(duration); holder.duration.setText(time); return view; } class ViewHolder { TextView song;// 歌曲名 TextView singer;// 歌手 TextView duration;// 时长 TextView position;// 序号 } }
adapter里面的列表项布局代码如下
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="60dp" android:orientation="horizontal"> <TextView android:id="@+id/item_mymusic_postion" android:layout_width="30dp" android:layout_height="30dp" android:layout_alignBottom="@+id/item_mymusic_singer" android:layout_gravity="center_vertical" android:gravity="center" android:layout_margin="10dp" android:text="1" android:textSize="18sp" /> <RelativeLayout android:layout_width="match_parent" android:layout_height="60dp"> <TextView android:id="@+id/item_mymusic_song" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:ellipsize="end" android:maxLines="1" android:layout_marginRight="10dp" android:layout_marginTop="5dp" android:text="歌曲名" android:textSize="18sp" /> <TextView android:id="@+id/item_mymusic_singer" android:layout_width="wrap_content" android:layout_height="30dp" android:layout_alignParentBottom="true" android:layout_alignParentLeft="true" android:layout_toLeftOf="@+id/item_mymusic_duration" android:gravity="bottom" android:text="歌手" android:ellipsize="end" android:maxLines="1" android:layout_marginBottom="5dp" android:textSize="16sp" /> <TextView android:id="@+id/item_mymusic_duration" android:layout_width="wrap_content" android:layout_height="30dp" android:layout_alignParentBottom="true" android:layout_alignParentRight="true" android:gravity="bottom" android:layout_marginRight="5dp" android:layout_marginBottom="5dp" android:text="歌曲时间" android:textSize="16sp" /> </RelativeLayout> </LinearLayout>
然后在Listview所在的activity里,调用工具类获取音乐集合,构造适配器,给Listview设置适配器,即可在Listview中显示本地所有的音乐啦,关键代码就三行,如下
List<Song> list = MusicUtils.getMusicData(MainActivity.this); MyAdapter adapter = new MyAdapter(MainActivity.this, list); listview.setAdapter(adapter);
好了,到现在为止,你已经实现了,显示手机里所有的音乐,但是还不能播放,怎么播放,接着往下看
2.音乐的播放与控制
实现音乐播放,需要用到的类为MediaPlayer,为了方便,封装一个播放音乐的方法,如下
private void musicplay(int position) { try { mplayer.reset(); mplayer.setDataSource(list.get(position).getPath()); mplayer.prepare(); mplayer.start(); } catch (Exception e) { e.printStackTrace(); } }
传入的position为,播放的音乐的位置,即序号。
然后给listview设置点击事件
listview.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { musicplay(currentposition); } });
这样我们只是实现了简单的播放,点击Listview对应的条目,即可播放对应的音乐
我们下一步就是实现,音乐播放的控制,即暂停,下一曲,上一曲的实现
首先是暂停,在播放按钮的点击时间中,我们通常的需求是这样的,如果当前音乐正在播放,那么点击,暂停音乐,再点击,即可再次接着上次的继续播放,所以在播放按钮的点击事件中,需要根据不同情况处理,同时为了直观,需要准备两张图片,播放的时候一张,暂停的时候一张,播放按钮的点击事件如下
imageView_play.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mplayer.isPlaying()) { mplayer.pause(); imageview.clearAnimation(); } else { mplayer.start(); // thread = new Thread(new SeekBarThread()); // thread.start(); imageview.startAnimation(AnimationUtils.loadAnimation( MainActivity.this, R.anim.imageview_rotate)); } } });
由于为了界面体验良好,我这里还设置了,当音乐播放的时候,左侧图片的旋转效果,代码已经在上面的点击事件中,效果图如下
左侧imageview的动画代码如下
<?xml version="1.0" encoding="utf-8"?> <rotate xmlns:android="http://schemas.android.com/apk/res/android" android:duration="20000" android:fromDegrees="0" android:interpolator="@android:anim/linear_interpolator" android:pivotX="50%" android:pivotY="50%" android:repeatCount="-1" android:repeatMode="restart" android:toDegrees="360" />
扯了点其他的,下面来实现上一曲和下一曲的效果,我们也可以和播放一个,分别写一个对应的方法
上一曲方法代码如下
// 上一曲 private void frontMusic() { currentposition--; if (currentposition < 0) { currentposition = list.size() - 1; } musicplay(currentposition); }
其中,currentposition是记录的当前音乐播放序号,这里有一点需要考虑的是,当前播放音乐的序号为0的时候,进行--操作之后那么会变成负数,所以,这里根据逻辑,处理为播放列表最后一曲,即设置序号为list.size()-1,形成一个环形。
相信你看了上一曲的方法,那么下一曲也很简单了,下一曲方法代码如下
// 下一曲 private void nextMusic() { currentposition++; if (currentposition > list.size() - 1) { currentposition = 0; } musicplay(currentposition); }同样我们也需要处理播放歌曲到最后一曲的时候,设置为播放列表第一首歌曲。
3.关联进度条seekbar,自定义seekbar
关联进度条的方法也很简单,这里将更新seekbar的方法重新开了一个线程,专门处理更新,代码如下
// 自定义的线程,用于下方seekbar的刷新 class SeekBarThread implements Runnable { @Override public void run() { while (!ischanging && mplayer.isPlaying()) { // 将SeekBar位置设置到当前播放位置 seekBar.setProgress(mplayer.getCurrentPosition()); try { // 每500毫秒更新一次位置 Thread.sleep(500); // 播放进度 } catch (InterruptedException e) { e.printStackTrace(); } } } }
其中,ischanging用于判断当前的seekbar是否处于滑动状态,然后在音乐播放的地方,也就是刚才封装的musicplay方法中,更改为如下代码
private void musicplay(int position) { seekBar.setMax(list.get(position).getDuration()); imageview.startAnimation(AnimationUtils.loadAnimation( MainActivity.this, R.anim.imageview_rotate)); try { mplayer.reset(); mplayer.setDataSource(list.get(position).getPath()); mplayer.prepare(); mplayer.start(); } catch (Exception e) { e.printStackTrace(); } thread = new Thread(new SeekBarThread()); thread.start(); }
当然,不要忘了先设置seekbar的最大刻度值,也就是上面代码中setMax方法。
至此,你的音乐播放就已经和seekbar进度条关联起来了,但是你可能会发现系统默认的进度条很丑,不符合你的审美,那么我们就需要更改seekbar的样式,也就是自定义seekbar。
自定义seekbar,需要在布局中设置progressDrawable和thumb,分别对应进度条的背景和进度条的指示小图标,我这里进度条的背景采用的是drawable,代码如下
<?xml version="1.0" encoding="utf-8"?> <layer-list xmlns:android="http://schemas.android.com/apk/res/android" > <item android:id="@+id/background"> <shape> <solid android:color="#DCDCDC"/> </shape> </item> <item android:id="@+id/secondaryProgress"> <clip> <shape> <solid android:color="@color/blue" /> </shape> </clip> </item> <item android:id="@+id/progress"> <clip> <shape> <solid android:color="@color/blue" /> </shape> </clip> </item> </layer-list>
而thumb,我这里使用的就是一张图片,可以在我的项目源代码中找到,图片长下面这个样子
当然你也可以采用自己的图片,来实现炫酷的效果哦!
4.单曲循环,顺序播放,随机播放的实现
实现这个效果,首先我哦们定义一个变量,用于记录当前的播放类型是哪种,如下
// 用于判断当前的播放顺序,0->单曲循环,1->顺序播放,2->随机播放 private int play_style = 0;
然后在我们的更改播放类型的按钮点击事件中,更改它的值,点击事件代码如下
imageview_playstyle.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { play_style++; if (play_style > 2) { play_style = 0; } switch (play_style) { case 0: imageview_playstyle.setImageResource(R.mipmap.cicle); Toast.makeText(MainActivity.this, "单曲循环", Toast.LENGTH_SHORT).show(); break; case 1: imageview_playstyle.setImageResource(R.mipmap.ordered); Toast.makeText(MainActivity.this, "顺序播放", Toast.LENGTH_SHORT).show(); break; case 2: imageview_playstyle.setImageResource(R.mipmap.unordered); Toast.makeText(MainActivity.this, "随机播放", Toast.LENGTH_SHORT).show(); break; } } });
逻辑比较简单,应该都能看懂,然后就是怎么根据这个变量来实现对应的效果,核心方法就是MediaPLayer的setOnCompeleteListener,代码如下
// 监听mediaplayer播放完毕时调用 mplayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { // TODO Auto-generated method stub switch (play_style) { case 0: musicplay(currentposition); break; case 1: nextMusic(); break; case 2: random_nextMusic(); break; default: break; } } });
下一曲的代码上面已经给出了,下面是随机播放下一曲的代码,思想很简单,就是生成一个随机数,再设置为currentpositon,然后调用musicplay方法即可
// 随机播放下一曲 private void random_nextMusic() { currentposition = currentposition + random.nextInt(list.size() - 1); currentposition %= list.size(); musicplay(currentposition); }
5.设置喜爱音乐
喜爱音乐的设置,我这里处理的比较简单, 当长按列表项的时候,弹出对话框,用于设置喜爱音乐,效果如下
然后,用sharepreference记录下喜爱音乐的序号值,当要播放喜爱音乐的时候,直接取到该序号值,然后调用musicplay方法播放序号值对应的音乐即可。主要就是sharepreference的使用,代码很简单,就不贴了
6.播放列表背景图设置与保存
设置播放列表背景也就是调用一下,listview.setBackground即可,但是我们如果不进行保存的话,下次进入APP的时候,背景图可能又恢复为初始的,那么我们就需要保存列表ode背景图,这里也采用sharepreference来保存,首先用Base64将图片转换为String,然后保存起来,下次进入APP的时候,再取出来,用Base64将String转为drawable对象,在设置上去即可。相关代码如下。
// 使用sharedPreferences保存listview背景图片 private void saveDrawable(Drawable drawable) { SharedPreferences.Editor editor = sharedPreferences.edit(); BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable; Bitmap bitmap = bitmapDrawable.getBitmap(); // Bitmap bitmap = BitmapFactory.decodeResource(getResources(), id); ByteArrayOutputStream baos = new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.JPEG, 50, baos); String imageBase64 = new String(Base64.encodeToString( baos.toByteArray(), Base64.DEFAULT)); editor.putString("listbg", imageBase64); editor.commit(); } // 加载用sharedPreferences保存的图片 private Drawable loadDrawable() { String temp = sharedPreferences.getString("listbg", ""); ByteArrayInputStream bais = new ByteArrayInputStream(Base64.decode( temp.getBytes(), Base64.DEFAULT)); return Drawable.createFromStream(bais, ""); }
7.实现APP主题换肤的功能
实现主题效果,有很多种方法,我这里采用的是自定义属性的方法,首先我们在values下新建一个文件attrs,内容如下
<?xml version="1.0" encoding="utf-8"?> <resources> <attr name="theme_color" format="color" /> <attr name="popupwindow_bg" format="reference"/> <attr name="dialogactivity_bg" format="reference"/> <attr name="btn_submit_bg" format="reference"/> <attr name="seekbar_progress_bg" format="reference"/> <attr name="play_image" format="reference" /> <attr name="next_image" format="reference" /> <attr name="front_image" format="reference" /> <attr name="thumb_image" format="reference" /> <attr name="indicate_image" format="reference" /> </resources>
这里每一个attr属性代表了哪些内容需要根据主题不同而更换,比如popupwindow_bg,即弹出窗口的背景色等等,然后在styles文件文件中指定各个主题下,这些值分别对应哪个具体的值,styles中相关代码如下
<style name="Theme_blue"> <item name="theme_color">@color/blue</item> <item name="popupwindow_bg">@drawable/popupwindow_bg</item> <item name="dialogactivity_bg">@drawable/dialogactivity_bg</item> <item name="btn_submit_bg">@drawable/btn_submit_bg</item> <item name="seekbar_progress_bg">@drawable/seekbar_progress_bg</item> <item name="play_image">@mipmap/play</item> <item name="next_image">@mipmap/next</item> <item name="front_image">@mipmap/front</item> <item name="thumb_image">@mipmap/seekbar_thumb</item> <item name="indicate_image">@mipmap/play_small</item> </style> <style name="Theme_purple"> <item name="theme_color">@color/purple</item> <item name="popupwindow_bg">@drawable/popupwindow_bg_purple</item> <item name="dialogactivity_bg">@drawable/dialogactivity_bg_purple</item> <item name="btn_submit_bg">@drawable/btn_submit_bg_purple</item> <item name="seekbar_progress_bg">@drawable/seekbar_progress_bg_purple</item> <item name="play_image">@mipmap/play_purple</item> <item name="next_image">@mipmap/next_purple</item> <item name="front_image">@mipmap/front_purple</item> <item name="thumb_image">@mipmap/seekbar_thumb_purple</item> <item name="indicate_image">@mipmap/play_small_purple</item> </style> <style name="Theme_green"> <item name="theme_color">@color/green</item> <item name="popupwindow_bg">@drawable/popupwindow_bg_green</item> <item name="dialogactivity_bg">@drawable/dialogactivity_bg_green</item> <item name="btn_submit_bg">@drawable/btn_submit_bg_green</item> <item name="seekbar_progress_bg">@drawable/seekbar_progress_bg_green</item> <item name="play_image">@mipmap/play_green</item> <item name="next_image">@mipmap/next_green</item> <item name="front_image">@mipmap/front_green</item> <item name="thumb_image">@mipmap/seekbar_thumb_green</item> <item name="indicate_image">@mipmap/play_small_green</item> </style> <style name="Theme_red"> <item name="theme_color">@color/red</item> <item name="popupwindow_bg">@drawable/popupwindow_bg_red</item> <item name="dialogactivity_bg">@drawable/dialogactivity_bg_red</item> <item name="btn_submit_bg">@drawable/btn_submit_bg_red</item> <item name="seekbar_progress_bg">@drawable/seekbar_progress_bg_red</item> <item name="play_image">@mipmap/play_red</item> <item name="next_image">@mipmap/next_red</item> <item name="front_image">@mipmap/front_red</item> <item name="thumb_image">@mipmap/seekbar_thumb_red</item> <item name="indicate_image">@mipmap/play_small_red</item> </style>
可以很清楚的看到,我设置了四个主题,每个主题中,我都对attrs中定义的属性进行了具体的赋值,然后怎么使用呢,举个例子,比如我现在需要让popupwindow的背景色随主题改变而更换,那么在popupwindow的布局中,设置其background属性为如下即可
android:background="?attr/popupwindow_bg"
其他属性的使用方法同理,然后我们如何来让用户设置主题呢,可以写一个dialog,也可popupwindow,不过我这里为了学习一下样式为dialog的activity,便采用了这种方式,最后效果如下
看上去就像一个dialog,其实是一个activity,然后在这里根据用户的选择,来设置不同的主题,然后拿到主题的类型之后,在代码中根据这个值去判断应该显示哪个主题,相关代码如下
// 主题设置 string_theme = sharedPreferences.getString("theme_select", "blue"); if (string_theme.equals("blue")) { setTheme(R.style.Theme_blue); } else if (string_theme.equals("purple")) { setTheme(R.style.Theme_purple); } else if (string_theme.equals("green")) { setTheme(R.style.Theme_green); } else { setTheme(R.style.Theme_red); } setContentView(R.layout.activity_main);记住一定要在setContentView方法之前调用,具体细节各方面由于代码比较散,不方便贴,可以去源码里看我是怎么设置的,最终四个主题下的主界面效果如下
当然这个APP里,还有很多其他的细节,诸如,控制当前播放的列表项为不同颜色,顶部显示歌曲名字的彩色TextView等,这些可以直接去看源码,实现的方法也不难,欢迎访问源码!!