Android 音乐APP(六)Activity和Notification通讯

简介: Android 音乐APP(六)Activity和Notification通讯

Activity和Notification通讯


前言


 上一篇博客实现了通知栏操控后台音乐的播放,但是这还不够,因为你还得让页面上也能知道当前音乐的状态是怎么样的,这样用户才能感知到。


正文


这一篇要实现双向控制,也就是说可以通过通知栏控制Activity的UI,也可以通过Activity来控制通知栏上的UI。这里可以通过观察者模式来实现,什么是观察者模式呢?就是一对多,举个例子,平时各位大佬可能会看技术博客,会关注某一些博主,而你关注的某一个博主就是被观察者,你则是观察者,而一个博主可能有很多个粉丝,也就是可以由多个观察者,每个观察者都能知道这个博主的最新动态和文章。这样的方式叫做观察者模式,Android中的广播,EventBus、 RXBus、LiveData都可以实现这种模式,当前最新的应该是LiveData了,这一个也是Jetpack全家桶里面的一个重要成员,下面就用这个LiveData来实现通知栏和页面的双向通讯。


① 通知栏控制Activity


  首先在com.llw.goodmusic下新建一个livedata包,然后新建一个LiveDataBus类,如下图所示,这个里面就是存放不同的Livedata的


20201109093056369.png


代码如下:

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);


这样,卧底就安插好了,下面就是要告诉卧底什么时候要给页面传递消息了。

20201109094731399.png


首先当然是播放了,注意上图中红色方框中的代码,postValue就是发送消息,PLAY是在Constant中定义的全局变量。就是告诉页面,我当前的音乐开始播放了。

20201109095155392.png


当我点击通知的播放按钮时回调用这个方法,那么在这个方法里面首先控制了通知栏本身的按钮UI,于是我再加上对页面的控制,就是一举两得。

20201109095337261.png


关闭通知栏的时候也要告诉页面,这个时候页面上的UI也要做出改变,


20201109095437440.png

20201109095506134.png


下一首和上一首都要通知页面,到这里该告诉页面的信息都说了,再说的话卧底可能就要暴露了,不太好吧。既然卧底的消息都发出去了,那么页面怎么做出相应的改变呢?进入MainActivity。

  /**
     * 当Service中通知栏有变化时接收到消息
     */
    private LiveDataBus.BusMutableLiveData<String> activityLiveData;


创建对象,然后在initData中调用notificationObserver方法对服务发送过来的消息进行处理。


20201109100102504.png


创建对象,然后在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


20201109105425784.png


目前我只是通过MainActivity的底部播放按钮开始播放音乐,这个时候时候通知栏有变化,但是如果这时再点击一次按钮,就是暂停了,而不是重新播放,暂停后再点就是继续播放。所以要在底部按钮的点击事件中,发送一个消息过去。


20201109105755614.png


下面进入到MusicService。

  /**
     * Activity控制通知栏UI
     */
    private LiveDataBus.BusMutableLiveData<String> notificationLiveData;


然后在MusicService的onCreate方法中调用activityObserver方法


20201109110026519.png


再写这个方法之前,先修改一些MusicService继承的Service,改成LifecycleService,如果你没有这个类就说明你的依赖库没有加进来。


20201109110139886.png


然后


20201109110207523.png


下面写这个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方法,这样环节就走通过了。

image.gif



很明显,无论是通过页面操作通知栏还是通知栏操作页面,都搞定了。


③ 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中调用,否则就没有入口了。


20201109113350859.png


做完这一步,就是回到MainActivity中去根据收到消息时做出改变就可以了,如下图所示,这个改变进度只是当前歌曲的进度值让用户看得到而已,所以其他的不需要更改,就没有去调用changeUI(musicService.getPlayPosition());当然你也可以去调用这个方法,只不过歌曲文字超出TextView的时候跑马灯效果就会鬼畜一样,你试试就知道了。


20201109113604839.png

20201109112536987.gif

④ 效果图


OK,到这里,功能就写好了,不是吗?至于对LocalMusicActivity的改写要放到下一篇文章了,因为改动比较多,会麻烦一些,感觉是自己给自己挖到坑,只能慢慢填了。

