学习Android有一个多月,看完了《第一行代码》以及mars老师的第一期视频通过音乐播放器小项目加深对知识点的理解。从本文开始,将详细的介绍简单仿多米音乐播放器的实现,以及网络解析数据获取百度音乐最新排行音乐以及下载功能。
功能介绍如下:
1、获取本地歌曲列表,实现歌曲播放功能。
2、利用jsoup解析网页数据,从网络获取歌曲列表,同时实现歌曲和歌词下载到手机本地的功能。
3、通知栏提醒,实现仿QQ音乐播放器的通知栏功能.
涉及的技术有:
1、jsoup解析网络网页,从而获取需要的数据
2、android中访问网络,获取文件到本地的网络请求技术,以及下载文件到本地实现断点下载
3、线程池
4、图片缓存
5、service一直在后台运行
6、Activity与Fragment间的切换以及通信
7、notification通知栏设计
8、自定义广播
9、android系统文件管
该播放器详细设计参考下面博文:
UI设计部分见: 【android】音乐播放器之UI设计的点点滴滴
Service服务设i:【android】音乐播放器之service服务设计
数据存储技术的运用见: 【android】音乐播放器之数据存储总结
下面是最终结果展示:
图一主界面以及本地音乐列表
图二: 在本地列表中长按菜单键可以弹出功能选择键,短按listview中的歌曲列表跳转到播放列表,长按listview中的个人去items可以歌曲的删除。
上面两幅图都关于网络列表:按listview中的歌曲items可以实现下载功能
播放界面了~!~我还是比较喜欢这个播放界面的,仿QQ音乐播放界面风格。通过左右滑动可以实现播放歌曲当前的的图片到播放歌曲歌词显示的切换
上面这两幅图分别为下载时和播放歌曲时候通知栏的显示,其余的小功能就不一一展示。如果感兴趣可以下载代码编也好改也好随你便吧,~!~hahahha!!!
这边文章主要讲讲大致的设计思路~~~~~~。
还是先谈谈service服务吧,音乐播放器用到了两个service服务:PlayService和DownLoadService。PlayService主要负责的是播放音乐的功能,当然,在启动的时候到sdcard中读取数据、动态跟新通知栏以及歌词等这些也都是在PlayService中完成的;而DownLoadService主要结合DownLoad类实现歌曲下载的功能。也是必须要考虑到服务必须要在后台长期运行的缘故,因此,这边的处理是在应用启动时候,通过application类调用它的startService方法启动service。详细情况可以参考我的博文【android】音乐播放器之service服务设计~!~哈哈哈哈哈哈,application类已经在前面提到的博文中已经贴出来这边就不再贴出相关的代码。
有了application类后可以着手去设计Activity类,很自然想到实现一个父类,把子类中复用的代码都放在BaseActivity。然后在子类中覆盖或者重写父类中的相关代码 ,代码如下:
public abstract class BaseActivity extends FragmentActivity {protected PlayService mPlayService;protected DownloadService mDownloadService;private final String TAG = BaseActivity.class.getSimpleName();private ServiceConnection mPlayServiceConnection = new ServiceConnection() {@Overridepublic void onServiceConnected(ComponentName arg0, IBinder service) {// TODO Auto-generated method stubmPlayService = ((PlayService.PlayBinder) service).getService();mPlayService.setOnMusicEventListener(mMusicEventListener);onChange(mPlayService.getPlayingPosition());}@Overridepublic void onServiceDisconnected(ComponentName arg0) {mPlayService = null;}};private ServiceConnection mDownloadServiceConnection = new ServiceConnection() {@Overridepublic void onServiceDisconnected(ComponentName name) {L.l(TAG, "download--->onServiceDisconnected");mDownloadService = null;}@Overridepublic void onServiceConnected(ComponentName name, IBinder service) {mDownloadService = ((DownloadService.DownloadBinder) service).getService();}};/** 音乐播放服务回调接口的实现类*/private PlayService.OnMusicEventListener mMusicEventListener = new PlayService.OnMusicEventListener() {@Overridepublic void onPublish(int progress) {BaseActivity.this.onPublish(progress);}@Overridepublic void onChange(int position) {BaseActivity.this.onChange(position);}};/*** Fragment的view加载完成后回调*/public void allowBindService() {bindService(new Intent(this, PlayService.class), mPlayServiceConnection,Context.BIND_AUTO_CREATE);}/*** fragment的view消失后回调*/public void allowUnbindService() {unbindService(mPlayServiceConnection);}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);bindService(new Intent(this, DownloadService.class), mDownloadServiceConnection,Context.BIND_AUTO_CREATE);}@Overrideprotected void onDestroy() {unbindService(mDownloadServiceConnection);super.onDestroy();}public DownloadService getDownloadService() {return mDownloadService;}/*** 更新进度* @param progress 进度*/public abstract void onPublish(int progress);/*** 切换歌曲* @param position 歌曲在list中的位置*/public abstract void onChange(int position);}
有了BaseActivity,可以设计子类部分。在一个准备类启动加载一个布局全屏显示一张启动封面延时两秒钟进入主界面。主界面设计仿多米音乐风格,在MainActivity中添加5个Fragment(详细的UI设计见:【android】音乐播放器之UI设计的点点滴滴),同时监听按钮实现Fragment的切换:
@Overridepublic void onClick(View v){resetImgs();switch (v.getId()){case R.id.id_tab_user:setSelect(TAB_USER);break;case R.id.id_tab_cd:setSelect(TAB_CD);break;case R.id.id_tab_search:setSelect(TAB_SEARCH);break;case R.id.id_tab_compass:setSelect(TAB_COMPASS);break;case R.id.id_tab_topjump:startActivity(new Intent(this, PlayActivity.class));break;case R.id.tv_pop_exit:stopService(new Intent(this, PlayService.class));//stopService(new Intent(this, DownloadService.class));case R.id.tv_pop_shutdown:finish();case R.id.tv_pop_cancel:if(mPopupWindow != null && mPopupWindow.isShowing()) mPopupWindow.dismiss();onPopupWindowDismiss();break;default:break;}}
其中,1、需要注意的是本地音乐列表LocalFragment的监听事件是通过在UserFragment中监听获得并调用主活动的的select方法跳转到本地音乐列表。当然,此时本地音乐列表已经初始化过~~~初始化过程主要在PlayService中启动过程完成(这个在上面提到的相关博文有详细的说明);2、在LocalFragment启动过程通过bingservice()绑定服务,这样就可以实现当点击本地音乐列表时候播放按钮同时跳转到PlayActivity播放界面。代码如下:
@Overridepublic void onStart() {super.onStart();mActivity.allowBindService();}@Overridepublic void onStop() {super.onStop();mActivity.allowUnbindService();}
@Overridepublic void onItemClick(AdapterView<?> parent, View view, int position, long id) {Intent intent = new Intent(mActivity, PlayActivity.class);intent.putExtra("pos", position);startActivity(intent);play(position);}
private void play(int position) {int pos = mActivity.getPlayService().play(position);onPlay(pos);}
同时,当长按本地音乐播放列表可以实现删除本地音乐的功能,通过listView的setOnItemLongClickListener方法监听,获取事件时调用MusicUtils的remove方法删除本地音乐,并启动扫描sdcard通过广播通知列表变化更新本地列表:
private OnItemLongClickListener mItemLongClickListener = new OnItemLongClickListener() {@Overridepublic boolean onItemLongClick(AdapterView<?> parent, View view,int position, long id) {final int pos = position;AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);builder.setTitle("删除该条目");builder.setMessage("确认要删除该条目吗?");builder.setPositiveButton("删除",new DialogInterface.OnClickListener() {public void onClick(DialogInterface dialog, int which) {Music music = MusicUtils.sMusicList.remove(pos);mMusicListAdapter.notifyDataSetChanged();if (new File(music.getUri()).delete()) {scanSDCard();}}});builder.setNegativeButton("取消", null);builder.create().show();return true;}};
private void scanSDCard() {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {// 判断SDK版本是不是4.4或者高于4.4String[] paths = new String[]{Environment.getExternalStorageDirectory().toString()};MediaScannerConnection.scanFile(mActivity, 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()));mActivity.sendBroadcast(intent);}}
跳转到PlayActivity后实现和在LocalFragment实现十分相同,绑定Playservice服务,初始化监听事件,获取事件调用服务中的方法播放音乐,具体的Ui设计部分见【android】音乐播放器之UI设计的点点滴滴。另外,注意一点是:在布局文件中介个监听听按钮设置了onclick属性:
<ImageButtonandroid:id="@+id/ib_play_pre"android:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@android:color/transparent"android:contentDescription="@string/app_name"android:onClick="pre"android:src="@drawable/player_btn_pre_normal" /><ImageButtonandroid:id="@+id/ib_play_start"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginLeft="20dp"android:layout_marginRight="20dp"android:background="@android:color/transparent"android:contentDescription="@string/app_name"android:onClick="play"android:src="@drawable/player_btn_play_normal" /><ImageButtonandroid:id="@+id/ib_play_next"android:layout_width="wrap_content"android:layout_height="wrap_content"android:background="@android:color/transparent"android:contentDescription="@string/app_name"android:onClick="next"android:src="@drawable/player_btn_next_normal" />
当在MainActivity中捕获到网络按钮时,跳转到SearchFragment网络列表,因为在BaseActivity的Oncreate方法直接绑定DownLoadService,所以在该Fragment中就没必要再去绑定服务。在SearchFragment创建后调用setUserVisibleHint方法实现Fragment实现懒加载,结合SongsRecommendation类解析百度音乐中页面获取音乐数据,显示在SearchFragment布局中,网页解析获取歌曲列表主要通过JSOUP解析技术实现(可以参考:jsoup下载地址以及jsoup中文开发指南)。代码如下:
@Overridepublic void onResume() {super.onResume();if(getUserVisibleHint()){Boolean isVisibleToUser = getUserVisibleHint();setUserVisibleHint(isVisibleToUser);}}/*** 该方法实现的功能是: 当该Fragment不可见时,isVisibleToUser=false* 当该Fragment可见时,isVisibleToUser=true* 该方法由系统调用,重写该方法实现用户可见当前Fragment时再进行数据的加载*/@Overridepublic void setUserVisibleHint(boolean isVisibleToUser) {super.setUserVisibleHint(isVisibleToUser);// 当Fragment可见且是第一次加载时if (isVisibleToUser && isFirstShown) {mSearchProgressBar.setVisibility(View.VISIBLE);mSearchResultListView.setVisibility(View.GONE);SongsRecommendation.getInstance().setListener(new SongsRecommendation.OnRecommendationListener() {@Overridepublic void onRecommend(ArrayList<SearchResult> results) {if (results == null || results.isEmpty())return;mSearchProgressBar.setVisibility(View.GONE);mSearchResultListView.setVisibility(View.VISIBLE);mResultData.clear();mResultData.addAll(results);mSearchResultAdapter.notifyDataSetChanged();}}).get();isFirstShown = false;}}
/*** 真正执行网页解析的方法* 线程池中开启新的线程执行解析,解析完成之后发送消息* 将结果传递到主线程中*/public void get() {mThreadPool.execute(new Runnable() {@Overridepublic void run() {ArrayList<SearchResult> result = getMusicList();if (result == null) {mHandler.sendEmptyMessage(Constants.FAILED);return;}mHandler.obtainMessage(Constants.SUCCESS, result).sendToTarget();}});}private ArrayList<SearchResult> getMusicList() {try {/*** 一下方法调用请参考官网* 说明:timeout设置请求时间,不宜过短。* 时间过短导致异常,无法获取。*/Document doc = Jsoup.connect(URL).userAgent("Mozilla/5.0 (Windows NT 6.1; Win64; x64)" +" AppleWebKit/537.36"+ " (KHTML, like Gecko)" +" Chrome/42.0.2311.22 Safari/537.36").timeout(60 * 1000).get();//select为选择器,请参考官网说明Elements songTitles = doc.select("span.song-title");Elements artists = doc.select("span.author_list");ArrayList<SearchResult> searchResults = new ArrayList<SearchResult>();for (int i = 0; i < songTitles.size(); i++) {SearchResult searchResult = new SearchResult();Elements urls = songTitles.get(i).getElementsByTag("a");searchResult.setUrl(urls.get(0).attr("href"));searchResult.setMusicName(urls.get(0).text());Elements artistElements = artists.get(i).getElementsByTag("a");searchResult.setArtist(artistElements.get(0).text());searchResult.setAlbum("最新推荐");searchResults.add(searchResult);}return searchResults;} catch (IOException e) {e.printStackTrace();}return null;}
具体监听点击事件开始下载功能的实现见其他几篇小博文。这边就不一一介绍了,如果感兴趣还是建议看下源码。
源码地址:音乐播放器源码
git 源码地址(免积分):音乐播放器源码
理