乐趣区

关于移动应用开发:悬浮窗开发设计实践

目录介绍

  • 01. 整体概述

    • 1.1 我的项目背景
    • 1.2 遇到问题
    • 1.3 根底概念
    • 1.4 设计指标
    • 1.5 收益剖析
  • 02.Window 概念

    • 2.1 Window 增加 View
    • 2.2 Window 的概念
    • 2.3 LayoutParams
    • 2.4 WMS 流程梳理
  • 03. 悬浮窗技术要点

    • 3.1 业务思考点剖析
    • 3.2 关键技术要点
    • 3.3 利用悬浮窗
    • 3.4 增加浮窗源码流程
    • 3.5 了解 WMS 原理
    • 3.6 拖拽回弹吸附
  • 04. 开发重要步骤

    • 4.1 悬浮窗实现流程
    • 4.2 申请悬浮窗权限
    • 4.3 初始化悬浮窗
    • 4.4 设置悬浮窗参数
    • 4.5 增加 View 到悬浮窗
    • 4.6 悬浮窗拖拽实现
    • 4.8 悬浮窗权限适配
    • 4.9 LayoutParam 坑
  • 05. 计划根底设计

    • 5.1 整体架构图
    • 5.2 UML 设计图
    • 5.3 要害流程图
    • 5.4 接口设计图
    • 5.5 模块间依赖关系
  • 06. 其余设计说明

    • 6.1 性能设计
    • 6.2 稳定性设计
    • 6.3 异样设计
    • 6.4 事件上报设计
  • 07. 遇到的问题和坑

    • 7.1 解决输入法层级关系
    • 7.2 边界逻辑敞开悬浮窗
    • 7.3 点击屡次关上页面
    • 7.4 Home 键遇到的问题

01. 整体概述

1.1 我的项目背景

  • 业务场景剖析

    • 以视频通话为例,在视频通话时,咱们关上其余利用或点击 Home 键退出时或点击缩放图标,悬浮窗会显示在其余利用之上,给人的假象是通话页面变小了,点击悬浮窗回到通过页面,悬浮窗隐没。退出通话页面悬浮窗隐没。
  • 市面上常见的悬浮窗,如微信视频通话性能,有如下特点:

    • 整屏页面能切换到一个小的悬浮窗;悬浮窗能运行在其余 app 上方;悬浮窗能跳回整屏页面,并且悬浮窗隐没
  • 需要悬浮窗成果

    • 点击放大按钮,将以后远端视屏加载进悬浮窗,且悬浮窗可拖拽,不影响其余界面焦点;点击悬浮窗可返回原来的 Activity

1.2 遇到问题

  • 什么是悬浮窗

    • 全局悬浮窗在许多利用中都能见到,点击 Home 键,小窗口依然会在屏幕上显示。留神:悬浮窗留神申请权限!
  • 那么开发全局悬浮窗属于那一类呢?

    • 属于零碎窗口,相当于跟 Toast 是一个级别的。针对悬浮窗的展现和移除,则能够模拟 Toast 中 addView 和 removeView 操作……
  • 视频通话 Activity 如何最小化

    • Activity 自身自带了一个 moveTaskToBack(boolean nonRoot),咱们要实现最小化只须要调用 moveTaskToBack(true)传入一个 true 值就能够了,然而这里有一个前提,就是须要设置 Activity 的启动模式为 singleInstance 模式,两步搞定。
    • 注:activity 最小化后从新从后盾回到前台会回调 onRestart()办法。点击悬浮窗开启 activity 会回调 onNewIntent(留神能够 setIntent(intent)一下)

