实验目的

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

文章目录

    • 效果预览
    • 布局
      • 进度条的布局
      • 圆形ImageView
    • Service的使用
      • 如何启动`Service`
      • 注册Service
      • 创建Service
      • 绑定Service
    • MediaPlayer的使用
      • 添加文件访问权限
        • 静态添加
        • 动态添加
      • onServiceConnected的补充
        • 毫秒格式化为时间显示
      • 播放/暂停按钮
      • 停止按钮
      • 图片的旋转
      • 拖动条事件
    • Handler的使用
      • 创建新线程
      • 创建Handler
    • 退出播放器
    • 后台播放
    • 选择歌曲播放
    • 解决一些细节问题
      • 点击停止按钮UI不复位

效果预览

布局

进度条的布局

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

  • 如果使用match_parent,右边的全部时间又显示不了
  • 如果使用wrap_content,又不能填充满
  • 如果自定义dp值,不同尺寸显示又会不一样

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

圆形ImageView

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

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

  2. xml中使用

    <de.hdodenhof.circleimageview.CircleImageView    xmlns: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 {        @Override        protected boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException {            switch (code) {                //service solve                case 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方法

@Overridepublic 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方法

@Overridepublic void onDestroy() {    super.onDestroy();    if(mp!= null){        //release之前一定要reset,不然会报下面的错        //W/MediaPlayer(7564): mediaplayer went away with unhandled events        mp.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() {    @Override    public void onServiceConnected(ComponentName name, IBinder service) {        mBinder = service;        //绑定成功时进行的操作        //还有一些没有写出来    }    @Override    public 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;@Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);//获取权限        ActivityCompat.requestPermissions(this, PERMISSIONS_STORAGE, REQUEST_PERMISSION_CODE);......}//成功获取权限的回调方法,在这里@Overridepublic 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;@Overridepublic 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() {    @Override    public 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() {    @Override    public 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() {    @Override    public 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() );                    }                }    }        @Override    public void onStartTrackingTouch(SeekBar seekBar) {    }    @Override    public void onStopTrackingTouch(SeekBar seekBar) {    }});

Handler的使用

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

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

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

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

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

创建新线程

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

public Runnable  myThread = new Runnable() {        @Override        public 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(){        @Override        public void handleMessage(Message msg) {            super.handleMessage(msg);            switch (msg.what) { // 根据消息类型进行操作          //待补充                default:                    //播放结束后,模拟点击一次停止按键,置位UI                     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);            }        }    };

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

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

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

退出播放器

首先要重写onDestory方法

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

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

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

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

很简单,一句代码就够了

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

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

后台播放

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

点击home键能实现后台播放

点击back键却结束了播放

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

重写点击返回键的方法

@Overridepublic 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() {            @Override            public 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() {        @Override        public 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(){        @Override        public 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() {            @Override            public 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() {            @Override            public 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);以后再也不会使用了,应该会被回收掉

更多相关文章

  1. Android(安卓)Jetpack 之 LifeCycle
  2. android通过蓝牙实现两台手机传输数据
  3. Android(安卓)获取判断是否有悬浮窗权限的方法
  4. Android简单记录和恢复ListView滚动位置的方法
  5. Android(安卓)使用PDF.js浏览pdf的方法示例
  6. Android中Bitmap用法实例分析
  7. Android基于SOAP标准调用Webservice实现数据交互
  8. Android(安卓)编程下的TraceView 简介及其案例实战
  9. android 使用 service 实现音乐

随机推荐

  1. Android数据存储—使用SQLite数据库
  2. 使用LocalBroadcastManager
  3. Android简易实战教程--第十一话《获取手
  4. Android(安卓)实现截图功能
  5. android开发基础学习―按钮事件
  6. android app项目启动时的架构搭建
  7. [Android]百度地图之地图标注
  8. Android(安卓)左右滑动切换页面或Activit
  9. ubuntu10.10 编译android2.3源码 sdk adt
  10. 将Android(安卓)Studio默认布局Constrain