实验目的

  1. 学会使用MediaPlayer
  2. 学会简单的多线程编程,使用Handler更新UI
  3. 学会使用Service进行后台工作
  4. 学会使用Service与Activity进行通信

效果预览

布局

进度条的布局

如何实现让进度条占满当前时间全部时间中间的部分呢?

  • 如果使用match_parent,右边的全部时间又显示不了

  • 如果使用wrap_content,又不能填充满

  • 如果自定义dp值,不同尺寸显示又会不一样

这就利用了LinearLayout的特点,只需设置中间进度条的layout_weight = 1就好了,它就会自动延伸到右边最远处(不占据别的控件)

圆形ImageView

这里使用了github上的开源控件:链接

  1. 添加依赖implementation 'de.hdodenhof:circleimageview:2.2.0'

  2. xml中使用

    <de.hdodenhof.circleimageview.CircleImageViewxmlns:app="http://schemas.android.com/apk/res-auto"android:id="@+id/profile_image"android:layout_width="match_parent"android:layout_height="290dp"android:layout_marginTop="30dp"android:src="@drawable/img"app:layout_constraintTop_toTopOf="parent" />
    

Service的使用

Service(服务)是一种可以在后台执行长时间运行操作而没有用户界面的应用组件。

服务可由其他应用组件启动(如Activity),服务一旦被启动将在后台一直运行,即使启动服务的组件(Activity
已销毁也不受影响。

如何启动Service

  1. 通过startService启动

    startService()启动和stopService()关闭服务,Service与访问者之间基本不存在太多关联,因此Service和访问者之间无法通讯和数据交换。

  2. 通过bindService启动

    用于Service和访问者之间需要进行方法调用或数据交换的情况

注册Service

manifests里面的application里,添加

<service android:name=".MusicService" android:exported="true"/>

创建Service

右键 -> New -> Service -> Service,取名为MusicService(跟注册时一致)

Service里添加成员

//用来跟Activity进行绑定
public final IBinder binder = new MyBinder();
//媒体播放类
public MediaPlayer mp =  new MediaPlayer();
//对Service控制的不同数字码
private final int PLAY_CODE = 1, STOP_CODE = 2, SEEK_CODE = 3, NEWMUSIC_CODE = 4, CURRENTDURATION_CODE = 5, TOTALDURATION = 6;
//重写onTransact方法,对不同的CODE做出不同的反应
public class MyBinder extends Binder {@Overrideprotected boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException {switch (code) {//service solvecase PLAY_CODE:play_pause();break;case STOP_CODE:stop();break;case SEEK_CODE:mp.seekTo(data.readInt());break;case NEWMUSIC_CODE:newMusic(Uri.parse(data.readString()));reply.writeInt(mp.getDuration());case TOTALDURATION:reply.writeInt(mp.getDuration());break;case CURRENTDURATION_CODE:reply.writeInt(mp.getCurrentPosition());break;}return super.onTransact(code, data, reply, flags);}}

这里的Environment.getExternalStorageDirectory()指的外部存储不是扩展卡中的存储,而是相对程序来讲,不是程序的InternalStorage,而是本机的通用存储

重写onBind方法

@Override
public IBinder onBind(Intent intent) {try {mp.setDataSource(Environment.getExternalStorageDirectory() + "/data/山高水长.mp3");mp.prepare();} catch (IOException e) {Log.e("prepare error", "getService: " + e.toString());}return binder;
}

这里返回的binder就相当于一个Service组件所返回的代理对象,Service
许客户端通过该IBinder对象来访问Service内部的数据,实现客户端与Service之间的通信

编写一些简单的控制方法

public void play_pause() {if (mp.isPlaying()) {mp.pause();} else {mp.start();}}public void stop() {if (mp != null) {mp.stop();try {mp.prepare();mp.seekTo(0);} catch (Exception e) {Log.d("stop", "stop: " + e.toString());}}}public void newMusic(Uri uri){try{mp.reset();mp.setDataSource(this, uri);mp.prepare();}catch (Exception e){Log.d("New Music", "new music: " + e.toString());}}

重写onDestory方法

@Override
public void onDestroy() {super.onDestroy();if(mp!= null){//release之前一定要reset,不然会报下面的错//W/MediaPlayer(7564): mediaplayer went away with unhandled eventsmp.reset();mp.release();}
}

绑定Service

MainActivity中添加成员变量

private IBinder mBinder;
private final int PLAY_CODE = 1, STOP_CODE = 2, SEEK_CODE = 3, NEWMUSIC_CODE = 4, CURRENTDURATION_CODE = 5, TOTALDURATION = 6;
private int total_duration = 0;

添加绑定成功的操作

private ServiceConnection sc = new ServiceConnection() {@Overridepublic void onServiceConnected(ComponentName name, IBinder service) {mBinder = service;//绑定成功时进行的操作//还有一些没有写出来}@Overridepublic void onServiceDisconnected(ComponentName name) {ms = null;//结束绑定时进行的操作}
};

使用下面代码进行绑定

Intent intent = new Intent(this, MusicService.class);
bindService(intent, sc, BIND_AUTO_CREATE);

这就像用Intent开启一个Activity一样,只不过这里是开启Service

但是具体的操作还是在onServiceConnected中完成

MediaPlayer的使用

绑定好了之后就可以通过ms访问mp

但是现在直接调用ms.play()会报错

因为没有权限去访问预先设置的音频文件

添加文件访问权限

Android6.0之前,我们只需要在AndroidManifest.xml文件中直接添加权限即可

但是在Android6.0之后,我们只在AndroidManifest.xml文件中配置是不够的,还需要在Java代码中进行动态获取权限。

静态添加

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

动态添加

/读写权限
private static String[] PERMISSIONS_STORAGE = {Manifest.permission.READ_EXTERNAL_STORAGE,Manifest.permission.WRITE_EXTERNAL_STORAGE};
//请求状态码
private static int REQUEST_PERMISSION_CODE = 1;
@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);//获取权限ActivityCompat.requestPermissions(this, PERMISSIONS_STORAGE, REQUEST_PERMISSION_CODE);......
}
//成功获取权限的回调方法,在这里
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {super.onRequestPermissionsResult(requestCode, permissions, grantResults);if (requestCode == REQUEST_PERMISSION_CODE) {//其实应该放在这里开始绑定,因为只有获取权限成功后绑定才能正确让音乐播放Intent intent = new Intent(this, MusicService.class);bindService(intent, sc, BIND_AUTO_CREATE);}
}

onServiceConnected的补充

绑定成功后,需要更新进度条的长度,音乐总时间和当前进度(初始化为0)

//记得在onCreate外面添加类成员变量
private final int PLAY_CODE = 1, STOP_CODE = 2, SEEK_CODE = 3, NEWMUSIC_CODE = 4, CURRENTDURATION_CODE = 5, TOTALDURATION = 6;
private int total_duration = 0;@Override
public void onServiceConnected(ComponentName name, IBinder service) {mBinder = service;//与服务通信Parcel data = Parcel.obtain();Parcel reply = Parcel.obtain();try {mBinder.transact(TOTALDURATION, data, reply, 0);}catch (Exception e){Log.d("SERVICE CONNECTION", "onServiceConnected: " + e.toString());}total_duration = reply.readInt();seekbar.setProgress(0);seekbar.setMax(total_duration);//两种方法实现毫秒转时间//total_time.setText(time.format(new Date(ms.mp.getDuration())));total_time.setText(DateFormat.format(time_format, total_duration));current_time.setText(time.format(new Date(0)));
}

毫秒格式化为时间显示

ms.mp.getDuration()返回的是一个long型的歌曲毫秒数,需要格式化显示为时间的形式,如:12:12

  • 方法一:

    private SimpleDateFormat time = new SimpleDateFormat("mm:ss");
    total_time.setText(time.format(new Date(ms.mp.getDuration())));
    
  • 方法二:

    private String time_format = "mm:ss";
    total_time.setText(DateFormat.format(time_format, (long)ms.mp.getDuration()));
    

播放/暂停按钮

play_pause.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {//与服务通信Parcel data = Parcel.obtain();Parcel reply = Parcel.obtain();try{mBinder.transact(PLAY_CODE, data, reply, 0);}catch (RemoteException e){Log.e("STOP:", "onClick: " + e.toString() );}if(isPlay){isPlay = false;play_pause.setImageResource(R.mipmap.play);}else {isPlay = true;isStop = false;play_pause.setImageResource(R.mipmap.pause);//开始监听myThread.run();}}
});

停止按钮

stop.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {isPlay = false;isStop = true;play_pause.setImageResource(R.mipmap.play);seekbar.setProgress(0);current_time.setText(time.format(0));imageView.setRotation(0);//与服务通信Parcel data = Parcel.obtain();Parcel reply = Parcel.obtain();try{mBinder.transact(STOP_CODE, data, reply, 0);}catch (RemoteException e){Log.e("STOP:", "onClick: " + e.toString() );}}
});

图片的旋转

有很多方法,这里用一种比较简单的设置角度的方法

imageView.setPivotX(imageView.getWidth()/2);
imageView.setPivotY(imageView.getHeight()/2);//支点在图片中心
//表示顺时针旋转90度
imageView.setRotation(90);

拖动条事件

seekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {@Overridepublic void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {//判断是否来自用户,因为后面还要设置播放的时候进度条跟着变化,如果那样的变化也调用这个方法的话进度条将会卡住if(fromUser){imageView.setPivotX(imageView.getWidth()/2);imageView.setPivotY(imageView.getHeight()/2);//支点在图片中心//progress表示毫秒数,非常大,所以转化为比较容易观察的数据imageView.setRotation(progress/30);current_time.setText(time.format(progress));//与服务通信Parcel data = Parcel.obtain();Parcel reply = Parcel.obtain();data.writeInt(progress);try{mBinder.transact(SEEK_CODE, data, reply, 0);}catch (RemoteException e){Log.e("STOP:", "onClick: " + e.toString() );}}}@Overridepublic void onStartTrackingTouch(SeekBar seekBar) {}@Overridepublic void onStopTrackingTouch(SeekBar seekBar) {}
});

Handler的使用

现在已经实现了最基础的播放、暂停、停止、拖动播放功能

但是还需要播放时更新当前进度,更新进度条,旋转封面

因为要一直监听着MediaPlayer的进度,然而它又没有onProgressChangeListener方法可以用

所以需要单独用一个线程来观察它的变化,然后使用Handler通知UI变化

为什么不用主线程,因为它的本质就是一个循环,观察UI线程,抽不出身来观察别的东西了

创建新线程

有两种方法,要么直接继承Thread类,要么实现Runnable方法,只要实现了run方法就好了

public Runnable  myThread = new Runnable() {@Overridepublic void run() {Message msg = handler.obtainMessage();//待补充try{//与服务通信Parcel data = Parcel.obtain();Parcel reply = Parcel.obtain();mBinder.transact(CURRENTDURATION_CODE, data, reply, 0);msg.arg1 = reply.readInt();}catch (Exception e){Log.d("Run", "run: " + e.toString());return;}handler.sendMessage(msg);}};

创建Handler

@SuppressLint("HandlerLeak")private final Handler handler = new Handler(){@Overridepublic void handleMessage(Message msg) {super.handleMessage(msg);switch (msg.what) { // 根据消息类型进行操作//待补充default://播放结束后,模拟点击一次停止按键,置位UIif(msg.arg1 >= total_duration)stop.performClick();seekbar.setProgress(msg.arg1);current_time.setText(time.format(new Date(msg.arg1)));imageView.setPivotX(imageView.getWidth()/2);imageView.setPivotY(imageView.getHeight()/2);//支点在图片中心imageView.setRotation(msg.arg1/30);handler.postDelayed(myThread, 1);}}};

听说这样子可能会造成内存泄漏,不过我感觉这个应用应该不会有这个问题,如果有的话,尝试去把Activity设置为单实例

<activityandroid:name=".MainActivity"android:launchMode="singleInstance">

现在就能实现播放时进度条移动,封面旋转以及播放时间更新了!

退出播放器

首先要重写onDestory方法

虽然Service可以在后台服务,可是也保不准什么时候被系统杀掉,我的手机播放完一首歌就会被系统kill了

如果没有在onDestory方法中处理好资源的释放,就会弹出恼人的“应用已停止”出错对话框

@Override
public void onDestroy(){super.onDestroy();handler.removeCallbacks(myThread);if(sc != null){//解绑unbindService(sc);}
}

现在返回键和主页键都已经结束不了这个播放器了,点击退出按钮要怎么结束呢?

很简单,一句代码就够了

quit.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {finish();}
});

之前我还在里面手动调用了onDestory,其实finish方法之后系统会自动调用onDestory销毁当前Activity,所以多次调用会出错提示:“java.lang.IllegalStateException: No activity”,因为第一遍已经销毁了,第二遍已经不存在这个Activity

后台播放

其实这个已经使用了Service,程序进入后台之后应该还是会继续播放,然而结果并不是这样子

点击home键能实现后台播放

点击back键却结束了播放

想了想,应该是系统觉得我的应用太简单,点击返回就可以直接杀掉了,所以得制止这种行为!

重写点击返回键的方法

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {if (keyCode==KeyEvent.KEYCODE_BACK){moveTaskToBack(true);return false;}return super.onKeyDown(keyCode, event);
}

return true:返回键是回到上一个activity

return false:后者会直接最小化应用,重新进入应用之后首先就会看到你所操作的这个avtivity!

选择歌曲播放

刚刚已经获取了文件访问权限,现在就可以直接打开文件选择窗口了

这里筛选文件类型为音频

select.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {Intent intent = new Intent(Intent.ACTION_GET_CONTENT);intent.setType("audio/*");intent.addCategory(Intent.CATEGORY_OPENABLE);startActivityForResult(intent,1);}});

重写选择文件后的返回事件

protected void onActivityResult(int requestCode, int resultCode, Intent data) {if (data != null) {try{//与服务通信Parcel send_data = Parcel.obtain();Parcel reply = Parcel.obtain();send_data.writeString(data.getData().toString());try{mBinder.transact(NEWMUSIC_CODE, send_data, reply, 0);}catch (RemoteException e){Log.e("STOP:", "onClick: " + e.toString() );}//设置信息seekbar.setProgress(0);seekbar.setMax(ms.mp.getDuration());total_time.setText(DateFormat.format(time_format, (long)ms.mp.getDuration()));current_time.setText(time.format(new Date(0)));//......//待补充}catch (Exception e){Log.d("Open file", "onActivityResult: " + e.toString());}}super.onActivityResult(requestCode, resultCode, data);
}

这里要注意的是,重新设置播放源文件的时候需要先reset,但是不能release

不过也可以release,然后ms.mp = new MediaPlayer();就好了,虽然充分利用了JAVA的垃圾回收机制,不过总感觉这样不是很稳妥

在返回事件里还得重新解析音乐的信息

MediaMetadataRetriever mmr = new MediaMetadataRetriever();
mmr.setDataSource(MainActivity.this,data.getData());
//获取媒体标题
music_title.setText(mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE));
//获取媒体艺术家
music_singer.setText(mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST));
//获取媒体图片(专辑封面)
byte[] picture = mmr.getEmbeddedPicture();
if(picture.length!=0){Bitmap bitmap = BitmapFactory.decodeByteArray(picture, 0, picture.length);imageView.setImageBitmap(bitmap);
}
//释放
mmr.release();
//自动播放
isPlay = false;
play_pause.performClick();
//不自动播放
//模拟停止
//stop.performClick();

名称: mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE))

专辑: mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM))

歌手: mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST))