1.3 根底概念

  • Window 有三种类型,别离是利用 Window、子 Window 和零碎 Window。

    • 利用 Window:z-index 在 1~99 之间,它往往对应着一个 Activity。
    • 子 Window:z-index 在 1000~1999 之间,它往往不能独立存在,须要附丽在父 Window 上,例如 Dialog 等。
    • 零碎 Window:z-index 在 2000~2999 之间,它往往须要申明权限能力创立,例如 Toast、状态栏、零碎音量条、谬误提示框都是零碎 Window。
  • 这些层级范畴对应着 WindowManager.LayoutParams 的 type 参数

    • 如果想要 Window 位于所有 Window 的最顶层,那么采纳较大的层级即可,很显然零碎 Window 的层级是最大的。
  • Android 显示零碎分为 3 层

    • UI 框架层:负责管理窗口中 View 组件的布局与绘制以及响应用户输出事件
    • WindowManagerService 层:负责管理窗口 Surface 的布局与秩序
    • SurfaceFlinger 层:将 WindowManagerService 治理的窗口依照肯定的秩序显示在屏幕上
  • WMS(WindowManagerService)相干概念

    • Window: 它是一个抽象类,具体实现类为 PhoneWindow,它对 View 进行治理。Window 是 View 的容器,View 是 Window 的具体表现内容;
    • WindowManager: 是一个接口类,继承自接口 ViewManager,从它的名称就晓得它是用来治理 Window 的,它的实现类为 WindowManagerImpl;
    • WMS: 是窗口的管理者,它负责窗口的启动、增加和删除。另外窗口的大小和层级也是由它进行治理的;

1.4 设计指标

  • 目前开发悬浮窗的计划有以下几种

    • 第一种:写在 base 外面或者监听所有 activity 生命周期,这样每次启动一个新的 Activity 都要往页面上 addView 一次,耦合性比拟强。
    • 第二种:采纳在 Window 上增加 View 的模式,相当于是全局性的悬浮窗。封装成库,裸露 Api 给开发者调用。
    • 第三种:采纳服务 Service,而后在 Service 中采纳 WindowManager 增加和移除 View 操作。那么在 Activity 中想要展现弹窗则须要通过播送通信,让 Service 收到播送解决逻辑。移植性比拟弱!
  • 悬浮窗设计指标

    • 良好的接口设计,能够设置各种自定义视图,反对拖动和拖拽吸附到边缘。弱小的 Api 办法和傻瓜式调用链路。
  • 展现悬浮窗是否想 Popup 那样附丽在某控件地位

    • 我在写悬浮窗库时,思考是否想 Popup 那种有 showAsDropDown 办法 Api,能够显示在某个 View 的重心地位,而后在设置 x 和 y 偏移量。这个是能够做到的,加上这个 Api 不便库的弱小应用!

1.5 收益剖析

  • 悬浮窗收益

    • 进步产品的用户体验,app 推到后盾,或者推出页面做其余操作(比方查看信息),这个时候浮窗性能次要是减少通话的敌对
  • 技能收益

    • 下沉为性能根底库,能够不便各个产品线应用,进步开发的效率。防止跟业务解耦合。应用场景有:音视频,直播,debug 悬浮工具等……
  • 悬浮窗库代码

    • https://github.com/yangchong2…

02.Window 概念

2.1 Window 增加 View

  • 先看一个简略的案例。在主屏幕上增加一个 TextView 并展现,并且这个 TextView 独占一个窗口。

    TextView mview = new TextView(context);
    WindowManager mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
    WindowManager.LayoutParams wmParams = new WindowManager.LayoutParams();
    wmParams.type = WindowManager.LayoutParams.TYPE_TOAST;
    wmParams.width = 800;
    wmParams.height = 800;
    mWindowManager.addView(mview, wmParams);
  • 对 Window 增加 View 的流程步骤剖析

    • WindowManager.addView 增加窗口之前,TextView 的 onDraw 不会被调用,也就说 View 必须被增加到窗口中,才会被绘制。只有申请了附丽窗口,View 才会有能够绘制的指标内存。
    • 当 APP 通过 WindowManagerService 的代理向其增加窗口的时候,WindowManagerService 除了本人进行注销整顿,还须要向 SurfaceFlinger 服务申请一块 Surface 画布,其实次要是画布背地所对应的一块内存,只有这一块内存申请胜利之后,APP 端才有绘图的指标,并且这块内存是 APP 端同 SurfaceFlinger 服务端共享的,这就省去了绘图资源的拷贝。
    • APP 端是能够通过 unLockCanvasAndPost 间接同 SurfaceFlinger 通信进行重绘的,就是说图形的绘制同 WMS 没有关系,WMS 只是负责窗口的治理,并不负责窗口的绘制。