相关文章
|
6天前
|
XML Java 数据库
安卓项目:app注册/登录界面设计
本文介绍了如何设计一个Android应用的注册/登录界面,包括布局文件的创建、登录和注册逻辑的实现,以及运行效果的展示。
34 0
安卓项目:app注册/登录界面设计
|
1月前
|
Java 数据库 Android开发
一个Android App最少有几个线程?实现多线程的方式有哪些?
本文介绍了Android多线程编程的重要性及其实现方法,涵盖了基本概念、常见线程类型(如主线程、工作线程)以及多种多线程实现方式(如`Thread`、`HandlerThread`、`Executors`、Kotlin协程等)。通过合理的多线程管理,可大幅提升应用性能和用户体验。
61 15
一个Android App最少有几个线程?实现多线程的方式有哪些?
|
21天前
|
消息中间件 Android开发 索引
Android面试高频知识点(4) 详解Activity的启动流程
讲解Activity的启动流程了,Activity的启动流程相对复杂一下,涉及到了Activity中的生命周期方法,涉及到了Android体系的CS模式,涉及到了Android中进程通讯Binder机制等等, 首先介绍一下Activity,这里引用一下Android guide中对Activity的介绍:
33 4
|
21天前
|
存储 开发工具 Android开发
使用.NET MAUI开发第一个安卓APP
【9月更文挑战第24天】使用.NET MAUI开发首个安卓APP需完成以下步骤:首先,安装Visual Studio 2022并勾选“.NET Multi-platform App UI development”工作负载;接着,安装Android SDK。然后,创建新项目时选择“.NET Multi-platform App (MAUI)”模板,并仅针对Android平台进行配置。了解项目结构,包括`.csproj`配置文件、`Properties`配置文件夹、平台特定代码及共享代码等。
|
26天前
|
XML Android开发 数据格式
🌐Android国际化与本地化全攻略!让你的App走遍全球无障碍!🌍
在全球化背景下,实现Android应用的国际化与本地化至关重要。本文以一款旅游指南App为例,详细介绍如何通过资源文件拆分与命名、适配布局与方向、处理日期时间及货币格式、考虑文化习俗等步骤,完成多语言支持和本地化调整。通过邀请用户测试并收集反馈,确保应用能无缝融入不同市场,提升用户体验与满意度。
58 3
|
5天前
|
安全 网络安全 Android开发
深度解析:利用Universal Links与Android App Links实现无缝网页至应用跳转的安全考量
【10月更文挑战第2天】在移动互联网时代,用户经常需要从网页无缝跳转到移动应用中。这种跳转不仅需要提供流畅的用户体验,还要确保安全性。本文将深入探讨如何利用Universal Links(仅限于iOS)和Android App Links技术实现这一目标,并分析其安全性。
49 0
|
1月前
|
Java 数据库 Android开发
一个Android App最少有几个线程?实现多线程的方式有哪些?
本文介绍了Android应用开发中的多线程编程,涵盖基本概念、常见实现方式及最佳实践。主要内容包括主线程与工作线程的作用、多线程的多种实现方法(如 `Thread`、`HandlerThread`、`Executors` 和 Kotlin 协程),以及如何避免内存泄漏和合理使用线程池。通过有效的多线程管理,可以显著提升应用性能和用户体验。
52 10
|
20天前
|
XML 数据库 Android开发
10分钟手把手教你用Android手撸一个简易的个人记账App
该文章提供了使用Android Studio从零开始创建一个简单的个人记账应用的详细步骤,包括项目搭建、界面设计、数据库处理及各功能模块的实现方法。
|
21天前
|
Android开发 开发者
Android面试之Activity启动流程简述
每个Android开发者都熟悉的Activity,但你是否了解它的启动流程呢?本文将带你深入了解。启动流程涉及四个关键角色:Launcher进程、SystemServer的AMS、应用程序的ActivityThread及Zygote进程。核心在于AMS与ActivityThread间的通信。文章详细解析了从Launcher启动Activity的过程,包括通过AIDL获取AMS、Zygote进程启动以及ActivityThread与AMS的通信机制。接着介绍了如何创建Application及Activity的具体步骤。整体流程清晰明了,帮助你更深入理解Activity的工作原理。
25 0
|
2月前
|
API Android开发
Android P 性能优化:创建APP进程白名单,杀死白名单之外的进程
本文介绍了在Android P系统中通过创建应用进程白名单并杀死白名单之外的进程来优化性能的方法,包括设置权限、获取运行中的APP列表、配置白名单以及在应用启动时杀死非白名单进程的代码实现。
51 1