码率: mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE))

时长:mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION))

类型:mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE))

解决一些细节问题

点击停止按钮UI不复位

点击停止按钮之后进度条、当前播放时间和封面都没有复位,但是再次点击播放按钮之后还是会会重新开始

调试的时候handler.removeCallbacks(myThread);确实起了作用,而且UI也都复位了,但是正常运行的时候却不行,也许是线程的时间间隔太短吧……

那就再想个办法!

新建一个变量指示当前是否是已经停止的状态(需要通知UI复位)

boolean isStop = false;

修改myThread

public Runnable  myThread = new Runnable() {@Overridepublic void run() {Message msg = handler.obtainMessage();if(isStop){msg.what = -1;handler.sendMessage(msg);return;}try{//与服务通信Parcel data = Parcel.obtain();Parcel reply = Parcel.obtain();mBinder.transact(CURRENTDURATION_CODE, data, reply, 0);msg.arg1 = reply.readInt();}catch (Exception e){Log.d("Run", "run: " + e.toString());return;}handler.sendMessage(msg);}};

修改handler

@SuppressLint("HandlerLeak")private final Handler handler = new Handler(){@Overridepublic void handleMessage(Message msg) {super.handleMessage(msg);switch (msg.what) { // 根据消息类型进行操作case -1:handler.removeCallbacks(myThread);seekbar.setProgress(0);current_time.setText(time.format(0));imageView.setRotation(0);break;default:if(msg.arg1 >= total_duration)stop.performClick();seekbar.setProgress(msg.arg1);current_time.setText(time.format(new Date(msg.arg1)));imageView.setPivotX(imageView.getWidth()/2);imageView.setPivotY(imageView.getHeight()/2);//支点在图片中心imageView.setRotation(msg.arg1/30);handler.postDelayed(myThread, 1);}}};

修改按钮监听事件

play_pause.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {//与服务通信Parcel data = Parcel.obtain();Parcel reply = Parcel.obtain();try{mBinder.transact(PLAY_CODE, data, reply, 0);}catch (RemoteException e){Log.e("STOP:", "onClick: " + e.toString() );}if(isPlay){isPlay = false;play_pause.setImageResource(R.mipmap.play);}else {isPlay = true;isStop = false;play_pause.setImageResource(R.mipmap.pause);//开始监听myThread.run();}}});stop.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {isPlay = false;isStop = true;play_pause.setImageResource(R.mipmap.play);seekbar.setProgress(0);current_time.setText(time.format(0));imageView.setRotation(0);//与服务通信Parcel data = Parcel.obtain();Parcel reply = Parcel.obtain();try{mBinder.transact(STOP_CODE, data, reply, 0);}catch (RemoteException e){Log.e("STOP:", "onClick: " + e.toString() );}}});

这样子的话

如果按了停止按钮 ->

isStop就会置位true ->

myThread知道之后就会发送值为-1的msg.what ->

handler接收到就会让所有UI复位

然后重新点击按钮的话会重新开启另一个线程mThread

原来的线程因为没有调用handler.postDelayed(myThread, 1);以后再也不会使用了,应该会被回收掉

Android手机应用开发(八) | 制作简单音乐播放器相关推荐

  1. 【C++】QT制作简单音乐播放器

    QT音乐播放器Mymusic 整体布局 写ui文件: Headers Sources 整体布局 创建QT项目,使用Qt5.9版本 分为三栏,最上面放标题和图标,中间放播放的列表,最下面放控制按钮. 引 ...

  2. android音乐播放器实现,Android实现简单音乐播放器(MediaPlayer)

    Android实现简单音乐播放器(MediaPlayer),供大家参考,具体内容如下 开发工具:Andorid Studio 1.3 运行环境:Android 4.4 KitKat 工程内容 实现一个 ...

  3. JAVA毕业设计vue开发一个简单音乐播放器计算机源码+lw文档+系统+调试部署+数据库

    JAVA毕业设计vue开发一个简单音乐播放器计算机源码+lw文档+系统+调试部署+数据库 JAVA毕业设计vue开发一个简单音乐播放器计算机源码+lw文档+系统+调试部署+数据库 本源码技术栈: 项目 ...

  4. 基于JAVAvue开发一个简单音乐播放器计算机毕业设计源码+数据库+lw文档+系统+部署

    基于JAVAvue开发一个简单音乐播放器计算机毕业设计源码+数据库+lw文档+系统+部署 基于JAVAvue开发一个简单音乐播放器计算机毕业设计源码+数据库+lw文档+系统+部署 本源码技术栈: 项目 ...

  5. Android复习02(ListView具体操作[很详细]、简单音乐播放器)

    2020年 3月24日 星期二 Android录播回放 笔记[腾讯课堂] https://ke.qq.com/webcourse/index.html#cid=989760&term_id=1 ...

  6. java计算机毕业设计vue开发一个简单音乐播放器(附源码、数据库)

    java计算机毕业设计vue开发一个简单音乐播放器(附源码.数据库) 项目运行 环境配置: Jdk1.8 + Tomcat8.5 + Mysql + HBuilderX(Webstorm也行)+ Ec ...

  7. java计算机毕业设计vue开发一个简单音乐播放器源码+mysql数据库+系统+lw文档+部署

    java计算机毕业设计vue开发一个简单音乐播放器源码+mysql数据库+系统+lw文档+部署 java计算机毕业设计vue开发一个简单音乐播放器源码+mysql数据库+系统+lw文档+部署 本源码技 ...

  8. java计算机毕业设计vue开发一个简单音乐播放器MyBatis+系统+LW文档+源码+调试部署

    java计算机毕业设计vue开发一个简单音乐播放器MyBatis+系统+LW文档+源码+调试部署 java计算机毕业设计vue开发一个简单音乐播放器MyBatis+系统+LW文档+源码+调试部署 本源 ...

  9. 计算机毕业设计Javavue开发一个简单音乐播放器(源码+系统+mysql数据库+lw文档)

    计算机毕业设计Javavue开发一个简单音乐播放器(源码+系统+mysql数据库+lw文档) 计算机毕业设计Javavue开发一个简单音乐播放器(源码+系统+mysql数据库+lw文档) 本源码技术栈 ...

最新文章

  1. 什么是DCI? 它有什么用?
  2. AOE网与关键路径简介
  3. DLL型后门原理及完全清除秘诀
  4. Cesium入门-2-增加地形
  5. 链接时出现LNK2001错误的解决方法
  6. 通达信公式转python为什么很难_转行数据分析为什么这么难?
  7. python通过解释器内置的open_Python 之 文件读写的学习
  8. C语言的那些小秘密之【内存分配】
  9. 文件批量改名特工 v1.0 怎么用
  10. python下载安装教程(官网)
  11. java 分解质因数
  12. Unity3D射击游戏的准心
  13. VSCode常用插件汇总
  14. 初学前端需要掌握的HTML知识点
  15. 使用EventLog类写Windows事件日志
  16. Opencv contours找出最大轮廓
  17. 锁定计算机好在下游戏吗,求一款能让电脑在某一时段锁定不能玩游戏的软件
  18. java动态创建代理对象
  19. FTP文件夹打开错误,Windows无法访问此文件夹的问题处理方法
  20. git-删除fork的项目

热门文章

  1. kingroot权限管理_KingRoot(授权管理)
  2. 总结Cocos2d-x 3.0 中新字体标签Label
  3. Java入门_主函数+数据类型
  4. 2022级-第3次作业-控制语句 7-1 计算指数
  5. IE 下载 CVS 文件问题
  6. 苹果 iPod touch 卸载应用
  7. istio 实战 三 智能路由
  8. python3+robotframework+selenium3 浏览器兼容性测试
  9. canny边缘检测 阈值调整
  10. 与“老大”一起谈软件行业