2.2 Window 的概念

  • Window 是个抽象类,PhoneWindow 是 Window 惟一的实现类。PhoneWindow 像是一个工具箱,封装了三种工具:

    • DecorView、WindowManager.LayoutParams、WindowManager。
    • 其中 DecorView 和 WindowManager.LayoutParams 负责窗口的动态属性,比方窗口的题目、背景、输入法模式、屏幕方向等等。WindowManager 负责窗口的动静操作,比方窗口的增、删、改。
    • Window 抽象类对 WindowManager.LayoutParams 相干的属性(如:输入法模式、屏幕方向)都提供了具体的办法。而对 DecorView 相干的属性(如:题目、背景),只提供了形象办法,这些形象办法由 PhoneWindow 实现。
  • Window 并不是实在地存在着的,而是以 View 的模式存在。

    • Window 自身就只是一个形象的概念,而 View 是 Window 的表现形式。要想显示窗口,就必须调用 WindowManager.addView(View view, ViewGroup.LayoutParams params)。
    • 参数 view 就代表着一个窗口。在 Activity 和 Dialog 的显示过程中都会调用到 wm.addView(decor, l);所以 Activity 和 Dialog 的 DecorView 就代表着各自的窗口。

2.3 WindowManager

  • 在理解 WindowManager 治理 View 实现之前,先理解下 WindowManager 相干类图以及 Activity 界面各层级显示关系;

2.4 LayoutParams

  • WindowManager.LayoutParams 这个类用于提供悬浮窗所需的参数,其中有几个常常会用到的变量:

    • type 值用于确定悬浮窗的类型,个别设为 2002,示意在所有应用程序之上,但在状态栏之下。
    • flags 值用于确定悬浮窗的行为,比如说不可聚焦,非模态对话框等等,属性十分多,大家能够查看文档。
    • gravity 值用于确定悬浮窗的对齐形式,个别设为左上角对齐,这样当拖动悬浮窗的时候不便计算坐标。
    • x 值用于确定悬浮窗的地位,如果要横向挪动悬浮窗,就须要扭转这个值。
    • y 值用于确定悬浮窗的地位,如果要纵向挪动悬浮窗,就须要扭转这个值。
    • width 值用于指定悬浮窗的宽度。
    • height 值用于指定悬浮窗的高度。
  • 那么这个外面如何计算悬浮窗上下左右的地位呢?比方有个场景悬浮窗和音视频页面放大和放大就须要拿到悬浮窗地位

    • 一般 View 如何拿到上下左右地位,能够采纳 sourceView.getGlobalVisibleRect(visibleRect),简略来说就是对指标 view 在父 view 映射,而后从屏幕左上角开始计算,而后保留到 rect 中。
    • 悬浮窗 View 如何拿到上下左右地位,left = layoutParams.x;top = y,right = x + layoutParams.width;bottom = y + layoutParams.height

03. 悬浮窗技术要点

3.1 业务思考点剖析

  • 针对窗口放大或者悬浮窗须要思考几个重要的点:

    • 悬浮窗体的比例以及层级,层级要在 statusBar 之下且在 activity 之上,这样能力保障其不会被其余业务界面笼罩;
    • 悬浮框显示后,外部的内容如何无缝连接持续显示;

3.2 关键技术要点

  • 悬浮窗权限判断

    • 这个须要留神针对不同的版本须要适配权限。留神网上说有什么办法能够绕过权限申请,这个是不可能的事件。同时要留神,局部手机判断悬浮窗权限 Api 可能生效……
  • 将 view 增加到悬浮窗上

    • 利用 addView 将 View 增加在 window 上,同样的,WindowManager.LayoutParams.type 能够设置 View 的层级,避免被其余业务界面所笼罩。

