懸浮窗開發設計實踐

楊充發表於2022-11-22

目錄介紹

  • 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:是視窗的管理者,它負責視窗的啟動、新增和刪除。另外視窗的大小和層級也是由它進行管理的;
    • image

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懸浮工具等……
  • 懸浮窗庫程式碼

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介面各層級顯示關係;

    • image

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 的具體流程如下所示:

    • image
  • 這裡講解一下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類圖

    • image

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...

相關文章