Android 音乐通知栏
前言
这篇文章的标题有些言简意赅了,也突出了这篇文章的核心,那就是通知栏的操作,你可以看到市面上的音乐类APP都会有这个操作,通过音乐通知栏可以播放暂停、上一曲、下一曲、收藏、显示歌词等等。当然我这个Demo目前不考虑这么多,先实现播放暂停、上一曲、下一曲这些基本功能再说,你说对吧。
正文
在第四篇文章的到最后显示了通知栏,那么为什么我要把通知的的操作单独放到一篇文章来进行讲解呢?因为里面有很多业务逻辑,还有通信的关系,所以才这么做的。
① 通知栏按钮点击监听
要实现具体的业务功能,首先要监听到点击事件,这一点是毋庸置疑的,谁赞成,谁反对。首先增加几个全局变量,打开Constant
/** * 歌曲播放 */ public static final String PLAY = "play"; /** * 歌曲暂停 */ public static final String PAUSE = "pause"; /** * 上一曲 */ public static final String PREV = "prev"; /** * 下一曲 */ public static final String NEXT = "next"; /** * 关闭通知栏 */ public static final String CLOSE = "close"; /** * 进度变化 */ public static final String PROGRESS = "progress";
这些都是用来表明当前歌曲的状态的,至关重要。之前我通过RemoteViews来指定一个布局文件,从而实现自定义通知栏样式的效果,那么对于通知栏页面的按钮的点击事件,也是交给RemoteViews来完成来的,下面进行实例化,把它变成成员变量。
在Service中实例化
private static RemoteViews remoteViews;
然后单独写一个方法对RemoteViews进行初始化配置。
/** * 初始化自定义通知栏 的按钮点击事件 */ private void initRemoteViews() { remoteViews = new RemoteViews(this.getPackageName(), R.layout.notification); //通知栏控制器上一首按钮广播操作 Intent intentPrev = new Intent(PREV); PendingIntent prevPendingIntent = PendingIntent.getBroadcast(this, 0, intentPrev, 0); //为prev控件注册事件 remoteViews.setOnClickPendingIntent(R.id.btn_notification_previous, prevPendingIntent); //通知栏控制器播放暂停按钮广播操作 //用于接收广播时过滤意图信息 Intent intentPlay = new Intent(PLAY); PendingIntent playPendingIntent = PendingIntent.getBroadcast(this, 0, intentPlay, 0); //为play控件注册事件 remoteViews.setOnClickPendingIntent(R.id.btn_notification_play, playPendingIntent); //通知栏控制器下一首按钮广播操作 Intent intentNext = new Intent(NEXT); PendingIntent nextPendingIntent = PendingIntent.getBroadcast(this, 0, intentNext, 0); //为next控件注册事件 remoteViews.setOnClickPendingIntent(R.id.btn_notification_next, nextPendingIntent); //通知栏控制器关闭按钮广播操作 Intent intentClose = new Intent(CLOSE); PendingIntent closePendingIntent = PendingIntent.getBroadcast(this, 0, intentClose, 0); //为close控件注册事件 remoteViews.setOnClickPendingIntent(R.id.btn_notification_close, closePendingIntent); }
目前通知栏上看到的按钮只有四个,因为播放和暂停是一个按钮,到时候可以根据MediaPlayer的播放状态做进一步的处理,上面四个按钮,点击之后会发送一个广播,既然有广播,那自然要有一个广播接收器,就好比,你到淘宝上买衣服,别人给你发货了,你总要设置一个收货地址吧。这是一个道理的。至于广播接收器,可以写在Service里面,作为一个内部类使用。那么先创建这个内部类。
/** * 广播接收器 (内部类) */ public class MusicReceiver extends BroadcastReceiver { public static final String TAG = "MusicReceiver"; @Override public void onReceive(Context context, Intent intent) { //UI控制 UIControl(intent.getAction(), TAG); } }
然后来看看UIControl方法。
/** * 页面的UI 控制 ,通过服务来控制页面和通知栏的UI * * @param state 状态码 * @param tag */ private void UIControl(String state, String tag) { switch (state) { case PLAY: BLog.d(tag,PLAY+" or "+PAUSE); break; case PREV: BLog.d(tag,PREV); break; case NEXT: BLog.d(tag,NEXT); break; case CLOSE: BLog.d(tag,CLOSE); break; default: break; } }
对应四个通知栏的按钮,这是是作为广播的接收。但是要实际收到,还要注册才行。
所以要注册动态广播。
/** * 注册动态广播 */ private void registerMusicReceiver() { musicReceiver = new MusicReceiver(); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(PLAY); intentFilter.addAction(PREV); intentFilter.addAction(NEXT); intentFilter.addAction(CLOSE); registerReceiver(musicReceiver, intentFilter); }
在这里你可以发现,我对四个值进行了拦截过滤,也就是说当我点击通知栏的上一曲按钮时,会发送动作名为PREV的广播,而这个时候MusicReceiver拦截到PREV的广播,传递给onReceive。然后在onReceive对不同的动作做不同的处理,目前我只是打印了日志而已。
现在你可以将showNotification方法中的如下代码删除掉。
RemoteViews remoteViews = new RemoteViews(this.getPackageName(), R.layout.notification);
然后在Service中的onCreate中调用。
@Override public void onCreate() { super.onCreate(); initRemoteViews(); //注册动态广播 registerMusicReceiver(); showNotification(); BLog.d(TAG, "onCreate"); }
initRemoteViews 方法一定要在 showNotification之前调用,否则你就等着null Object 然后APP崩溃吧。
在服务销毁的时候要解绑广播接收者
@Override public void onDestroy() { super.onDestroy(); if (musicReceiver != null) { //解除动态注册的广播 unregisterReceiver(musicReceiver); } }
下面运行一下,日志如下:
现在通知栏的按钮点击事件就已经监听到了,下面做通知栏的点击事件。
② 通知栏点击监听
只要是通知栏按钮以外的点击都属于通知栏的点击,这个要区分开,别搞混了。在写代码要想清楚一点,当我们点击通知栏的时候,要进入那个页面,我仔细观察过其他音乐APP的这个点击通知栏的效果,是从那个页面切换到后台,下次点击通知栏时就进入到那个页面,也就是说它点击跳转的页面是动态的,所以不能是写死的。有了这个业务需求那么就可以开始写代码了。这里也是需要用到广播的,只不过不再是写内部类了。在com.llw.goodmusic下面新建一个receiver的包,然后创建NotificationClickReceiver,里面的两个可以不用勾选。
创建好之后,打开AndroidManifest.xml你会看到如下代码:
<receiver android:name=".receiver.NotificationClickReceiver"/>
下面进入MusicService中,
//点击整个通知时发送广播 Intent intent = new Intent(getApplicationContext(), NotificationClickReceiver.class); PendingIntent pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
然后通过.setContentIntent(pendingIntent)设置进去,如下图所示
下面进入到NotificationClickReceiver中。
package com.llw.goodmusic.receiver; import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import com.llw.goodmusic.utils.BLog; /** * 通知点击广播接收器 跳转到栈顶的Activity ,而不是new 一个新的Activity * * @author llw */ public class NotificationClickReceiver extends BroadcastReceiver { public static final String TAG = "NotificationClickReceiver"; @Override public void onReceive(Context context, Intent intent) { BLog.d(TAG,"通知栏点击"); } }
然后运行,运行之后点击通知栏,再看日志打印,如下所示:
③ 通知栏业务处理
在上面已经实现了通知栏的点击监听了,下面就要开始进行业务逻辑的处理了。先解决通知栏的点击业务处理,再解决通知栏按钮的点击处理,打开AndroidManager,注意这是之前我自己写的,不是系统的。在里面增加
/** * 弱引用 */ private static WeakReference<Activity> activityWeakReference; private static Object activityUpdateLock = new Object(); /** * 得到当前Activity * @return */ public static Activity getCurrentActivity() { Activity currentActivity = null; synchronized (activityUpdateLock){ if (activityWeakReference != null) { currentActivity = activityWeakReference.get(); } } return currentActivity; } /** * 设置当前Activity * @return */ public static void setCurrentActivity(Activity activity) { synchronized (activityUpdateLock){ activityWeakReference = new WeakReference<Activity>(activity); } }
然后进入到BasicApplication中,在onCreate中写入:
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() { @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { } @Override public void onActivityStarted(Activity activity) { } @Override public void onActivityResumed(Activity activity) { ActivityManager.setCurrentActivity(activity); } @Override public void onActivityPaused(Activity activity) { } @Override public void onActivityStopped(Activity activity) { } @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) { } @Override public void onActivityDestroyed(Activity activity) { } });
通过上面得代码就可以得到栈顶的Activity,那么怎么来使用这个Activity呢,进入到NotificationClickReceiver
@Override public void onReceive(Context context, Intent intent) { BLog.d(TAG,"通知栏点击"); //获取栈顶的Activity Activity currentActivity = ActivityManager.getCurrentActivity(); intent = new Intent(Intent.ACTION_MAIN); intent.addCategory(Intent.CATEGORY_LAUNCHER); intent.setClass(context, currentActivity.getClass()); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); context.startActivity(intent); }
这样就可以实现,点击通知栏时跳转到栈顶的Activity而不是新建一个Activity。
下面就是针对通知栏的信息显示做处理了,首先肯定要根据不同的音乐显示不同的歌曲信息,这一点毋庸置疑。那么这样的话就不能一开始就显示通知栏了,而是在点击播放按钮的时候显示通知栏,当切歌,或者暂停时更新这个通知栏的状态,于是就可以在MusicService中写入一个这样的方法。
/** * 初始化通知 */ private void initNotification() { String channelId = "play_control"; String channelName = "播放控制"; int importance = NotificationManager.IMPORTANCE_HIGH; createNotificationChannel(channelId, channelName, importance); //点击整个通知时发送广播 Intent intent = new Intent(getApplicationContext(), NotificationClickReceiver.class); PendingIntent pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); //初始化通知 notification = new NotificationCompat.Builder(this, "play_control") .setContentIntent(pendingIntent) .setWhen(System.currentTimeMillis()) .setSmallIcon(R.mipmap.icon_big_logo) .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.icon_big_logo)) .setCustomContentView(remoteViews) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setAutoCancel(false) .setOnlyAlertOnce(true) .setOngoing(true) .build(); }
这里就是把原来的showNotification方法改了一下,把显示通知的代码放到改变通知栏状态的时候使用。当然还是要在onCreate中调用这个方法的。
下面先在MusiceService中定义这些变量
/** * 歌曲间隔时间 */ private static final int INTERNAL_TIME = 1000; /** * 歌曲列表 */ private static List<Song> mList = new ArrayList<>(); /** * 音乐播放器 */ public MediaPlayer mediaPlayer; /** * 记录播放的位置 */ int playPosition = 0; /** * 通知 */ private static Notification notification; /** * 通知栏视图 */ private static RemoteViews remoteViews; /** * 通知ID */ private int NOTIFICATION_ID = 1; /** * 通知管理器 */ private static NotificationManager manager; /** * 音乐广播接收器 */ private MusicReceiver musicReceiver;
然后写入一个更改通知栏样式的方法,每次对音乐进行控制时都会调用。
/** * 更改通知的信息和UI * @param position 歌曲位置 */ public void updateNotificationShow(int position) { //播放状态判断 if (mediaPlayer.isPlaying()) { remoteViews.setImageViewResource(R.id.btn_notification_play, R.drawable.pause_black); } else { remoteViews.setImageViewResource(R.id.btn_notification_play, R.drawable.play_black); } //封面专辑 remoteViews.setImageViewBitmap(R.id.iv_album_cover, MusicUtils.getAlbumPicture(this, mList.get(position).getPath(), 0)); //歌曲名 remoteViews.setTextViewText(R.id.tv_notification_song_name, mList.get(position).getSong()); //歌手名 remoteViews.setTextViewText(R.id.tv_notification_singer, mList.get(position).getSinger()); //发送通知 manager.notify(NOTIFICATION_ID, notification); }
在这个方法里面我调用MusicUtils工具类的getAlbumPicture方法。这个方法我做了一点点改动
改动如下图所示:
下面就是点击播放时的音乐方法了。
/** * 播放 */ public void play(int position) { if (mediaPlayer == null) { mediaPlayer = new MediaPlayer(); //监听音乐播放完毕事件,自动下一曲 mediaPlayer.setOnCompletionListener(this); } //播放时 获取当前歌曲列表是否有歌曲 mList = LitePal.findAll(Song.class); if (mList.size() <= 0) { return; } try { //切歌前先重置,释放掉之前的资源 mediaPlayer.reset(); playPosition = position; //设置播放音频的资源路径 mediaPlayer.setDataSource(mList.get(position).path); mediaPlayer.prepare(); mediaPlayer.start(); //显示通知 updateNotificationShow(position); } catch (IOException e) { e.printStackTrace(); } }
在上面的播放方法中,首先初始化了MediaPlayer,然后添加了播放完成的监听,这个在后面也是要实现的。然后获取当前的播放位置赋值给成员变量,之后通过位置得到歌曲的路径,通过路径来播放音乐,播放音乐之后将位置传递给显示通知栏的方法,此时通知栏的信息久会更改。
在onCreate方法中添加如下代码,获取本地歌曲数据。
mList = LitePal.findAll(Song.class);
这样做是避免空对象导致APP的崩溃。
接下来就是上一曲的方法
/** * 上一首 */ public void previousMusic() { if (playPosition <= 0) { playPosition = mList.size() - 1; } else { playPosition -= 1; } play(playPosition); }
通过播放位置,先判断当前是为第一首歌,是则将播放位置移动到最后一首,不是则直接减一,之后则调用play方法播放上一首歌曲。
下一曲的方法
/** * 下一首 */ public void nextMusic() { if (playPosition >= mList.size() - 1) { playPosition = 0; } else { playPosition += 1; } play(playPosition); }
先判断当前是否为最后一首,是的话则从移动到第一首,不是则加一到下一首。然后调用play方法播放下一首歌曲。
暂停继续音乐
/** * 暂停/继续 音乐 */ public void pauseOrContinueMusic() { if (mediaPlayer.isPlaying()) { mediaPlayer.pause(); } else { mediaPlayer.start(); } //更改通知栏播放状态 updateNotificationShow(playPosition); }
更改播放状态
最后是关闭通知栏的方法
/** * 关闭音乐通知栏 */ public void closeNotification() { if (mediaPlayer != null) { if (mediaPlayer.isPlaying()) { mediaPlayer.pause(); } } manager.cancel(NOTIFICATION_ID); }
下面就是调用的地方了
然后还要实现MediaPlayer的音乐播放完成的监听,
public class MusicService extends Service implements MediaPlayer.OnCompletionListener
然后重写onCompletion方法,在里面直接调用nextMusic播放下一曲即可。
/** * 当前音乐播放完成监听 * * @param mp */ @Override public void onCompletion(MediaPlayer mp) { //下一曲 nextMusic(); }
下面就要设置通知出现的入口,一般来说是在点击播放按钮,当前有音乐播放时,才会显示通知。然后在layout下面新建一个通用的底部通知布局。
play_control_layout.xml
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <!--底部播放控制布局--> <LinearLayout android:id="@+id/lay_bottom" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:background="@color/bottom_bg_color" android:gravity="center_vertical" android:paddingLeft="@dimen/dp_8" android:paddingTop="@dimen/dp_8" android:paddingRight="@dimen/dp_16" android:paddingBottom="@dimen/dp_8"> <!-- logo和播放进度 使用相对布局达成覆盖的效果--> <RelativeLayout android:layout_width="wrap_content" android:layout_height="wrap_content"> <!--logo--> <com.google.android.material.imageview.ShapeableImageView android:id="@+id/iv_logo" android:layout_width="@dimen/dp_48" android:layout_height="@dimen/dp_48" android:padding="1dp" android:src="@mipmap/icon_music" app:shapeAppearanceOverlay="@style/circleImageStyle" app:strokeColor="@color/white" app:strokeWidth="@dimen/dp_2" /> <!--播放进度 自定义View--> <com.llw.goodmusic.view.MusicRoundProgressView android:id="@+id/music_progress" android:layout_width="@dimen/dp_48" android:layout_height="@dimen/dp_48" app:radius="22dp" app:strokeColor="@color/gold_color" app:strokeWidth="2dp" /> </RelativeLayout> <!--歌曲信息 歌名 - 歌手 --> <com.google.android.material.textview.MaterialTextView android:id="@+id/tv_song_name" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:ellipsize="marquee" android:focusable="true" android:focusableInTouchMode="true" android:marqueeRepeatLimit="marquee_forever" android:paddingLeft="@dimen/dp_12" android:paddingRight="@dimen/dp_12" android:singleLine="true" android:text="Good Music" android:textColor="@color/white" android:textSize="@dimen/sp_16" /> <!--歌曲控制按钮--> <com.google.android.material.button.MaterialButton android:id="@+id/btn_play" android:layout_width="@dimen/dp_36" android:layout_height="@dimen/dp_36" android:insetLeft="@dimen/dp_0" android:insetTop="@dimen/dp_0" android:insetRight="@dimen/dp_0" android:insetBottom="@dimen/dp_0" android:theme="@style/Theme.MaterialComponents.Light.NoActionBar" app:backgroundTint="@color/transparent" app:cornerRadius="@dimen/dp_18" app:icon="@mipmap/icon_play" app:iconGravity="textStart" app:iconPadding="@dimen/dp_0" app:iconSize="@dimen/dp_36" /> </LinearLayout> </layout>
其实就是之前LocalMusicActivity的底部布局。
下面进入MainActivity中,
/** * 底部logo图标,点击之后弹出当前播放歌曲详情页 */ private ShapeableImageView ivLogo; /** * 底部当前播放歌名 */ private MaterialTextView tvSongName; /** * 底部当前歌曲控制按钮, 播放和暂停 */ private MaterialButton btnPlay; /** * 自定义进度条 */ private MusicRoundProgressView musicProgress; /** * 列表位置 */ private int listPosition = 0;
然后在initData中,通过引入的布局绑定控件,并且添加点击监听,下面就可以在
@Override public void onClick(View v) { switch (v.getId()) { case R.id.lay_local_music: //本地音乐 startActivity(new Intent(context, LocalMusicActivity.class)); break; case R.id.btn_play: if (mList.size() == 0) { show("没有可播放的音乐,请到 “本地音乐” 进行扫描"); return; } musicService.play(listPosition); break; default: break; } }
下面运行测试一波:
④ 运行效果图
结语
现在已经搞定了后台播放和通知栏控制音乐,下一篇就该是通知栏和Activity的双向控制了。