3.3 利用悬浮窗

  • 利用内悬浮窗实现流程

    • 1. 获取 WindowManager;2. 创立悬浮 View;3. 设置悬浮 View 的拖拽事件;4. 增加 View 到 WindowManager 中
  • 对于利用悬浮窗来说,Android 版本对其影响不大。

    • Type 为 TYPE_APPLICATION:只有 Activity 建设了,就能够增加。
    • Type 为 TYPE_APPLICATION_ATTACHED_DIALOG:须要在 Activity 获取焦点,并且用户可操作时才可增加。

3.4 增加浮窗源码流程

  • 悬浮窗增加流程:

    • -> WindowManager.addView 这个是调用 ViewManager 接口的 addView 办法增加视图
    • -> WindowManagerImpl.addView 接着会调用具体实现类
    • -> WindowManagerGlobal.addView 在这个办法中会找到外围的 ViewRootImpl,这个 Impl 相当于是 root 根
    • -> ViewRootImpl.setView 最初会调用 setView 将 view 设置进去,mWindowSession 在创立 ViewRootImpl 对象时被实例化
    • -> WindowSession.addToDisplay(AIDL 进行 IPC)
    • -> WindowManagerService.addWindow()
    • -> ViewRootImpl.setView
  • 从 WindowManager 到 WMS 的具体流程如下所示:

  • 这里解说一下 AIDL 交互的流程逻辑

    • 次要剖析是 ViewRootImpl#setView()到 WindowManagerService.addWindow()的这个过程,波及到跨过程通信。
    • 1.ViewRootImpl#setView()过程。mWindowSession 是 IWindowSession 对象。在创立 ViewRootImpl 对象时被实例化。
    • 2.WindowManagerGlobal#getWindowSession()过程。getWindowManagerService()通过 AIDL 返回 WindowManagerService 实例。之后调用 WindowManagerService#openSession()。
    • 3.WindowManagerService#openSession()过程。返回一个 Session 对象。也就是说在 ViewRootImpl#setView()中调用的是 mWindowSession.addToDisplay,其实就是 Session#addToDisplay()。
    • 4.Session#addToDisplay()过程。mService 是个 WindowManagerService 对象,也就是说最初调用的是 WindowManagerService#addWindow()
    • 5.WindowManagerService#addWindow()过程。mWindowMap 是个 Map 实例,将 WindowManager 增加进 WindowManagerService 对立治理。至此,整个增加视图操作解析结束。
  • WindowManager.updateViewLayout()解析

    • 和 addView()过程一样,最终会进入到 WindowManagerGlobal#updateViewLayout()。将传入的 View 设置参数之后,更新 mRoot 中 View 的参数。
  • WindowManager.removeView()解析

    • 和下面过程一样,最终会进入到 WindowManagerGlobal#removeView()。这个过程要略微麻烦点,首先调用 root.die(),接着将 View 增加进 mDyingViews。
    • ViewRootImpl#die()中,参数 immediate 默认为 false, 也就是说这里只是发送了一个 what=MSG_DIE 的空音讯。ViewRootHandler 收到这条音讯会执行 doDie()。
    • 通过一圈效验最终还是回到 WindowManagerGlobal 中移除 View

3.6 拖拽回弹吸附

  • 先看微信成果

    • 当你拖动微信悬浮窗的时候,手指松开,这个时候悬浮窗回到边缘,会有一个很敌对的动画过渡成果。而并非是扭转地位那么僵硬。
  • 为何做该性能

    • 拖拽回到边缘,如果是间接调用 updateLocation,那太僵硬了。
  • 如何做敌对动画

    • 这里能够增加属性动画,给动画设置工夫,而后在动画执行获取坐标值。而后再更改地位,这样就比拟连贯,成果更好一些。

