Step1:布局
整体用的是线性布局,比较适合。
音乐专辑图片用到圆形图片空间CircleImageView
<de.hdodenhof.circleimageview.CircleImageView
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:src="@drawable/img"
app:civ_border_color="#FF000000"
android:layout_margin="20dp"
android:layout_weight="10"
android:layout_gravity="center"/>
进度条用的是SeekBar:
<SeekBar
android:id="@+id/seek_bar"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintLeft_toRightOf="@id/start_time"
app:layout_constraintRight_toLeftOf="@id/end_time"
android:layout_marginHorizontal="2dp" />
其它的就是布局的一些基本功了。
Step2:创建一个服务Service
File->New->Service->Service,通过这种方法创建AS会自动配置好,比如创建名叫MusicService的服务,在AndroidManifest.xml会生成以下代码:
<service
android:name=".MusicService"
android:enabled="true"
android:exported="true">service>
在MusicService新创一个MyBinder继承Binder用于在主活动和服务之间联系:
private IBinder myBinder = new MyBinder();
public class MyBinder extends Binder{
public MusicService getService(){
return MusicService.this;
}
}
@Override
public IBinder onBind(Intent intent) {
return myBinder;
}
点击返回键活动会销毁,但如果没有指定销毁服务,服务还是不会销毁的,所以变量MediaPlayer要在服务MusicService类里面创建,并且要设为static,由此至终只有一个MediaPlayer实例,个人感觉MusicService可以设成单例模式,但查了关于音乐播放器的其它例子,没见到有用单例模式的,所以最后还是没采用了。把MediaPlayer的相关操作封装在MusicService里面:
//MusicService.java
public static MediaPlayer mediaPlayer = null;
private static String mediaPath;
public MusicService(){
//Log.d("MusicService","Gouzhao");
if(mediaPlayer == null){
mediaPlayer= new MediaPlayer();
initMediaPlayer();
}
}
//TODO :播放或暂停
public boolean playOrPause(){
Log.d("MusicService","playOrPause");
if(!mediaPlayer.isPlaying()){
mediaPlayer.start();
return true;
}else {
mediaPlayer.pause();
return false;
}
}
//TODO :设置播放位置
public void seekTo(int pos){
mediaPlayer.seekTo(pos);
}
//TODO : 返回正在播放的音频的路径
public String getMediaPath(){
return mediaPath;
}
//TODO: 停止播放音乐并将播放位置设为0
public void stop(){
mediaPlayer.stop();
try {
mediaPlayer.prepare();
}catch (Exception e){
e.printStackTrace();
}
mediaPlayer.seekTo(0);
}
@Override
public void onDestroy(){
super.onDestroy();
mediaPlayer.stop();
mediaPlayer.release();
}
//TODO :初始化播放设置
private void initMediaPlayer(){
try{
//mediaPlayer.reset();
mediaPath = Environment.getExternalStorageDirectory()+"/data/山高水长.mp3";
mediaPlayer.setDataSource(mediaPath);
mediaPlayer.prepare();
}catch (Exception e){
e.printStackTrace();
}
}
//TODO :设置播放的音频路径
public void setPlaySource(String path){
try{
mediaPlayer.reset();
mediaPlayer.setDataSource(path);
Log.d("MusicService",path);
mediaPath = path;
mediaPlayer.prepare();
}catch (Exception e){
e.printStackTrace();
}
}
Step3:圆形专辑图片的旋转
有了上面的MusicService对MediaPlayer的封装,音乐的播放,暂停,停止应该不是问题了,说一下图片的旋转,介绍几个资料:ObjectAnimator实现图片旋转,官网Animatior, 官网ObjectAnimator.
//TODO:用ObjectAnimator实现专辑图片的旋转.
objectAnimator = ObjectAnimator.ofFloat(circleImageView,"rotation",0,360);
objectAnimator.setDuration(5000); //设置旋转一周的时间
objectAnimator.setInterpolator(new LinearInterpolator()); //匀速旋转
objectAnimator.setRepeatCount(Animation.INFINITE); //设置重复旋转的次数
objectAnimator.setRepeatMode(ValueAnimator.RESTART); //设置重复模式
objectAnimator.setCurrentPlayTime(MusicService.mediaPlayer.getCurrentPosition());//设置当前播放的时间
if(MusicService.mediaPlayer.isPlaying()){
objectAnimator.start();
}
其中setCurrentPlayTime和下面的条件判断是为了按“返回”按钮后再次进入页面的时候根据播放进度重设一下旋转信息。
Step4:音乐的播放,暂停,停止以及以及相关的操作:
//TODO:播放或暂停按钮的监听事件
playImage.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent bindIntent = new Intent(MainActivity.this,MusicService.class);
startService(bindIntent);
//bindService(bindIntent,connection,BIND_AUTO_CREATE);
if(musicService.playOrPause()){
if(!objectAnimator.isStarted()){
objectAnimator.start();
}else{
objectAnimator.resume();
}
playImage.setImageResource(R.drawable.pause);
}else{
playImage.setImageResource(R.drawable.play);
objectAnimator.pause();
}
}
});
//TODO:停止播放并重置UI界面相关信息
stopImage.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
musicService.stop();
objectAnimator.end();
init();
}
});
Step5:步骤4中的init函数主要是在onCreate函数里也即刚进入主页面或者按“返回”键退出再次进入主页面后对seekBar,专辑图片,歌曲信息的正确显示。其中要用到MediaMedataRetriever解释mp3歌曲信息。
//TODO: 根据服务里的MeidaPlayer对象内容初始化活动的界面
public void init(){
seekBar.setMax(MusicService.mediaPlayer.getDuration());
seekBar.setProgress(MusicService.mediaPlayer.getCurrentPosition());
startTimeText.setText(time.format(MusicService.mediaPlayer.getCurrentPosition()));
endTimeText.setText(time.format(MusicService.mediaPlayer.getDuration()));
MediaMetadataRetriever mmr = new MediaMetadataRetriever();
mmr.setDataSource(musicService.getMediaPath());
nameText.setText(mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE)); //歌名
authorText.setText(mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST)); //歌手名字
byte[] pictureData = mmr.getEmbeddedPicture();
if(pictureData != null){
Bitmap bitmap = BitmapFactory.decodeByteArray(pictureData,0,pictureData.length);
circleImageView.setImageBitmap(bitmap);
}else{
circleImageView.setImageResource(R.drawable.img);
}
mmr.release();//要记得release释放
}
Step6:创建ServiceConnection实例并开启服务:
//TODO:ServiceConnection实例,用于连接主活动和服务。
connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
musicService = ((MusicService.MyBinder)service).getService();
}
@Override
public void onServiceDisconnected(ComponentName name) {
musicService = null;
}
};
开启服务用startService,我是在点击播放按钮里面开启服务的
Intent bindIntent = new Intent(MainActivity.this,MusicService.class);
startService(bindIntent);
结束服务用stopService,在退出按钮点击事件里面实现:
//TODO:退出按钮,终止服务并结束进程
backImage.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
musicService.stop();
Intent bindIntent = new Intent(MainActivity.this,MusicService.class);
stopService(bindIntent);
//unbindService(connection);
finish();
System.exit(0);
}
});
用bindService实现后台播放好像不行,有资料说bindService后服务的生命周期是依赖于开启服务的活动的生命周期的,也就是说但点击“返回”按钮退出后,服务也结束了。
Step7:进度条的监听函数,很简单:
//TODO:设置SeekBar的进度条变化监听函数
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if(fromUser){
musicService.seekTo(progress);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
Step8:开启一个线程,当播放时对进度条和时间进行更新:
new Thread(new SeekBarThread()).start();
//TODO:开启一个线程,当播放音乐时更新进度条的进度,由于要涉及到UI的修改,所以用到Handler
class SeekBarThread implements Runnable{
@Override
public void run(){
while(musicService!=null){
Message message = new Message();
message.what = 0;
message.obj = MusicService.mediaPlayer.getCurrentPosition();
handler.sendMessage(message);
try{
Thread.sleep(800);
}catch (Exception e){
e.printStackTrace();
}
}
}
}
//TODO:Handler实例,对UI进行修改
@SuppressLint("HandlerLeak")
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg){
super.handleMessage(msg);
switch (msg.what){
case 0:
seekBar.setProgress((int)msg.obj);
startTimeText.setText(time.format(msg.obj));
break;
default:break;
}
}
};
加分项: 本地选择音频播放,和选择本地图片库的图片差不多,对于歌曲信息读取上面有介绍:
//TODO:选择播放音频文件,要先申请权限
fileImage.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},RESULT_LOAD_AUDIO);
}
Intent i = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI);
startActivityForResult(i,RESULT_LOAD_AUDIO);
}
});
@Override
protected void onActivityResult(int requestCode,int resultCode,Intent data){
super.onActivityResult(requestCode,resultCode,data);
if(requestCode == RESULT_LOAD_AUDIO && resultCode == RESULT_OK && data != null){
Uri selectAudioUri = data.getData();
String[] filePathColumn = {MediaStore.Audio.Media.DATA};
Cursor cursor = getContentResolver().query(selectAudioUri,filePathColumn,null,null,null);
cursor.moveToLast();
String audioPath = cursor.getString(cursor.getColumnIndex(filePathColumn[0]));
cursor.close();
// Log.d("MainActivity",audioPath);
musicService.setPlaySource(audioPath);
objectAnimator.end(); //停止图片旋转
init(); //重设界面
}
}
1.觉得难点还是在怎么实现后台播放的功能
查了不少资料,实现的方法与实现的程度也是参差不齐,最后还是自己总结了一下:
2.另一个小难点是图片的旋转,用到ObjectAnimator来实现