本系列博文,详细讲述一个音乐播放器的实现,以及从网络解析数据获取最新推荐歌曲以及歌曲下载的功能。
功能介绍如下:
1、获取本地歌曲列表,实现歌曲播放功能。
2、利用硬件加速感应器,摇动手机实现切换歌曲的功能
3、利用jsoup解析网页数据,从网络获取歌曲列表,同时实现歌曲和歌词下载到手机本地的功能。
4、通知栏提醒,实现仿QQ音乐播放器的通知栏功能.
涉及的技术有:
1、jsoup解析网络网页,从而获取需要的数据
2、android中访问网络,获取文件到本地的网络请求技术,以及下载文件到本地实现断点下载
3、线程池
4、图片缓存
5、service一直在后台运行
6、手机硬件加速器
7、notification通知栏设计
8、自定义广播
9、android系统文件管理
上面两篇博文android-音乐播放器实现及源码下载(一)讲述了activity基类实现和application类的实现,以及最终设计界面展示。
android-音乐播放器实现及源码下载(二)讲述了主界面的设计和实现
本篇主要讲述两个service服务的设计和实现,一个是播放歌曲的service服务,一个是下载歌曲的service服务。
播放歌曲的service服务代码如下:
/** * 2015年8月15日 16:34:37 * 博文地址:http://blog.csdn.net/u010156024 * 音乐播放服务 服务中启动了硬件加速器感应器 * 实现功能:摇动手机自动播放下一首歌曲 */public class PlayService extends Service implements MediaPlayer.OnCompletionListener { private static final String TAG = PlayService.class.getSimpleName(); private SensorManager mSensorManager; private MediaPlayer mPlayer; private OnMusicEventListener mListener; private int mPlayingPosition; // 当前正在播放 private WakeLock mWakeLock = null;//获取设备电源锁,防止锁屏后服务被停止 private boolean isShaking; private Notification notification;//通知栏 private RemoteViews remoteViews;//通知栏布局 private NotificationManager notificationManager; // 单线程池 private ExecutorService mProgressUpdatedListener = Executors .newSingleThreadExecutor(); public class PlayBinder extends Binder { public PlayService getService() { return PlayService.this; } } @Override public IBinder onBind(Intent intent) { mSensorManager.registerListener(mSensorEventListener, mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), SensorManager.SENSOR_DELAY_GAME); return new PlayBinder(); } @SuppressWarnings("deprecation") @Override public void onCreate() { super.onCreate(); acquireWakeLock();//获取设备电源锁 mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); MusicUtils.initMusicList(); mPlayingPosition = (Integer) SpUtils.get(this, Constants.PLAY_POS, 0); Uri uri = Uri.parse(MusicUtils.sMusicList.get( getPlayingPosition()).getUri()); mPlayer = MediaPlayer.create(PlayService.this,uri); mPlayer.setOnCompletionListener(this); // 开始更新进度的线程 mProgressUpdatedListener.execute(mPublishProgressRunnable); /** * 该方法虽然被抛弃过时,但是通用! */ PendingIntent pendingIntent = PendingIntent .getActivity(PlayService.this, 0, new Intent(PlayService.this, PlayActivity.class), 0); remoteViews = new RemoteViews(getPackageName(), R.layout.play_notification); notification = new Notification(R.drawable.ic_launcher, "歌曲正在播放", System.currentTimeMillis()); notification.contentIntent = pendingIntent; notification.contentView = remoteViews; //标记位,设置通知栏一直存在 notification.flags =Notification.FLAG_ONGOING_EVENT; Intent intent = new Intent(PlayService.class.getSimpleName()); intent.putExtra("BUTTON_NOTI", 1); PendingIntent preIntent = PendingIntent.getBroadcast( PlayService.this, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT); remoteViews.setOnClickPendingIntent( R.id.music_play_pre, preIntent); intent.putExtra("BUTTON_NOTI", 2); PendingIntent pauseIntent = PendingIntent.getBroadcast( PlayService.this, 2, intent, PendingIntent.FLAG_UPDATE_CURRENT); remoteViews.setOnClickPendingIntent( R.id.music_play_pause, pauseIntent); intent.putExtra("BUTTON_NOTI", 3); PendingIntent nextIntent = PendingIntent.getBroadcast (PlayService.this, 3, intent, PendingIntent.FLAG_UPDATE_CURRENT); remoteViews.setOnClickPendingIntent( R.id.music_play_next, nextIntent); intent.putExtra("BUTTON_NOTI", 4); PendingIntent exit = PendingIntent.getBroadcast(PlayService.this, 4, intent, PendingIntent.FLAG_UPDATE_CURRENT); remoteViews.setOnClickPendingIntent( R.id.music_play_notifi_exit, exit); notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); setRemoteViews(); /** * 注册广播接收者 * 功能: * 监听通知栏按钮点击事件 */ IntentFilter filter = new IntentFilter( PlayService.class.getSimpleName()); MyBroadCastReceiver receiver = new MyBroadCastReceiver(); registerReceiver(receiver, filter); } public void setRemoteViews(){ L.l(TAG, "进入——》setRemoteViews()"); remoteViews.setTextViewText(R.id.music_name, MusicUtils.sMusicList.get( getPlayingPosition()).getTitle()); remoteViews.setTextViewText(R.id.music_author, MusicUtils.sMusicList.get( getPlayingPosition()).getArtist()); Bitmap icon = MusicIconLoader.getInstance().load( MusicUtils.sMusicList.get( getPlayingPosition()).getImage()); remoteViews.setImageViewBitmap(R.id.music_icon,icon == null ? ImageTools.scaleBitmap(R.drawable.ic_launcher) : ImageTools .scaleBitmap(icon)); if (isPlaying()) { remoteViews.setImageViewResource(R.id.music_play_pause, R.drawable.btn_notification_player_stop_normal); }else { remoteViews.setImageViewResource(R.id.music_play_pause, R.drawable.btn_notification_player_play_normal); } //通知栏更新 notificationManager.notify(5, notification); } @Override public int onStartCommand(Intent intent, int flags, int startId) { startForeground(0, notification);//让服务前台运行 return Service.START_STICKY; } /** * 感应器的时间监听器 */ private SensorEventListener mSensorEventListener = new SensorEventListener() { @Override public void onSensorChanged(SensorEvent event) { if (isShaking) return; if (Sensor.TYPE_ACCELEROMETER == event.sensor.getType()) { float[] values = event.values; /** * 监听三个方向上的变化,数据变化剧烈,next()方法播放下一首歌曲 */ if (Math.abs(values[0]) > 8 && Math.abs(values[1]) > 8 && Math.abs(values[2]) > 8) { isShaking = true; next(); // 延迟200毫秒 防止抖动 new Handler().postDelayed(new Runnable() { @Override public void run() { isShaking = false; } }, 200); } } } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { } }; /** * 更新进度的线程 */ private Runnable mPublishProgressRunnable = new Runnable() { @Override public void run() { while (true) { if (mPlayer != null && mPlayer.isPlaying() && mListener != null) { mListener.onPublish(mPlayer.getCurrentPosition()); } /* * SystemClock.sleep(millis) is a utility function very similar * to Thread.sleep(millis), but it ignores InterruptedException. * Use this function for delays if you do not use * Thread.interrupt(), as it will preserve the interrupted state * of the thread. 这种sleep方式不会被Thread.interrupt()所打断 */SystemClock.sleep(200); } } }; /** * 设置回调 * * @param l */ public void setOnMusicEventListener(OnMusicEventListener l) { mListener = l; } /** * 播放 * * @param position * 音乐列表播放的位置 * @return 当前播放的位置 */ public int play(int position) { L.l(TAG, "play(int position)方法"); if (position < 0) position = 0; if (position >= MusicUtils.sMusicList.size()) position = MusicUtils.sMusicList.size() - 1; try { mPlayer.reset(); mPlayer.setDataSource(MusicUtils .sMusicList.get(position).getUri()); mPlayer.prepare(); start(); if (mListener != null) mListener.onChange(position); } catch (Exception e) { e.printStackTrace(); } mPlayingPosition = position; SpUtils.put(Constants.PLAY_POS, mPlayingPosition); setRemoteViews(); return mPlayingPosition; } /** * 继续播放 * * @return 当前播放的位置 默认为0 */ public int resume() { if (isPlaying()) return -1; mPlayer.start(); setRemoteViews(); return mPlayingPosition; } /** * 暂停播放 * * @return 当前播放的位置 */ public int pause() { if (!isPlaying()) return -1; mPlayer.pause(); setRemoteViews(); return mPlayingPosition; } /** * 下一曲 * * @return 当前播放的位置 */ public int next() { if (mPlayingPosition >= MusicUtils.sMusicList.size() - 1) { return play(0); } return play(mPlayingPosition + 1); } /** * 上一曲 * * @return 当前播放的位置 */ public int pre() { if (mPlayingPosition <= 0) { return play(MusicUtils.sMusicList.size() - 1); } return play(mPlayingPosition - 1); } /** * 是否正在播放 * * @return */ public boolean isPlaying() { return null != mPlayer && mPlayer.isPlaying(); } /** * 获取正在播放的歌曲在歌曲列表的位置 * * @return */ public int getPlayingPosition() { return mPlayingPosition; } /** * 获取当前正在播放音乐的总时长 * * @return */ public int getDuration() { if (!isPlaying()) return 0; return mPlayer.getDuration(); } /** * 拖放到指定位置进行播放 * * @param msec */ public void seek(int msec) { if (!isPlaying()) return; mPlayer.seekTo(msec); } /** * 开始播放 */ private void start() { mPlayer.start(); } /** * 音乐播放完毕 自动下一曲 */ @Override public void onCompletion(MediaPlayer mp) { next(); } @Override public boolean onUnbind(Intent intent) { L.l("play service", "unbind"); mSensorManager.unregisterListener(mSensorEventListener); return true; } @Override public void onRebind(Intent intent) { super.onRebind(intent); if (mListener != null) mListener.onChange(mPlayingPosition); } @Override public void onDestroy() { L.l(TAG, "PlayService.java的onDestroy()方法调用"); release(); stopForeground(true); mSensorManager.unregisterListener(mSensorEventListener); super.onDestroy(); } /** * 服务销毁时,释放各种控件 */ private void release() { if (!mProgressUpdatedListener.isShutdown()) mProgressUpdatedListener.shutdownNow(); mProgressUpdatedListener = null; //释放设备电源锁 releaseWakeLock(); if (mPlayer != null) mPlayer.release(); mPlayer = null; } // 申请设备电源锁 private void acquireWakeLock() { L.l(TAG, "正在申请电源锁"); if (null == mWakeLock) { PowerManager pm = (PowerManager) this .getSystemService(Context.POWER_SERVICE); mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, ""); if (null != mWakeLock) { mWakeLock.acquire(); L.l(TAG, "电源锁申请成功"); } } } // 释放设备电源锁 private void releaseWakeLock() { L.l(TAG, "正在释放电源锁"); if (null != mWakeLock) { mWakeLock.release(); mWakeLock = null; L.l(TAG, "电源锁释放成功"); } } private class MyBroadCastReceiver extends BroadcastReceiver{ @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals( PlayService.class.getSimpleName())) { L.l(TAG, "MyBroadCastReceiver类——》onReceive()"); L.l(TAG, "button_noti-->" +intent.getIntExtra("BUTTON_NOTI", 0)); switch (intent.getIntExtra("BUTTON_NOTI", 0)) { case 1: pre(); break; case 2: if (isPlaying()) { pause(); // 暂停 } else { resume(); // 播放 } break; case 3: next(); break; case 4: if (isPlaying()) { pause(); } //取消通知栏 notificationManager.cancel(5); break; default: break; } } if (mListener != null) { mListener.onChange(getPlayingPosition()); } } } /** * 音乐播放回调接口 */ public interface OnMusicEventListener { public void onPublish(int percent); public void onChange(int position); }}
播放歌曲的服务控制歌曲的播放,该服务功能如下:
1、通过回调接口OnMusicEventListener实现service服务与activity界面进行歌曲播放进度的更新和歌曲的切换。
2、启动了硬件加速器。通过硬件加速器控制手机摇一摇切换歌曲的功能。
3、通知栏启动提醒。
4、创建MediaPlayer进行歌曲的播放。
5、注册广播接受者,监听通知栏的点击时间。
6、获取设备电源锁。这一点请参考我的博文:实现音乐播放器后台Service服务一直存在的解决思路
另一个service服务是下载歌曲的DownloadService类,代码如下:
/** * 2015年8月15日 16:34:37 * 博文地址:http://blog.csdn.net/u010156024 */public class DownloadService extends Service { private SparseArray<Download> mDownloads = new SparseArray<Download>(); private RemoteViews mRemoteViews; public class DownloadBinder extends Binder { public DownloadService getService() { return DownloadService.this; } } @Override public IBinder onBind(Intent intent) { return new DownloadBinder(); } @Override public void onCreate() { super.onCreate(); } public void download(final int id, final String url, final String name) { L.l("download", url); Download d = new Download(id, url, MusicUtils.getMusicDir() + name); d.setOnDownloadListener(mDownloadListener).start(false); mDownloads.put(id, d); } private void refreshRemoteView() { @SuppressWarnings("deprecation") Notification notification = new Notification( android.R.drawable.stat_sys_download, "", System.currentTimeMillis()); mRemoteViews = new RemoteViews(getPackageName(), R.layout.download_remote_layout); notification.contentView = mRemoteViews; StringBuilder builder = new StringBuilder(); for(int i=0,size=mDownloads.size();i<size;i++) { builder.append(mDownloads.get(mDownloads.keyAt(i)) .getLocalFileName()); builder.append("、"); } mRemoteViews.setTextViewText(R.id.tv_download_name, builder.substring(0, builder.lastIndexOf("、"))); startForeground(R.drawable.ic_launcher, notification); } private void onDownloadComplete(int downloadId) { mDownloads.remove(downloadId); if(mDownloads.size() == 0) { stopForeground(true); return; } refreshRemoteView(); } /** * 发送广播,通知系统扫描指定的文件 * 请参考我的博文: * http://blog.csdn.net/u010156024/article/details/47681851 * */ private void scanSDCard() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // 判断SDK版本是不是4.4或者高于4.4 String[] paths = new String[]{ Environment.getExternalStorageDirectory().toString()}; MediaScannerConnection.scanFile(this, paths, null, null); } else { Intent intent = new Intent(Intent.ACTION_MEDIA_MOUNTED); intent.setClassName("com.android.providers.media", "com.android.providers.media.MediaScannerReceiver"); intent.setData(Uri.parse("file://"+ MusicUtils.getMusicDir())); sendBroadcast(intent); } } private Download.OnDownloadListener mDownloadListener = new Download.OnDownloadListener() { @Override public void onSuccess(int downloadId) { L.l("download", "success"); Toast.makeText(DownloadService.this, mDownloads.get(downloadId).getLocalFileName() + "下载完成", Toast.LENGTH_SHORT).show(); onDownloadComplete(downloadId); scanSDCard(); } @Override public void onStart(int downloadId, long fileSize) { L.l("download", "start"); refreshRemoteView(); Toast.makeText(DownloadService.this, "开始下载" + mDownloads.get(downloadId).getLocalFileName(), Toast.LENGTH_SHORT).show(); } @Override public void onPublish(int downloadId, long size) {// L.l("download", "publish" + size); } @Override public void onPause(int downloadId) { L.l("download", "pause"); } @Override public void onGoon(int downloadId, long localSize) { L.l("download", "goon"); } @Override public void onError(int downloadId) { L.l("download", "error"); Toast.makeText(DownloadService.this, mDownloads.get(downloadId).getLocalFileName() + "下载失败", Toast.LENGTH_SHORT).show(); onDownloadComplete(downloadId); } @Override public void onCancel(int downloadId) { L.l("download", "cancel"); onDownloadComplete(downloadId); } };}
该下载服务代码比较简单,主要结合Download类来实现真正的文件下载,同时实现Download类的回调接口Download.OnDownloadListener接口实现数据的传递和更新UI,Download类的代码如下:
/** * 支持断点下载 * 支持回调进度、完成、手动关闭、下载失败、暂停/继续、取得文件大小, 获取文件名 * 支持设置同时下载线程个数 * 还需要优化 * 2015年8月15日 16:34:37 * 博文地址:http://blog.csdn.net/u010156024 */public class Download implements Serializable { private static final long serialVersionUID = 0x00001000L; private static final int START = 1; // 开始下载 private static final int PUBLISH = 2; // 更新进度 private static final int PAUSE = 3; // 暂停下载 private static final int CANCEL = 4; // 取消下载 private static final int ERROR = 5; // 下载错误 private static final int SUCCESS = 6; // 下载成功 private static final int GOON = 7; // 继续下载 private static final String UA = "Mozilla/5.0 (Windows NT 6.1; WOW64)" + " AppleWebKit/537.36 (KHTML, like Gecko)" + " Chrome/37.0.2041.4 Safari/537.36"; private static ExecutorService mThreadPool;// 线程池 static { mThreadPool = Executors.newFixedThreadPool(5); // 默认5个 } private int mDownloadId; // 下载id private String mFileName; // 本地保存文件名 private String mUrl; // 下载地址 private String mLocalPath; // 本地存放目录 private boolean isPause = false; // 是否暂停 private boolean isCanceled = false; // 是否手动停止下载 private OnDownloadListener mListener; // 监听器 /** * 配置下载线程池的大小 * @param maxSize 同时下载的最大线程数 */ public static void configDownloadTheadPool(int maxSize) { mThreadPool = Executors.newFixedThreadPool(maxSize); } /** * 添加下载任务 * @param downloadId 下载任务的id * @param url 下载地址 * @param localPath 本地存放地址 */ public Download(int downloadId, String url, String localPath) { if (!new File(localPath).getParentFile().exists()) { new File(localPath).getParentFile().mkdirs(); } L.l("下载地址", url); mDownloadId = downloadId; mUrl = url; String[] tempArray = url.split("/"); mFileName = tempArray[tempArray.length-1]; mLocalPath = localPath.replaceAll("\"|\\(|\\)", ""); } /** * 设置监听器 * @param listener 设置下载监听器 * @return this */ public Download setOnDownloadListener(OnDownloadListener listener) { mListener = listener; return this; } /** * 获取文件名 * @return 文件名 */ public String getFileName() { return mFileName; } public String getLocalFileName() { String[] split = mLocalPath.split(File.separator); return split[split.length-1]; } /** * 开始下载 * params isGoon是否为继续下载 */ @SuppressLint("HandlerLeak") public void start(final boolean isGoon) { // 处理消息 final Handler handler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case ERROR: mListener.onError(mDownloadId); break; case CANCEL: mListener.onCancel(mDownloadId); break; case PAUSE: mListener.onPause(mDownloadId); break; case PUBLISH: mListener.onPublish(mDownloadId, Long.parseLong(msg.obj.toString())); break; case SUCCESS: mListener.onSuccess(mDownloadId); break; case START: mListener.onStart(mDownloadId, Long.parseLong(msg.obj.toString())); break; case GOON: mListener.onGoon(mDownloadId, Long.parseLong(msg.obj.toString())); break; } } }; // 真正开始下载 mThreadPool.execute(new Runnable() { @Override public void run() { download(isGoon,handler); } }); } /** * 下载方法 * @param handler 消息处理器 */ private void download(boolean isGoon, Handler handler) { Message msg = null; L.l("开始下载。。。"); try { RandomAccessFile localFile = new RandomAccessFile(new File(mLocalPath), "rwd"); DefaultHttpClient client = new DefaultHttpClient(); client.setParams(getHttpParams()); HttpGet get = new HttpGet(mUrl); long localFileLength = getLocalFileLength(); final long remoteFileLength = getRemoteFileLength(); long downloadedLength = localFileLength; // 远程文件不存在 if (remoteFileLength == -1l) { L.l("下载文件不存在..."); localFile.close(); handler.sendEmptyMessage(ERROR); return; } // 本地文件存在 if (localFileLength > -1l && localFileLength < remoteFileLength) { L.l("本地文件存在..."); localFile.seek(localFileLength); get.addHeader("Range", "bytes=" + localFileLength + "-" + remoteFileLength); } msg = Message.obtain(); // 如果不是继续下载 if(!isGoon) { // 发送开始下载的消息并获取文件大小的消息 msg.what = START; msg.obj = remoteFileLength; }else { msg.what = GOON; msg.obj = localFileLength; } handler.sendMessage(msg); HttpResponse response = client.execute(get); int httpCode = response.getStatusLine().getStatusCode(); if (httpCode >= 200 && httpCode <= 300) { InputStream in = response.getEntity().getContent(); byte[] bytes = new byte[1024]; int len = -1; while (-1 != (len = in.read(bytes))) { localFile.write(bytes, 0, len); downloadedLength += len; if ((int)(downloadedLength/ (float)remoteFileLength * 100) % 10 == 0) { // 发送更新进度的消息 msg = Message.obtain(); msg.what = PUBLISH; msg.obj = downloadedLength; handler.sendMessage(msg); } // 暂停下载, 退出方法 if (isPause) { // 发送暂停的消息 handler.sendEmptyMessage(PAUSE); L.l("下载暂停..."); break; } // 取消下载, 删除文件并退出方法 if (isCanceled) { L.l("手动关闭下载。。"); localFile.close(); client.getConnectionManager().shutdown(); new File(mLocalPath).delete(); // 发送取消下载的消息 handler.sendEmptyMessage(CANCEL); return; } } localFile.close(); client.getConnectionManager().shutdown(); // 发送下载完毕的消息 if(!isPause) handler.sendEmptyMessage(SUCCESS); } } catch (Exception e) { e.printStackTrace(); // 发送下载错误的消息 handler.sendEmptyMessage(ERROR); } } /** * 暂停/继续下载 * param pause 是否暂停下载 * 暂停 return true * 继续 return false */ public synchronized boolean pause(boolean pause) { if(!pause) { L.l("继续下载"); isPause = false; start(true); // 开始下载 }else { L.l("暂停下载"); isPause = true; } return isPause; } /** * 关闭下载, 会删除文件 */ public synchronized void cancel() { isCanceled = true; if(isPause) { new File(mLocalPath).delete(); } } /** * 获取本地文件大小 * @return 本地文件的大小 or 不存在返回-1 */ public synchronized long getLocalFileLength() { long size = -1l; File localFile = new File(mLocalPath); if (localFile.exists()) { size = localFile.length(); } L.l("本地文件大小" + size); return size <= 0 ? -1l : size; } /** * 获取远程文件大小 or 不存在返回-1 * @return */ public synchronized long getRemoteFileLength() { long size = -1l; try { DefaultHttpClient client = new DefaultHttpClient(); client.setParams(getHttpParams()); HttpGet get = new HttpGet(mUrl); HttpResponse response = client.execute(get); int httpCode = response.getStatusLine().getStatusCode(); if (httpCode >= 200 && httpCode <= 300) { size = response.getEntity().getContentLength(); } client.getConnectionManager().shutdown(); } catch (Exception e) { e.printStackTrace(); } L.l("远程文件大小" + size); return size; } /** * 设置http参数 不能设置soTimeout * @return HttpParams http参数 */ private static HttpParams getHttpParams() { HttpParams params = new BasicHttpParams(); HttpProtocolParams.setContentCharset(params, HTTP.UTF_8); HttpProtocolParams.setUseExpectContinue(params, true); HttpProtocolParams.setUserAgent(params, UA);// ConnManagerParams.setTimeout(params, 10000);// HttpConnectionParams.setConnectionTimeout(params, 10000); return params; } /** * 关闭下载线程池 */ public static void closeDownloadThread() { if(null != mThreadPool) { mThreadPool.shutdownNow(); } } /** * 下载过程中的监听器 * 更新下载信息 * */ public interface OnDownloadListener { public void onStart(int downloadId, long fileSize); // 回调开始下载 public void onPublish(int downloadId, long size); // 回调更新进度 public void onSuccess(int downloadId); // 回调下载成功 public void onPause(int downloadId); // 回调暂停 public void onError(int downloadId); // 回调下载出错 public void onCancel(int downloadId); // 回调取消下载 public void onGoon(int downloadId, long localSize); // 回调继续下载 }}
Download类实现文件的下载,保存到本地,使用线程池来进行文件的下载,细看看不难明白,里面没有需要特别说明的,如果大家有不明白的地方,欢迎留言,我会尽快给答复的。^_^
下一篇博文收尾,讲述播放界面的实现和最后的总结。
音乐播放器源码下载
版权声明:本文为博主原创文章,未经博主允许不得转载。