04. 开发重要步骤

4.1 悬浮窗实现流程

  • 利用内悬浮窗实现流程

    • 第一个是获取 WindowManager,而后设置相干 params 参数。留神配置参数的时候须要留神 type
    • 第二个是增加 xml 或者自定义 view 到 windowManager 上
    • 第三个是解决拖拽更改 view 地位的监听逻辑,别离在 down,move,up 三个事件处理业务
    • 第四个是吸附右边或者左边,大略的思路是判断手指抬起时候的点是在屏幕右边还是左边

4.2 申请悬浮窗权限

  • 对于悬浮窗的权限

    • 当 API<18 时,零碎默认是有悬浮窗的权限,不须要去解决;
    • 当 API >= 23 时,须要在 AndroidManifest 中申请权限,为了避免用户手动在设置中勾销权限,须要在每次应用时 check 一下是否有悬浮窗权限存在;

      Settings.canDrawOverlays(this)
    • 当 API > 25 时,零碎间接禁止用户应用 TYPE_TOAST 创立悬浮窗。

      <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

4.3 初始化悬浮窗

  • 第一步:首先创立 WindowManager

    // 创立 WindowManager
    windowManager = (WindowManager)applicationContext.getSystemService(Context.WINDOW_SERVICE);
    layoutParams = new WindowManager.LayoutParams();

4.4 设置悬浮窗参数

  • 第一步:创立 LayoutParams

    layoutParams = new WindowManager.LayoutParams();
  • 第二步:LayoutParam 设置

    wmParams.type = WindowManager.LayoutParams.TYPE_TOAST;
    wmParams.width = 800;
    wmParams.height = 800;
    mWindowManager.addView(mview, wmParams);

4.5 增加 View 到悬浮窗

  • 界面触发悬浮窗代码如下:

    // 新建悬浮窗控件
    View view = LayoutInflater.from(this).inflate(R.layout.float_window, null);
    view.setOnTouchListener(new FloatingOnTouchListener());
    // 将悬浮窗控件增加到 WindowManager
    windowManager.addView(view, layoutParams);
  • 须要留神的是,在暗藏悬浮窗的时候,最好是移除一下,下次须要显示的时候再增加。

4.6 悬浮窗拖拽实现

  • 如何实现悬浮窗可随手指拖动?

    • 思路非常简单,监听悬浮窗那个 onTouchListener 即可,在刚点击的 ACTION_DOWN(手指按下)事件中记录以后的 x,y 地位,而后在每次挪动 (ACTION_MOVE 事件) 后获取到本次挪动的地位,二者相减就是须要挪动的地位,这是自定义 view 的最基本操作了。
  • 如何实现悬浮窗左右边的吸顶成果?

    • 监听到手指抬起 (UP 事件) 的动作后,判断以后地位是凑近右边还是左边,凑近右边就以地位动画的形式平移到右边,凑近左边就平移到左边。

4.8 悬浮窗权限适配

  • 权限配置和申请,这一块倒是没什么坑

    • 在当 Android7.0 以上的时候,须要在 AndroidManifest.xml 文件中申明 SYSTEM_ALERT_WINDOW 权限

      <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
      <uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW" />

4.9 LayoutParam 坑

  • LayoutParam 的坑!!!!

    • WindowManager 的 addView 办法有两个参数,一个是须要退出的控件对象 View,另一个参数是 WindowManager.LayoutParam 对象。
    • LayoutParam 里的 type 变量。须要留神一个坑!!!!!!这个变量是用来指定窗口类型的。在设置这个变量时,须要对不同版本的 Android 零碎进行适配。

      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;} else {layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;}
  • 在 Android 8.0 之前,悬浮窗口设置能够为 TYPE_PHONE,这种类型是用于提供用户交互操作的非应用窗口。

    • 而 Android 8.0 对系统和 API 行为做了批改,包含应用 SYSTEM_ALERT_WINDOW 权限的利用无奈再应用一下窗口类型来在其余利用和窗口上方显示揭示窗口:
    • 如果须要实现在其余利用和窗口上方显示揭示窗口,那么必须该为 TYPE_APPLICATION_OVERLAY 的新类型。
  • 如果在 Android 8.0 以上版本依然应用 TYPE_PHONE 类型的悬浮窗口,则会呈现如下异样信息:

    android.view.WindowManager$BadTokenException: Unable to add window android.view.ViewRootImpl$W@f8ec928 -- permission denied for window type 2002

