Activity和Notification通讯
前言
上一篇博客实现了通知栏操控后台音乐的播放,但是这还不够,因为你还得让页面上也能知道当前音乐的状态是怎么样的,这样用户才能感知到。
正文
这一篇要实现双向控制,也就是说可以通过通知栏控制Activity的UI,也可以通过Activity来控制通知栏上的UI。这里可以通过观察者模式来实现,什么是观察者模式呢?就是一对多,举个例子,平时各位大佬可能会看技术博客,会关注某一些博主,而你关注的某一个博主就是被观察者,你则是观察者,而一个博主可能有很多个粉丝,也就是可以由多个观察者,每个观察者都能知道这个博主的最新动态和文章。这样的方式叫做观察者模式,Android中的广播,EventBus、 RXBus、LiveData都可以实现这种模式,当前最新的应该是LiveData了,这一个也是Jetpack全家桶里面的一个重要成员,下面就用这个LiveData来实现通知栏和页面的双向通讯。
① 通知栏控制Activity
首先在com.llw.goodmusic下新建一个livedata包,然后新建一个LiveDataBus类,如下图所示,这个里面就是存放不同的Livedata的
代码如下:
package com.llw.goodmusic.livedata; import androidx.annotation.NonNull; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Observer; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; /** * liveData管道总线 * * @author llw */ public class LiveDataBus { /** * LiveData集合 */ private Map<String, BusMutableLiveData<Object>> liveDataMap; private static LiveDataBus liveDataBus = new LiveDataBus(); private LiveDataBus(){ liveDataMap = new HashMap<>(); } public static LiveDataBus getInstance(){ return liveDataBus; } /** * 这个是存和取一体的方法 * @param key * @param clazz * @param <T> * @return */ public synchronized<T> BusMutableLiveData<T> with(String key,Class<T> clazz){ if(!liveDataMap.containsKey(key)){ liveDataMap.put(key,new BusMutableLiveData<Object>()); } return (BusMutableLiveData<T>) liveDataMap.get(key); } public static class BusMutableLiveData<T> extends MutableLiveData<T> { //是否需要粘性事件 private boolean isHad = false; //重写observe的方法 public void observe(@NonNull LifecycleOwner owner, boolean isHad,@NonNull Observer<? super T> observer) { if(isHad){ this.isHad = true; }else{ this.isHad = false; } this.observe(owner,observer); } //重写observe的方法 @Override public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) { super.observe(owner, observer); //改变observer.mLastVersion >= mVersion这个判断 然后拦截onChanged try { if(this.isHad){ hook((Observer<T>) observer); } } catch (Exception e) { e.printStackTrace(); } } /** * hook方法 hook系统源码 改变系统的一些参数 * @param observer */ private void hook(Observer<T> observer) throws Exception { //获取到LiveData的类对象 Class<LiveData> liveDataClass = LiveData.class; //获取到mObservers的反射对象 Field mObserversField = liveDataClass.getDeclaredField("mObservers"); //让mObserversField可以被访问 mObserversField.setAccessible(true); //获取到这个mObserversField的值 Object mObservers = mObserversField.get(this); //获取到mObservers的get方法的反射对象 Method get = mObservers.getClass().getDeclaredMethod("get", Object.class); //设置这个反射对象可以被访问 get.setAccessible(true); //执行这个方法 得到Entry Object invokeEntry = get.invoke(mObservers, observer); //定义一个空的对象 LifecycleBoundObserver Object observerWrapper = null; if(invokeEntry!=null && invokeEntry instanceof Map.Entry){ observerWrapper = ((Map.Entry)invokeEntry).getValue(); } if(observerWrapper == null){ throw new NullPointerException("ObserverWrapper不能为空"); } //获取到ObserverWrapper的类对象 Class<?> superclass = observerWrapper.getClass().getSuperclass(); //获取搭配这个类中的mLastVersion成员变量 Field mLastVersionField = superclass.getDeclaredField("mLastVersion"); mLastVersionField.setAccessible(true); //获取到mVersion的反射对象 Field mVersionField = liveDataClass.getDeclaredField("mVersion"); //打开权限 mVersionField.setAccessible(true); //得到的就是mVersion在当前类中的值 Object o = mVersionField.get(this); //把它的值给mLastVersion mLastVersionField.set(observerWrapper,o); } } }
这里面的代码是我在第一次听课的过程中一步一步写的,整体的思路并不是我想的,也可以说是学习了。上面的代码也都有注释,就不过多解释了,下面看怎么去用,首先通过通知栏控制Activity,也就是页面,下面我就都用页面二字代替Activity了。说白了其中的思路就是当我点击了通知栏上的某一个按钮的时候,服务会知道这个事情,因为之前注册了广播,但是页面怎么知道这个事情呢,所以其实就是要让服务去告诉页面,我当前点击了通知栏上某一个按钮。然后页面就去做出相应的操作,在这里服务是被观察者,而页面是观察者,所以我们可以在服务中安插一个卧底,让它告诉我们通知栏干了啥事。这么一想思路就很清晰了,下面来看具体的实现。
进入MusicService中,
/** * 通知栏控制Activity页面UI */ private LiveDataBus.BusMutableLiveData<String> activityLiveData;
创建对象,传入String就代表接收的也是String,这里你可以传递任何类型的数据。
然后要在onCreate进行实例化。
activityLiveData = LiveDataBus.getInstance().with("activity_control", String.class);
这样,卧底就安插好了,下面就是要告诉卧底什么时候要给页面传递消息了。
首先当然是播放了,注意上图中红色方框中的代码,postValue就是发送消息,PLAY是在Constant中定义的全局变量。就是告诉页面,我当前的音乐开始播放了。
当我点击通知的播放按钮时回调用这个方法,那么在这个方法里面首先控制了通知栏本身的按钮UI,于是我再加上对页面的控制,就是一举两得。
关闭通知栏的时候也要告诉页面,这个时候页面上的UI也要做出改变,
下一首和上一首都要通知页面,到这里该告诉页面的信息都说了,再说的话卧底可能就要暴露了,不太好吧。既然卧底的消息都发出去了,那么页面怎么做出相应的改变呢?进入MainActivity。
/** * 当Service中通知栏有变化时接收到消息 */ private LiveDataBus.BusMutableLiveData<String> activityLiveData;
创建对象,然后在initData中调用notificationObserver方法对服务发送过来的消息进行处理。
创建对象,然后在initData中调用notificationObserver方法对服务发送过来的消息进行处理。
/** * 通知栏动作观察者 */ private void notificationObserver() { activityLiveData = LiveDataBus.getInstance().with("activity_control", String.class); activityLiveData.observe(MainActivity.this, true, new Observer<String>() { @Override public void onChanged(String state) { switch (state) { case PLAY: btnPlay.setIcon(getDrawable(R.mipmap.icon_pause)); btnPlay.setIconTint(getColorStateList(R.color.gold_color)); changeUI(musicService.getPlayPosition()); break; case PAUSE: case CLOSE: btnPlay.setIcon(getDrawable(R.mipmap.icon_play)); btnPlay.setIconTint(getColorStateList(R.color.white)); changeUI(musicService.getPlayPosition()); break; case PREV: BLog.d(TAG, "上一曲"); changeUI(musicService.getPlayPosition()); break; case NEXT: BLog.d(TAG, "下一曲"); changeUI(musicService.getPlayPosition()); break; default: break; } } }); }
② Activity控制通知栏
在上面我通过在服务中安插卧底得知通知栏的一些机密信息,那么通知栏也不是省油的灯,也可以安插卧底在Activity中,得知页面的信息,然后更好的伪装自己。再次之前,首先要添加一个依赖。打开app下的build.gradle,在dependencies闭包中添加如下依赖库,然后Sync
//Service中使用lifecycle implementation "androidx.lifecycle:lifecycle-service:2.2.0"
到这一步就先不管这个了。还是在MainActivity中
/** * 当在Activity中做出播放状态的改变时,通知做出相应改变 */ private LiveDataBus.BusMutableLiveData<String> notificationLiveData;
然后在initData中,增加一个notification_control的标识key
目前我只是通过MainActivity的底部播放按钮开始播放音乐,这个时候时候通知栏有变化,但是如果这时再点击一次按钮,就是暂停了,而不是重新播放,暂停后再点就是继续播放。所以要在底部按钮的点击事件中,发送一个消息过去。
下面进入到MusicService。
/** * Activity控制通知栏UI */ private LiveDataBus.BusMutableLiveData<String> notificationLiveData;
然后在MusicService的onCreate方法中调用activityObserver方法
再写这个方法之前,先修改一些MusicService继承的Service,改成LifecycleService,如果你没有这个类就说明你的依赖库没有加进来。
然后
下面写这个activityObserver方法,代码如下:
/** * Activity的观察者 */ private void activityObserver() { notificationLiveData = LiveDataBus.getInstance().with("notification_control", String.class); notificationLiveData.observe(MusicService.this, new Observer<String>() { @Override public void onChanged(String state) { //UI控制 UIControl(state, TAG); } }); }
也是比较的简单因为它和通知栏相应时传递的值是一样的,所以可以复用UIControl方法,这样环节就走通过了。
很明显,无论是通过页面操作通知栏还是通知栏操作页面,都搞定了。
③ Activity获取后台音乐播放进度
这个播放进度相信不会陌生,因为之前在LocalMusicActivity中时就已经实现了这个通过,不过当时是只在当前的的Activity中实现的,现在是要获取到后台播放音乐的MediaPlayer的当前播放音乐的进度,所以有些不同,还记得当时安插的卧底吗,可以再给它一个任务,让它在进度变化时发送一个消息给页面,这样就可以了。那么怎么来做呢?可以把之前在LocalMusicActivity中的两个方法复制到MusicService中,然后再稍加改动即可。
private Handler mHandler = new Handler(new Handler.Callback() { @Override public boolean handleMessage(Message message) { //进度发生改变时, activityLiveData.postValue(PROGRESS); //更新进度 updateProgress(); return true; } }); /** * 更新进度 */ private void updateProgress() { // 使用Handler每间隔1s发送一次空消息,通知进度条更新 // 获取一个现成的消息 Message msg = Message.obtain(); // 使用MediaPlayer获取当前播放时间除以总时间的进度 int progress = mediaPlayer.getCurrentPosition(); msg.arg1 = progress; mHandler.sendMessageDelayed(msg, INTERNAL_TIME); }
可以看到当进度发生变化时,通过卧底去发送消息给页面,然后再通过updateProgress方法发送定时的消息,去更新进度。当然这方法还要在play中调用,否则就没有入口了。
做完这一步,就是回到MainActivity中去根据收到消息时做出改变就可以了,如下图所示,这个改变进度只是当前歌曲的进度值让用户看得到而已,所以其他的不需要更改,就没有去调用changeUI(musicService.getPlayPosition());当然你也可以去调用这个方法,只不过歌曲文字超出TextView的时候跑马灯效果就会鬼畜一样,你试试就知道了。
④ 效果图
OK,到这里,功能就写好了,不是吗?至于对LocalMusicActivity的改写要放到下一篇文章了,因为改动比较多,会麻烦一些,感觉是自己给自己挖到坑,只能慢慢填了。