引言:
这样的一个音乐播放器,用到了安卓四大组件的其中三个,等于说是一个比较综合性的小功能。实现方法其实有很多,我这里给出自己的方法,不喜勿喷。
需求分析
1.音乐播放器,那我们需要一个帮助类,来构建单例音乐播放器对象:
package com.example.jackandrose.entities;import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.net.Uri;import java.io.IOException;public class MediaHelper {private static MediaHelper instance;private Context mContext;private MediaPlayer mMediaPlayer;private OnMediaHelperListener mOnMediaHelperListener;private int mResID=-5;public void setmOnMediaHelperListener(OnMediaHelperListener mOnMediaHelperListener) {this.mOnMediaHelperListener = mOnMediaHelperListener;}public static MediaHelper getInstance(Context context) {if(instance==null){synchronized (MediaHelper.class){if(instance==null){instance=new MediaHelper(context);}}}return instance;}private MediaHelper(Context context){mContext=context;mMediaPlayer=new MediaPlayer();}/*** 当播放本地uri中音时调用* @param path*/public void setPath(String path){if(mMediaPlayer.isPlaying()){mMediaPlayer.reset();}try {mMediaPlayer.setDataSource(mContext, Uri.parse(path));} catch (IOException e) {e.printStackTrace();}mMediaPlayer.prepareAsync();mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {@Overridepublic void onPrepared(MediaPlayer mp) {if(mOnMediaHelperListener !=null){mOnMediaHelperListener.onPrepared(mp);}}});mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {@Overridepublic void onCompletion(MediaPlayer mp) {if(mOnMediaHelperListener !=null){mOnMediaHelperListener.onPauseState();}}});}/*** 当调用raw下的文件时使用* @param resid*/public void setRawFile(int resid){if(resid==mResID&&mResID!=-5){//相同音乐id或者且不是第一次播放,就直接返回return;}//mOnInitMusicListener.initMode();mResID=resid;final AssetFileDescriptor afd = mContext.getResources().openRawResourceFd(resid);try {mMediaPlayer.reset();mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);mMediaPlayer.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());mMediaPlayer.prepareAsync();} catch (IOException e) {e.printStackTrace();}mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {@Overridepublic void onPrepared(MediaPlayer mp) {if(mOnMediaHelperListener !=null){mOnMediaHelperListener.onPrepared(mp);try {afd.close();} catch (IOException e) {e.printStackTrace();}}}});mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {@Overridepublic void onCompletion(MediaPlayer mp) {if(mOnMediaHelperListener !=null){mOnMediaHelperListener.onPauseState();}}});}public void start(){if(mMediaPlayer.isPlaying()){return;}mMediaPlayer.start();if(mOnMediaHelperListener!=null){mOnMediaHelperListener.onPlayingState();}}public void pause(){if(!mMediaPlayer.isPlaying()){return;}mMediaPlayer.pause();if(mOnMediaHelperListener!=null){mOnMediaHelperListener.onPauseState();}}public boolean isPlaying(){if(mMediaPlayer!=null&&mMediaPlayer.isPlaying()){return true;}return false;}public int getCurrentPosition(){return mMediaPlayer.getCurrentPosition();}public int getDuration(){return mMediaPlayer.getDuration();}public void seekTo(int progress){mMediaPlayer.seekTo(progress);}public interface OnMediaHelperListener {//音乐准备好之后调用void onPrepared(MediaPlayer mp);//音乐暂停状态void onPauseState();//音乐播放状态void onPlayingState();}}
2.首先,要实现播放器显示在通知栏里面,毫无疑问,要创建通知,以前台服务的形式显示
<1>创建通知:
package com.example.jackandrose.entities;import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.widget.RemoteViews;import androidx.core.app.NotificationCompat;import com.example.jackandrose.R;
import com.example.jackandrose.activities.Main2Activity;
import com.example.jackandrose.activities.MainActivity;public class NotifyHelper {private static NotifyHelper instance;private Context mContext;public static NotifyHelper getInstance(Context context) {if(instance==null){instance=new NotifyHelper(context);}return instance;}private NotifyHelper(Context context){mContext=context;}public void CreateChannel(String channel_id,CharSequence channel_name,String description){//8.0以上版本通知适配if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {NotificationChannel notificationChannel=new NotificationChannel(channel_id,channel_name, NotificationManager.IMPORTANCE_HIGH);notificationChannel.setDescription(description);NotificationManager notificationManager=mContext.getSystemService(NotificationManager.class);notificationManager.createNotificationChannel(notificationChannel);}}/*** 返回一个前台通知* @param channel_id 通知渠道id,注意8.0创建通知的时候渠道id与此要匹配* @param musicPicture 数据对象* @param remoteViews 自定义通知样式的对象,但是与View不同,不提供findViewById方法,详细建议看看源码和官方文档* @return*/public Notification createForeNotification(String channel_id, MusicPicture musicPicture,RemoteViews remoteViews){Intent intent=new Intent(mContext, MainActivity.class);PendingIntent mainIntent=PendingIntent.getActivity(mContext,0,intent,0);NotificationCompat.Builder builder=new NotificationCompat.Builder(mContext,channel_id).setSmallIcon(R.mipmap.qqyinyue).setStyle(new NotificationCompat.DecoratedCustomViewStyle()) .setCustomBigContentView(remoteViews).setContentIntent(mainIntent).setPriority(NotificationCompat.PRIORITY_DEFAULT);return builder.build();}
}
细心的朋友肯定发现了,我构建通知的时候picture对象没有使用到,我也不想改了,就放着吧~
需要注意的是,RemoteViews不支持部分布局和控件,ConstraintLayout,用于显示音乐进度的SeekBar,还有各种第三方库,比如CircleImageView,这些都不被支持,反正我刚开始写出来的布局,通知界面便没有正确显示。
<2>布局代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:id="@+id/cl_music_player"android:layout_width="match_parent"android:layout_height="wrap_content"android:background="@color/myActionBarColor"android:orientation="horizontal"><ImageViewandroid:id="@+id/iv_music_icon"android:layout_width="@dimen/myBigMusicIconSize"android:layout_height="@dimen/myBigMusicIconSize"android:layout_gravity="center"android:scaleType="fitXY"android:layout_marginLeft="16dp"android:src="@mipmap/qqyinyue" /><LinearLayoutandroid:orientation="vertical"android:layout_weight="1"android:layout_width="0dp"android:layout_height="wrap_content"><FrameLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"android:layout_weight="1"><ImageViewandroid:id="@+id/iv_music_last_song"android:layout_width="@dimen/myMusicIconSize"android:layout_height="@dimen/myMusicIconSize"android:layout_margin="16dp"android:layout_gravity="left|center_vertical"android:src="@mipmap/music_last" /><ImageViewandroid:id="@+id/iv_music_play"android:layout_width="@dimen/myIconSize"android:layout_height="@dimen/myIconSize"android:layout_margin="16dp"android:src="@mipmap/play_music"android:layout_gravity="center"/><ImageViewandroid:id="@+id/iv_music_next_song"android:layout_width="@dimen/myMusicIconSize"android:layout_height="@dimen/myMusicIconSize"android:layout_margin="16dp"android:src="@mipmap/music_next"android:layout_gravity="right|center_vertical"/></FrameLayout></LinearLayout>
</LinearLayout>
<3>创建主服务,用于显示播放器和控制整个音乐播放的服务,并且这里面实现了音乐播放帮助类的外放接口:
package com.example.jackandrose.services;import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.MediaPlayer;
import android.os.Binder;
import android.os.IBinder;
import android.widget.RemoteViews;import com.example.jackandrose.R;import com.example.jackandrose.broadcasts.MyBroadCastReceiver;
import com.example.jackandrose.entities.MediaHelper;
import com.example.jackandrose.entities.MusicPicture;
import com.example.jackandrose.entities.NotifyHelper;public class MyService extends Service {/*** 此服务,用于展示音乐播放器,并实现音乐播放* 在此说明一下我们音乐播放器的实现* 1. 整体界面使用通知实现前台服务* 2. 上面的图片按钮操作选择了另外的Service和BroadCast* 3. 操作方面其实后台的话,服务和广播都是不错的选择* 4. 切换歌曲我选择了广播,暂停和继续播放我选择了另外的服务*/private static final String CHANNEL_ID = "music_channel_id";private static final String CHANNEL_NAME = "music_channel_name";private static final String CHANNEL_DESCRIPTION = "music_channel_description";private static final int NOTIFY_ID = 0x1;private NotifyHelper mNotifyHelper;private MediaHelper mMediaHelper;private RemoteViews notifyLayout;private MyBroadCastReceiver mMyBroadCastReceiver;private static final boolean MODE_PLAY = true;private static final boolean MODE_PAUSE = false;private boolean flag;@Overridepublic void onCreate() {super.onCreate();mMediaHelper = MediaHelper.getInstance(this);mNotifyHelper = NotifyHelper.getInstance(this);notifyLayout = new RemoteViews(MyService.this.getPackageName(), R.layout.player_layout);mMyBroadCastReceiver=MyBroadCastReceiver.getInstance();flag=MODE_PAUSE;mRegistBroadCast();}@Overridepublic void onDestroy() {super.onDestroy();unregisterReceiver(mMyBroadCastReceiver);}/*** 动态注册广播,因为8.0版本以后静态注册可能不起作用*/private void mRegistBroadCast() {IntentFilter intentFilter=new IntentFilter();intentFilter.addAction("com.example.jackandrose.broadcasts.PLAY_LAST");intentFilter.addAction("com.example.jackandrose.broadcasts.PLAY_NEXT");registerReceiver(mMyBroadCastReceiver,intentFilter);}public MyService() {}@Overridepublic IBinder onBind(Intent intent) {// TODO: Return the communication channel to the service.return new MyBinder();}public class MyBinder extends Binder {MyBinder() {super();}public MyService getService() {return MyService.this;}public void setMusic(final MusicPicture picture) {mMediaHelper.setmOnMediaHelperListener(new MediaHelper.OnMediaHelperListener() {@Overridepublic void onPrepared(MediaPlayer mp) {flag=MODE_PAUSE;playMusicState(picture);mMediaHelper.start();}@Overridepublic void onPauseState() {pauseMusicState(picture);}@Overridepublic void onPlayingState() {playMusicState(picture);}});changeMusic();mMediaHelper.setRawFile(picture.getPic_resource());}private void changeMusic() {/*** 发送广播,进行切歌到上一首操作*/Intent lastIntent=new Intent("com.example.jackandrose.broadcasts.PLAY_LAST");PendingIntent lastPendingIntent=PendingIntent.getBroadcast(MyService.this,0,lastIntent,0);notifyLayout.setOnClickPendingIntent(R.id.iv_music_last_song,lastPendingIntent);/*** 发送广播,进行切歌到下一首操作*/Intent nextIntent=new Intent("com.example.jackandrose.broadcasts.PLAY_NEXT");PendingIntent nextPendingIntent=PendingIntent.getBroadcast(MyService.this,0,nextIntent,0);notifyLayout.setOnClickPendingIntent(R.id.iv_music_next_song,nextPendingIntent);}/*** 更改了相关设置,比如notifyLayout布局显示之后,需要重新发送前台通知来更新UI* 太坑了,居然是这样的* 此外,由于我们是显示成一个播放器,因此通知id,使用固定id,就可以保证每次更新之后是同一个通知。* @param picture 设置播放器图标为指定音乐图标,此对象属于MVC开发模式的model层数据*/private void mstartForeground(MusicPicture picture) {mNotifyHelper.CreateChannel(CHANNEL_ID, CHANNEL_NAME, CHANNEL_DESCRIPTION);final Notification notification = mNotifyHelper.createForeNotification(CHANNEL_ID, picture, notifyLayout);startForeground(NOTIFY_ID, notification);}/*** 处于播放状态下的音乐应该具有的一些配置* @param picture*/private void playMusicState(MusicPicture picture) {if(flag==MODE_PLAY) return;flag=MODE_PLAY;Intent pauseIntent=new Intent(MyService.this,PauseService.class);PendingIntent pausePendingIntent=PendingIntent.getService(MyService.this,0,pauseIntent,0);notifyLayout.setOnClickPendingIntent(R.id.iv_music_play,pausePendingIntent);notifyLayout.setImageViewResource(R.id.iv_music_icon, picture.getPic_id());notifyLayout.setImageViewResource(R.id.iv_music_play, R.mipmap.pause_music);mstartForeground(picture);}/*** 处于暂停状态下的音乐应该具有的一些配置* @param picture*/private void pauseMusicState(MusicPicture picture) {if(flag==MODE_PAUSE) return;flag=MODE_PAUSE;Intent playIntent=new Intent(MyService.this,PlayService.class);PendingIntent playPendingIntent=PendingIntent.getService(MyService.this,0,playIntent,0);notifyLayout.setOnClickPendingIntent(R.id.iv_music_play,playPendingIntent);notifyLayout.setImageViewResource(R.id.iv_music_icon, picture.getPic_id());notifyLayout.setImageViewResource(R.id.iv_music_play, R.mipmap.play_music);mstartForeground(picture);}}
}
注意看代码注释,我觉得蛮清晰的,该说明的基本都说了哦~
特别注意一下我说坑的地方,就是更新通知栏播放器上面控件设置,必须在通知发送前实现,故我采用更新界面之后,重新发送相同id的一条通知!!!
3.好了,接下来,我们来实现两个服务,和一个广播接收器:
<1>播放与暂停服务:
package com.example.jackandrose.services;import android.app.Service;
import android.content.Intent;
import android.media.MediaPlayer;
import android.os.IBinder;import com.example.jackandrose.entities.MediaHelper;public class PlayService extends Service {/*** 用于使音乐继续播放的服务*/private MediaHelper mMediaHelper;public PlayService() {}@Overridepublic void onCreate() {super.onCreate();mMediaHelper=MediaHelper.getInstance(this);}@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {mMediaHelper.start();stopSelf();return super.onStartCommand(intent, flags, startId);}@Overridepublic IBinder onBind(Intent intent) {// TODO: Return the communication channel to the service.throw new UnsupportedOperationException("Not yet implemented");}
}
package com.example.jackandrose.services;import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;import com.example.jackandrose.entities.MediaHelper;public class PauseService extends Service {/*** 用于使音乐暂停的服务*/private MediaHelper mMediaHelper;public PauseService() {}@Overridepublic void onCreate() {super.onCreate();mMediaHelper=MediaHelper.getInstance(this);}@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {mMediaHelper.pause();stopSelf();return super.onStartCommand(intent, flags, startId);}@Overridepublic IBinder onBind(Intent intent) {// TODO: Return the communication channel to the service.throw new UnsupportedOperationException("Not yet implemented");}
}
这个时候,你就知道我们将音乐播放帮助类接口外放的原因了。我们在主服务里面实现了接口,即实现了播放和暂停状态下的UI界面,所以我们在服务里面只需要放心大胆的使用音乐帮助类的暂停和播放方法就可以了,音乐它们会调用外放的接口方法。
<2>切换歌曲的广播,其实播放和暂停完全也可以用广播实现,但是我为了看起来更加多元化,哈哈哈:
package com.example.jackandrose.broadcasts;import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import android.widget.Toast;public class MyBroadCastReceiver extends BroadcastReceiver {/*** 切换歌曲的广播,这里仍采用单例模式。* 原因是我们要将切换歌曲的接口外放到MusAdapter*/private static MyBroadCastReceiver instance;private static final String PLAY_LAST="com.example.jackandrose.broadcasts.PLAY_LAST";private static final String PLAY_NEXT="com.example.jackandrose.broadcasts.PLAY_NEXT";private MyBroadListner mMyBroadListner;public static MyBroadCastReceiver getInstance() {if(instance==null){instance=new MyBroadCastReceiver();}return instance;}public void setmMyBroadListner(MyBroadListner mMyBroadListner) {this.mMyBroadListner = mMyBroadListner;}@Overridepublic void onReceive(Context context, Intent intent) {if(intent.getAction()==null){Log.w("MyBroadCastReceiver","yes");}else{Log.w("MyBroadCastReceiver","no");}if(PLAY_LAST.equals(intent.getAction())){Toast.makeText(context, "即将切换至上一首,若切换失败,请回到界面点击音乐实现切换", Toast.LENGTH_LONG).show();if(mMyBroadListner!=null){mMyBroadListner.playLast();}}else if(PLAY_NEXT.equals(intent.getAction())){Toast.makeText(context, "即将切换至下一首,若切换失败,请回到界面点击音乐实现切换", Toast.LENGTH_LONG).show();if(mMyBroadListner!=null){mMyBroadListner.playNext();}}}/*** 定义外放接口,在MusAdapter类中我进行了实现*/public interface MyBroadListner{void playLast();void playNext();}
}
注意一下,广播这个东西,8.0以上也有变化,静态注册可能不起作用,因此我在MyService的代码里进行了动态注册,为了保险起见,我们静态也加上:
<receiver android:name=".broadcasts.MyBroadCastReceiver"><intent-filter><action android:name="com.example.jackandrose.broadcasts.PLAY_LAST" /><action android:name="com.example.jackandrose.broadcasts.PLAY_NEXT" /></intent-filter></receiver>
4.广播接收器的代码注释提到了MusAdapter,没错了,这里便是音乐播放的启动器,我们是单击RecyclerView单个条目实现播放的,并设置选中条目,单击选中条目则不会播放。
实现广播接收器的外放接口!!!
代码还是给出来吧:
package com.example.jackandrose.adapters;import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.graphics.Color;
import android.os.IBinder;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;import com.example.jackandrose.R;
import com.example.jackandrose.broadcasts.MyBroadCastReceiver;
import com.example.jackandrose.entities.MusicPicture;
import com.example.jackandrose.services.MyService;import java.util.ArrayList;public class MusAdapter extends RecyclerView.Adapter<MusAdapter.ViewHolder> {private static final String TAG = "MusAdapter";private ArrayList<MusicPicture> myPictures;private Context mContext;private int selectedPosition = -5;private int mResID=-5;private MyService.MyBinder mMyBinder;private MyService mMyService;private boolean isBind;private Intent mServiceIntent;private MusicPicture mMusicPicture;private MyBroadCastReceiver mMyBroadCastReceiver;class ViewHolder extends RecyclerView.ViewHolder {ImageView picImg;TextView picText;LinearLayout box;int d_id; //音乐图片idint d_resource; //音乐资源idView view;public ViewHolder(@NonNull View itemView) {super(itemView);picImg = (ImageView) itemView.findViewById(R.id.pic_image);picText = (TextView) itemView.findViewById(R.id.pic_text);box = itemView.findViewById(R.id.item_box);view = itemView;}}public MusAdapter(ArrayList<MusicPicture> pictures,Context context) {myPictures = pictures;mContext=context;mMyBroadCastReceiver=MyBroadCastReceiver.getInstance();isBind=false;}private void setMyBroadCastListener() {/*** 此处仍然来实现单例对象的接口对象* 即实现歌曲的切换,实现广播接收器的外放接口*/mMyBroadCastReceiver.setmMyBroadListner(new MyBroadCastReceiver.MyBroadListner() {@Overridepublic void playLast() {MusAdapter.this.playLast();}@Overridepublic void playNext() {MusAdapter.this.playNext();}});}private void playLast() {if (selectedPosition > 0) {setSelectedIndex(selectedPosition - 1);}}private void playNext() {if (selectedPosition < getItemCount() - 1) {setSelectedIndex(selectedPosition + 1);}}private void stateChange(ViewHolder holder, int position,MusicPicture picture) {if (selectedPosition == position) {setMyBroadCastListener();//不管怎么样,先把状态改变效果展示出来int color = holder.view.getContext().getResources().getColor(R.color.myActionBarColor);holder.box.setBackgroundColor(color);holder.picText.setTextColor(Color.WHITE);mMusicPicture =picture;if(picture.getPic_resource()==mResID&&mResID!=-5){//相同音乐id或者且不是第一次播放,就直接返回return;}mResID=picture.getPic_resource();//启动服务来播放if(mServiceIntent==null){mServiceIntent=new Intent(mContext,MyService.class);mContext.startService(mServiceIntent);}//每次切歌需要重新绑定服务destroy();if(!isBind){mContext.bindService(mServiceIntent,connection,Context.BIND_AUTO_CREATE);isBind=true;}} else {holder.box.setBackgroundColor(Color.WHITE);holder.picText.setTextColor(Color.BLACK);//因为我们从始至终只有一个显示图片的imageview,所以不应该写下面这行代码}}@NonNull@Overridepublic ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_item, parent, false);ViewHolder holder = new ViewHolder(view);return holder;}@Overridepublic void onBindViewHolder(@NonNull final ViewHolder holder, int position) {MusicPicture musicPicture = myPictures.get(position);holder.picImg.setImageResource(musicPicture.getPic_id());holder.picText.setText(musicPicture.getPic_name());holder.d_id = musicPicture.getPic_id();holder.d_resource = musicPicture.getPic_resource();setClick(holder, position);stateChange(holder, position,musicPicture);}@Overridepublic int getItemCount() {return myPictures.size();}private void setClick(ViewHolder holder, final int position) {holder.view.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {setSelectedIndex(position);}});}private void setSelectedIndex(int position) {selectedPosition = position;notifyItemChanged(position);notifyDataSetChanged();}private void destroy(){if(isBind){mContext.unbindService(connection);isBind=false;}}private ServiceConnection connection=new ServiceConnection() {@Overridepublic void onServiceConnected(ComponentName name, IBinder service) {mMyBinder= (MyService.MyBinder) service;mMyService=mMyBinder.getService();mMyBinder.setMusic(mMusicPicture);}@Overridepublic void onServiceDisconnected(ComponentName name) {}};
}
MusicPicture类就不再展示了,就是标准的数据类,包含get和set方法~
写的有些乱,不好的地方提提意见,不要喷我,毕竟我还是个新手,谢谢。
5.效果图展示:
<1>音乐条目界面:
<2>音乐播放界面:
6.好了,到这里就结束了,不过其实bug就是退出界面后,切歌功能实效,其实不难理解为什么,但是实在不知道如何解决,或者说直接换一种实现,避免这个问题。
能看完的都太厉害了…能坚持看完的兄弟,如果觉得我写的也不是特别差的话,动动小手点个赞呗。觉得我写的存在问题的,欢迎提出来,我也特别希望有一些改进的建议,谢谢观看!