05. 计划根底设计

5.1 整体架构图

5.2 UML 设计图

  • 悬浮窗整体 UML 类图

06. 其余设计说明

6.1 性能设计

  • 性能设计在该库中次要波及两点

    • 第一个如果是用在 activity 中,那么则须要留神内存透露的问题,须要开释 activity 上下文的援用
    • 第二个如果是用在全局,那么须要留神增加 view 防止反复增加(如果曾经增加则首先要移除),而后销毁的时候把 FloatWindow 各种属性设置成 null 清理

6.2 稳定性设计

  • 如何防止窗口挪动,挪动后松手的霎时触发了点击事件

    • 首先设置一个布尔标记值(触摸挪动标记),在手指按上来(ACTION_DOWN)的时候设置为 false。
    • 而后在挪动(ACTION_MOVE)的时候,如果用户挪动了手指,那么就拦挡本次触摸事件,从而不让点击事件失效。
    • 最初在手指抬起(ACTION_UP,ACTION_CANCEL)的时候,返回记录的触摸挪动标记。如果是 true 示意本人生产事件,则不会让点击事件失效。
  • 这个中央须要留神两点

    • 第一点:为何要监听 ACTION_CANCEL 事件,是因为手指拖动,疾速拖动到窗口外,这个时候没有手指抬起操作,那么监听事件完结次要是加强边界逻辑。
    • 第二点:怎么判断滑动?因为点击 click 也会执行 down,move,up 等一连串事件。这个时候就要判断最小 move 间隔是否大于零碎最小触摸间隔,如果是则为拖动,否则是点击!
  • 如何解决滑出指定间隔又滑入当作是点击事件 bug

    • 这个这个,能够当作一种加强逻辑,然而然而手指操作不进去,先放着……

6.3 异样设计

  • 针对悬浮窗的增加,移除和更新操作须要减少 catch 操作。那么为何要这样操作,模拟吐司。如下所示:

    try {mWindowManager.addView(mDecorView, mWindowParams);
    } catch (NullPointerException | IllegalStateException |
            IllegalArgumentException | WindowManager.BadTokenException e) {
        // 如果这个 View 对象被反复增加到 WindowManager 则会抛出异样
        // java.lang.IllegalStateException: View has already been added to the window manager.
    }
    
    // 上面这个是更新 view
    try {mWindowManager.updateViewLayout(mDecorView, mWindowParams);
    } catch (IllegalArgumentException e) {
        // 当 WindowManager 曾经隐没时调用会产生解体
        // IllegalArgumentException: View not attached to window manager
    }
    • 参考零碎级别的 Toast,其实悬浮窗跟吐司一样,设置零碎层级后,对 addView 减少 catch 操作。

      try {mWM.addView(mView, mParams);
      } catch (WindowManager.BadTokenException e) {/* ignore */}

6.4 事件上报设计

  • 在悬浮窗中,有一部分代码增加上了 catch 操作。那么是否把这一部分的异样当作事件上报到 APM 上来

    • 第一种计划:依赖 APM,而后调用 api 进行事件上报,显然这种是不可行的。因为该性能库是不想依赖太大的内部库。
    • 第二种计划:采纳接口 + 实现类,通过反射的模式去调用。但这样又感觉不太好,采纳 Class.forName 要防止混同导致类找不到。
    • 第三种计划:采纳抽象类 + 实现类,将实现类的对象设置到抽象类中调用,实现类在壳工程做具体操作。
  • 具体实现步骤如下所示

    • 举一个简略的例子阐明该思路,比方,我在悬浮窗依赖接口层,而后调用代码如下所示

      ExceptionReporter.reportCrash("Float FloatWindow updateViewLayout", e);
  • 而后,在 app 壳工程中具体操作如下所示

    ExceptionReporter.setExceptionReporter(ExceptionHelperImpl())
    public class ExceptionReporterImpl extends ExceptionReporter {
        @Override
        protected void reportCrash(Throwable throwable) {// 壳工程中能够拿到 APM,比方上传到 bugly 平台上}
    
        @Override
        protected void reportCrash(String tag, Throwable throwable) {}}

07. 遇到的问题和坑

7.1 解决输入法层级关系

  • 先看一下问题

    • 微信里的悬浮窗是在输入法之下的,所以交互的同学也要求悬浮窗也要在输入法之下。查看了一下 WindowManager 源码,悬浮窗的优先级 TYPE_APPLICATION_OVERLAY,下面大字写着明明是在输入法之下的,然而理论体现是在输入法之上。

7.2 边界逻辑敞开悬浮窗

  • 先看一下问题

    • 谷歌坑人的中央,都没中央设置这个悬浮窗是否只用到 app 内,所以默认在桌面上也会显示本人的悬浮窗。
    • 比方在微信里显示其余 app 的悬浮窗,这种蹩脚的体验可想而知,用户不给你卸载就真是奇观了。
  • 尝试解决这个问题

    • 为了解决这个问题,最后的实现形式是对所有通过的 activity 进行记录,显示就加 1,页面被挂起就减 1,如果减到以后计数为 0 时阐明所有页面曾经敞开了,就能够暗藏悬浮窗了。
    • 实际上这么做还是有问题的,在局部手机上如果是在首页按返回键的话依然不能暗藏,这个又是零碎级的兼容性问题。
    • 为了解决这问题,前面又做了一个解决,通过注册 registerActivityLifecycleCallbacks 监听 app 的前后台回调,检测到如果以后首页被销毁时,应该将悬浮窗进行暗藏。

7.3 点击屡次关上页面

  • 问题阐明一下

    • 如果你的悬浮窗点击事件是关上页面的话,这里须要留神了,别忘了将这个关上的页面的启动模式设置为 singleTop 或者是 singleTask,从而复用同一个,远离始终按返回的天堂操作。

7.4 Home 键遇到的问题

  • 先说一下遇到问题的场景

    • 按 home 退到桌面从桌面点击利用图标又从启动页重新启动的,挺奇怪的。点击 home 键按道理说是不会推出 MainActivity 的呀
  • 先说下代码逻辑

    • 语音 / 视频通话界面 activity 配置 android:launchMode=“singleInstance”模式,切换到悬浮框调用 moveTaskToBack(true)办法,能启动小窗口,通话页面退到后盾。
  • 调试中发现的问题

    • 通话界面按 home 键,之前的 activity 销毁了,日志发现走了 onDestroy,从新点击 app 图标,MainActivity 相干页面从新 onCreate(相当于重新启动 app 了)。
    • 因为通话页面是 singleInstance 模式,此时有两个工作栈,按 Home 键后再从工作程序中切回,此时利用只保留了第二个工作栈,曾经失去了和第一个工作栈的关系,finish 之后无奈在回到第一个工作栈。
  • 该问题解决方案

    • 给通话界面设置 taskAffinity,如果不设置的话,按下 home 键时零碎会清理最近不流动的和 application 雷同的 taskAffinity 的所有处于后盾的栈,taskAffinity 默认与 application 是同一个。
    • 给通话页面设置 taskAffinity 之后,MainActivity 所在后盾栈就不会被清理。须要留神:若想在 taskAffinity 属性失效,须要在启动该 Activity 时设置 Flag 为 FLAG_ACTIVITY_NEW_TASK。

封装库:https://github.com/yangchong2…

公共组件层:https://github.com/yangchong2…

退出移动版