從源碼角度解析懸浮窗

今天我們從源碼角度來一探 Android 懸浮窗的究竟前标。

一遵湖、如何創(chuàng)建一個(gè)懸浮窗

  1. 在 AndroidManifest.xml 中聲明權(quán)限 SYSTEM_ALERT_WINDOW容诬;
  2. 新建一個(gè) Service, 在其 onCreate() 函數(shù)中添加如下代碼:
@Override
public void onCreate() {
    super.onCreate();

    // WindowManager
    final WindowManager windowManager =
        (WindowManager) getSystemService(Context.WINDOW_SERVICE);

    // 定義一個(gè)按鈕
    final Button btn = new Button(getBaseContext());
    btn.setText("Dismiss Me");
    btn.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            wm.removeView(btn);
            stopSelf();
        }
    });

    // 布局參數(shù)
    WindowManager.LayoutParams p = new WindowManager.LayoutParams();
    p.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
    p.gravity = Gravity.TOP;
    p.width = WRAP_CONTENT;
    p.height = WRAP_CONTENT;
    p.x = 0;
    p.y = 0;
    p.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
    
    // 添加按鈕到 WindowManager
    windowManager.addView(btn, p);
}

當(dāng)我們的 Service 被啟動(dòng)后盲赊,手機(jī)屏幕頂部就會(huì)顯示一個(gè)按鈕坦胶,點(diǎn)擊此按鈕后杨凑,它會(huì)消失滤奈。

s1.png

二、如何定制懸浮窗撩满?

從上述代碼片段可知蜒程,定制懸浮窗的關(guān)鍵在于 WindowManager.LayoutParams 這個(gè)類,
我們來看看幾個(gè)主要的成員伺帘。

  • x, y
    窗口的絕對(duì)位置(考慮gravity屬性)
  • width, height
    窗口的尺寸
  • gravity
    窗口的布局方式
  • type
    窗口的類型昭躺,值越大顯示的位置越在上面
// type 共分三大類:

/* --------- Application Window ---------- */
//  id 從 1 到 99, 目前有效值 1~3
//  Activity -> TYPE_APPLICATION(2)(需要 token 設(shè)置成 Activity 的 token)

//開始應(yīng)用程序窗口
public static final int FIRST_APPLICATION_WINDOW = 1;
//所有程序窗口的base窗口,其他應(yīng)用程序窗口都顯示在它上面
public static final int TYPE_BASE_APPLICATION   = 1;
//普通應(yīng)用程序窗口伪嫁,token必須設(shè)置為Activity的token來指定窗口屬于誰
public static final int TYPE_APPLICATION        = 2;
//應(yīng)用程序啟動(dòng)時(shí)所顯示的窗口领炫,應(yīng)用自己不要使用這種類型,它被系統(tǒng)用來顯示一些信息张咳,直到應(yīng)用程序可以開啟自己的窗口為止
public static final int TYPE_APPLICATION_STARTING = 3;
//結(jié)束應(yīng)用程序窗口
public static final int LAST_APPLICATION_WINDOW = 99;


/* --------- Sub Window ---------- */
//  id 從 1000 到 1999, 目前有效值 1000~1005
// (與頂層窗口相關(guān)聯(lián)帝洪,需將 token 設(shè)置成它所附著宿主窗口的 token)
//  PopupWindow -> TYPE_APPLICATION_PANEL(1000)

//SubWindows子窗口,子窗口的Z序和坐標(biāo)空間都依賴于他們的宿主窗口
public static final int FIRST_SUB_WINDOW = 1000;
// 面板窗口脚猾,顯示于宿主窗口的上層
public static final int TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW;
//媒體窗口(例如視頻)葱峡,顯示于宿主窗口下層
public static final int TYPE_APPLICATION_MEDIA = FIRST_SUB_WINDOW+1;
//應(yīng)用程序窗口的子面板,顯示于所有面板窗口的上層
public static final int TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW+2;
//對(duì)話框龙助,類似于面板窗口砰奕,繪制類似于頂層窗口,而不是宿主的子窗口
public static final int TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW+3;
//媒體信息泌参,顯示在媒體層和程序窗口之間脆淹,需要實(shí)現(xiàn)半透明效果
public static final int TYPE_APPLICATION_MEDIA_OVERLAY = FIRST_SUB_WINDOW+4;
//子窗口結(jié)束
public static final int LAST_SUB_WINDOW = 1999;

/* --------- System Window ---------- */
// id 從 2000 到 2999, 目前有效值 2000~2033
// 系統(tǒng)功能使用,不能用于應(yīng)用程序沽一,使用時(shí)需要特殊權(quán)限盖溺,它是特定的系統(tǒng)功能才能使用;

//系統(tǒng)窗口铣缠,非應(yīng)用程序創(chuàng)建
public static final int FIRST_SYSTEM_WINDOW     = 2000;
//狀態(tài)欄烘嘱,只能有一個(gè)狀態(tài)欄,位于屏幕頂端蝗蛙,其他窗口都位于它下方
public static final int TYPE_STATUS_BAR         = FIRST_SYSTEM_WINDOW;
//搜索欄蝇庭,只能有一個(gè)搜索欄,位于屏幕上方
public static final int TYPE_SEARCH_BAR         = FIRST_SYSTEM_WINDOW+1;
//電話窗口捡硅,它用于電話交互(特別是呼入)哮内,置于所有應(yīng)用程序之上,狀態(tài)欄之下
public static final int TYPE_PHONE              = FIRST_SYSTEM_WINDOW+2;
//系統(tǒng)提示,出現(xiàn)在應(yīng)用程序窗口之上
public static final int TYPE_SYSTEM_ALERT       = FIRST_SYSTEM_WINDOW+3;
//鎖屏窗口
public static final int TYPE_KEYGUARD           = FIRST_SYSTEM_WINDOW+4;
//信息窗口北发,用于顯示Toast
public static final int TYPE_TOAST              = FIRST_SYSTEM_WINDOW+5;
//系統(tǒng)頂層窗口纹因,顯示在其他一切內(nèi)容之上,此窗口不能獲得輸入焦點(diǎn)琳拨,否則影響鎖屏
public static final int TYPE_SYSTEM_OVERLAY     = FIRST_SYSTEM_WINDOW+6;
//電話優(yōu)先瞭恰,當(dāng)鎖屏?xí)r顯示,此窗口不能獲得輸入焦點(diǎn)狱庇,否則影響鎖屏
public static final int TYPE_PRIORITY_PHONE     = FIRST_SYSTEM_WINDOW+7;
//系統(tǒng)對(duì)話框
public static final int TYPE_SYSTEM_DIALOG      = FIRST_SYSTEM_WINDOW+8;
//鎖屏?xí)r顯示的對(duì)話框
public static final int TYPE_KEYGUARD_DIALOG    = FIRST_SYSTEM_WINDOW+9;
//系統(tǒng)內(nèi)部錯(cuò)誤提示惊畏,顯示于所有內(nèi)容之上
public static final int TYPE_SYSTEM_ERROR       = FIRST_SYSTEM_WINDOW+10;
//內(nèi)部輸入法窗口,顯示于普通UI之上密任,應(yīng)用程序可重新布局以免被此窗口覆蓋
public static final int TYPE_INPUT_METHOD       = FIRST_SYSTEM_WINDOW+11;
//內(nèi)部輸入法對(duì)話框颜启,顯示于當(dāng)前輸入法窗口之上
public static final int TYPE_INPUT_METHOD_DIALOG= FIRST_SYSTEM_WINDOW+12;
//墻紙窗口
public static final int TYPE_WALLPAPER          = FIRST_SYSTEM_WINDOW+13;
//狀態(tài)欄的滑動(dòng)面板
public static final int TYPE_STATUS_BAR_PANEL   = FIRST_SYSTEM_WINDOW+14;
//安全系統(tǒng)覆蓋窗口,這些窗戶必須不帶輸入焦點(diǎn)批什,否則會(huì)干擾鍵盤
public static final int TYPE_SECURE_SYSTEM_OVERLAY = FIRST_SYSTEM_WINDOW+15;
//拖放偽窗口农曲,只有一個(gè)阻力層(最多),它被放置在所有其他窗口上面
public static final int TYPE_DRAG               = FIRST_SYSTEM_WINDOW+16;
//狀態(tài)欄下拉面板
public static final int TYPE_STATUS_BAR_SUB_PANEL = FIRST_SYSTEM_WINDOW+17;
//鼠標(biāo)指針
public static final int TYPE_POINTER = FIRST_SYSTEM_WINDOW+18;
//導(dǎo)航欄(有別于狀態(tài)欄時(shí))
public static final int TYPE_NAVIGATION_BAR = FIRST_SYSTEM_WINDOW+19;
//音量級(jí)別的覆蓋對(duì)話框驻债,顯示當(dāng)用戶更改系統(tǒng)音量大小
public static final int TYPE_VOLUME_OVERLAY = FIRST_SYSTEM_WINDOW+20;
//起機(jī)進(jìn)度框,在一切之上
public static final int TYPE_BOOT_PROGRESS = FIRST_SYSTEM_WINDOW+21;
//假窗形葬,消費(fèi)導(dǎo)航欄隱藏時(shí)觸摸事件
public static final int TYPE_HIDDEN_NAV_CONSUMER = FIRST_SYSTEM_WINDOW+22;
//夢(mèng)想(屏保)窗口合呐,略高于鍵盤
public static final int TYPE_DREAM = FIRST_SYSTEM_WINDOW+23;
//導(dǎo)航欄面板(不同于狀態(tài)欄的導(dǎo)航欄)
public static final int TYPE_NAVIGATION_BAR_PANEL = FIRST_SYSTEM_WINDOW+24;
//universe背后真正的窗戶
public static final int TYPE_UNIVERSE_BACKGROUND = FIRST_SYSTEM_WINDOW+25;
//顯示窗口覆蓋,用于模擬輔助顯示設(shè)備
public static final int TYPE_DISPLAY_OVERLAY = FIRST_SYSTEM_WINDOW+26;
//放大窗口覆蓋笙以,用于突出顯示的放大部分可訪問性放大時(shí)啟用
public static final int TYPE_MAGNIFICATION_OVERLAY = FIRST_SYSTEM_WINDOW+27;
//......
public static final int TYPE_KEYGUARD_SCRIM           = FIRST_SYSTEM_WINDOW+29;
public static final int TYPE_PRIVATE_PRESENTATION = FIRST_SYSTEM_WINDOW+30;
public static final int TYPE_VOICE_INTERACTION = FIRST_SYSTEM_WINDOW+31;
public static final int TYPE_ACCESSIBILITY_OVERLAY = FIRST_SYSTEM_WINDOW+32;
//系統(tǒng)窗口結(jié)束
public static final int LAST_SYSTEM_WINDOW      = 2999;
  • flags
    flags 和 type 可以共同決定 Window 的表示和行為
// FLAG_ALLOW_LOCK_WHILE_SCREEN_ON:
// 當(dāng)窗口可見時(shí)淌实,允計(jì)鎖屏界面出現(xiàn)。

// FLAG_DIM_BEHIND:
// 窗口之后的內(nèi)容變暗

// FLAG_NOT_FOCUSABLE:
// 不能獲得按鍵輸入焦點(diǎn)猖腕,所以不能向它發(fā)送按鍵或按鈕事件拆祈。那些事件將發(fā)送給它后面的可以獲得焦點(diǎn)的窗口。
// 此選項(xiàng)還會(huì)設(shè)置 FLAG_NOT_TOUCH_MODAL 選項(xiàng)倘感。設(shè)置此選項(xiàng)放坏,意味著窗口不能與軟輸入法進(jìn)行交互,
// 所以它的 Z 序獨(dú)立于任何活動(dòng)的輸入法(它可以全屏顯示老玛,如果需要的話淤年,可覆蓋輸入法窗口)。
// 要修改這一行為蜡豹,可參考 FLAG_ALT_FOCUSALBE_IM 選項(xiàng)麸粮。

// FLAG_NOT_TOUCHABLE:
// 不接受觸摸屏事件

// FLAG_NOT_TOUCH_MODAL:
// 當(dāng)窗口可以獲得焦點(diǎn)(沒有設(shè)置 FLAG_NOT_FOCUSALBE 選項(xiàng))時(shí),仍然將窗口范圍之外的點(diǎn)設(shè)備事件
//(鼠標(biāo)镜廉、觸摸屏)發(fā)送給后面的窗口處理弄诲。否則它將獨(dú)占所有的點(diǎn)設(shè)備事件,而不管它們是不是發(fā)生在窗口范圍之內(nèi)娇唯。

// FLAG_TOUCHABLE_WHEN_WAKING:
// 如果設(shè)置了這個(gè)標(biāo)志齐遵,當(dāng)設(shè)備休眠時(shí)寂玲,點(diǎn)擊觸摸屏,設(shè)備將收到這個(gè)第一觸摸事件洛搀。
// 通常第一觸摸事件被系統(tǒng)所消耗敢茁,用戶不會(huì)看到他們點(diǎn)擊屏幕有什么反應(yīng)。

// FLAG_KEEP_SCREEN_ON
// 當(dāng)此窗口為用戶可見時(shí)留美,保持設(shè)備常開彰檬,并保持亮度不變。

// FLAG_LAYOUT_IN_SCREEN:
// 窗口占滿整個(gè)屏幕谎砾,忽略周圍的裝飾邊框(例如狀態(tài)欄)逢倍。此窗口需考慮到裝飾邊框的內(nèi)容。

// FLAG_LAYOUT_NO_LIMITS:
// 允許窗口擴(kuò)展到屏幕之外

// FLAG_FULLSCREEN:
// 窗口顯示時(shí)景图,隱藏所有的屏幕裝飾(例如狀態(tài)條)较雕。使窗口占用整個(gè)顯示區(qū)域。

// FLAG_FORCE_NOT_FULLSCREEN:
// 此選項(xiàng)將覆蓋 FLAG_FULLSCREEN 選項(xiàng)挚币,并強(qiáng)制屏幕裝飾(如狀態(tài)條)彈出亮蒋。

// FLAG_SECURE:
// 不允許屏幕截圖

// FLAG_SCALED:
// 一種特殊模式,布局參數(shù)用于指示顯示比例

// FLAG_IGNORE_CHEEK_PRESSES:
// 當(dāng)屏幕有可能貼著臉時(shí)妆毕,這一選項(xiàng)可防止面頰對(duì)屏幕造成誤操作

// FLAG_LAYOUT_INSET_DECOR:
// 當(dāng)請(qǐng)求布局時(shí)慎玖,你的窗口可能出現(xiàn)在狀態(tài)欄的上面或下面,從而造成遮擋笛粘。
// 當(dāng)設(shè)置這一選項(xiàng)后趁怔,窗口管理器將確保窗口內(nèi)容不會(huì)被裝飾條(狀態(tài)欄)蓋住。 

// public static final int FLAG_ALT_FOCUSABLE_IM:
// 如果同時(shí)設(shè)置了FLAG_NOT_FOCUSABLE選項(xiàng)和本選項(xiàng)薪前,窗口將能夠與輸入法交互润努,允許輸入法窗口覆蓋; 
// 如果FLAG_NOT_FOCUSABLE沒有設(shè)置而設(shè)置了本選項(xiàng)示括,窗口不能與輸入法交互铺浇,可以覆蓋輸入法窗口。 


// FLAG_WATCH_OUTSIDE_TOUCH:
// 如果你設(shè)置了FLAG_NOT_TOUCH_MODAL例诀,那么當(dāng)觸屏事件發(fā)生在窗口之外時(shí)随抠,
// 可以通過設(shè)置此標(biāo)志接收到一個(gè)MotionEvent.ACTION_OUTSIDE事件。
// 注意繁涂,你不會(huì)收到完整的down/move/up事件拱她,只有第一次down事件時(shí)可以收到ACTION_OUTSIDE。 

// FLAG_SHOW_WHEN_LOCKED:
// 當(dāng)屏幕鎖定時(shí)扔罪,窗口可以被看到秉沼。這使得應(yīng)用程序窗口優(yōu)先于鎖屏界面。
// 可配合 FLAG_KEEP_SCREEN_ON 選項(xiàng)點(diǎn)亮屏幕并直接顯示在鎖屏界面之前。
// 可使用FLAG_DISMISS_KEYGUARD選項(xiàng)直接解除非加鎖的鎖屏狀態(tài)唬复。此選項(xiàng)只用于最頂層的全屏幕窗口矗积。 

// FLAG_SHOW_WALLPAPER:
// 請(qǐng)求系統(tǒng)墻紙顯示在你的窗口后面。窗口必須是半透明的敞咧。

// FLAG_TURN_SCREEN_ON:
// 窗口一旦顯示出來棘捣,系統(tǒng)將點(diǎn)亮屏幕,正如用戶喚醒設(shè)備那樣休建。

// FLAG_DISMISS_KEYGUARD:
// 解除鎖屏乍恐。只有鎖屏界面不是加密的才能解鎖。
// 如果鎖屏界面是加密的测砂,那么用戶解鎖之后才能看到此窗口茵烈,除非設(shè)置了FLAG_SHOW_WHEN_LOCKED選項(xiàng)。 

// FLAG_SPLIT_TOUCH:

// FLAG_HARDWARE_ACCELERATED:
// 硬件加速

// FLAG_LOCAL_FOCUS_MODE:

// FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS:

示例:

  • 使懸浮窗可以覆蓋通知欄
    type = TYPE_SYSTEM_ERROR
    flags = FLAG_LAYOUT_IN_SCREEN
  • 使懸浮窗不遮擋輸入法
    FLAG_ALT_FOCUSABLE_IM
  • 使懸浮窗不可點(diǎn)擊(可以獲得焦點(diǎn)砌些,會(huì)攔截 Back, Home 鍵)
    FLAG_NOT_TOUCHABLE
  • 使懸浮窗不可獲得焦點(diǎn)(不攔截 Back, Home 鍵)
    FLAG_NOT_FOCUSABLE

三呜投、懸浮窗是如何顯示到屏幕上的?

我們知道視圖是通過 WindowManager 添加到屏幕上的,所以第一個(gè)要了解的類便是 WindowManager存璃。

// android.view.WindowManager
public interface WindowManager extends ViewManager {
    // 獲取 Display
    public Display getDefaultDisplay();
    // 立即刪除 View
    public void removeViewImmediate(View view);
}

我們看到 WindowManager 繼承自 ViewManager仑荐, 有一個(gè)獲取 DefaultDisplay 的方法和刪除 View 的方法;
再來看看 ViewManager 的代碼:

// android.view.ViewManager
public interface ViewManager {
    public void addView(View view, ViewGroup.LayoutParams params);
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
    public void removeView(View view);
}

非常簡(jiǎn)潔的三個(gè)函數(shù):添加視圖纵东,更新視圖释漆,刪除視圖。
如果再查看一個(gè) ViewManager 的其它實(shí)現(xiàn)類篮迎,還會(huì)發(fā)現(xiàn) ViewGroup 也實(shí)現(xiàn)了這個(gè)接口。

public abstract class ViewGroup extends View implements ViewParent, ViewManager {...}

我們?cè)賮砜纯?WindowManager 的實(shí)現(xiàn)者是誰示姿?

Log.d("wm", windowManager.getClass().getName());
--------------------------------------------------
D/wm      (28372): android.view.WindowManagerImpl

原來是 WindowManagerImpl甜橱,如果有同學(xué)覺見得打 Log 的方式太偷懶了,可以閱讀這篇文章來了解 getSystemService(...) 的實(shí)現(xiàn)原理栈戳。

那我們就來看看 WindowManagerImpl 的 addView(...), updateViewLayout(...), removeView(...) 這三個(gè)函數(shù)吧:

// android.view.WindowManagerImpl
...
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
...
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.addView(view, params, mDisplay, mParentWindow);
}

@Override
public void updateViewLayout(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    mGlobal.updateViewLayout(view, params);
}

@Override
public void removeView(View view) {
    mGlobal.removeView(view, false);
}

這幾個(gè)函數(shù)無一例外地將實(shí)現(xiàn)都轉(zhuǎn)移給了 WindowManagerGlobal:

// android.view.WindowManagerGlobal
public final class WindowManagerGlobal {

    private WindowManagerGlobal() {
    }

    public static WindowManagerGlobal getInstance() {
        synchronized (WindowManagerGlobal.class) {
            if (sDefaultWindowManager == null) {
                sDefaultWindowManager = new WindowManagerGlobal();
            }
            return sDefaultWindowManager;
        }
    }
    ...
}

毫無疑問岂傲,WindowManagerGlobal 使用了單例,所以一個(gè)進(jìn)程中最多只有一個(gè) WindowManager子檀,所有 WindowManager 實(shí)例都是這個(gè) WindowManagerGlobal 實(shí)例的代理镊掖。

到此,我們對(duì) WindowManager 的結(jié)構(gòu)體系便有了一個(gè)概括的了解:


diagram.png

四褂痰、 WindowManagerGlobal 詳解

    private final ArrayList<View> mViews = new ArrayList<View>();
    private final ArrayList<ViewRootImpl> mRoots = new ArrayList<>();
    private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<>();
    private final ArraySet<View> mDyingViews = new ArraySet<>();

WindowManagerGlobal 有 4 個(gè)重要的集合:

  • mView : 根視圖集合
  • mRoots : ViewRootImpl 集合
  • mParams : 根視圖的布局的集合
  • mDyingViews : 要移除和根視圖的集合

我們先來分析 添加亩进、刪除、更新 3 種操作對(duì)這 4 個(gè)集合的影響缩歪。

1. 添加視圖

// android.view.WindowManagerGlobal
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
   ...
   final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
   ...
   ViewRootImpl root;
   View panelParentView = null;
   synchronized (mLock) {
       ...
       int index = findViewLocked(view, false);
       if (index >= 0) {
           if (mDyingViews.contains(view)) {
               // Don't wait for MSG_DIE to make it's way through root's queue.
               mRoots.get(index).doDie();
           } else {
               ...
           }
       }
       ...
       root = new ViewRootImpl(view.getContext(), display);
       view.setLayoutParams(wparams);
       mViews.add(view);
       mRoots.add(root);
       mParams.add(wparams);
   }

   try {
       root.setView(view, wparams, panelParentView);
   } catch (RuntimeException e) {
       // BadTokenException or InvalidDisplayException, clean up.
       synchronized (mLock) {
           final int index = findViewLocked(view, false);
           if (index >= 0) {
               removeViewLocked(index, true);
           }
       }
       throw e;
   }
}

添加視圖時(shí)归薛,做了以下幾件事:

  1. 將 view, viewRootImpl, layoutParams 分別添加到對(duì)應(yīng)的集合中;
  2. 將 layoutParams 設(shè)置給 view;
  3. 將 view 設(shè)置給 viewRootImpl;

2. 更新視圖

    // android.view.WindowManagerGlobal
    public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
        ...
        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
        view.setLayoutParams(wparams);
        synchronized (mLock) {
            int index = findViewLocked(view, true);
            ViewRootImpl root = mRoots.get(index);
            mParams.remove(index);
            mParams.add(index, wparams);
            root.setLayoutParams(wparams, false);
        }
    }

更新視圖比較簡(jiǎn)單:

  1. 設(shè)置 layoutParams 給 view;
  2. 更新 mParams 集合中對(duì)應(yīng)的 layoutParams;
  3. 更新 對(duì)應(yīng) viewRootImpl 的 laoutParams;

3. 刪除視圖

    // android.view.WindowManagerGlobal
    public void removeView(View view, boolean immediate) {
        ...
        synchronized (mLock) {
            int index = findViewLocked(view, true);
            View curView = mRoots.get(index).getView();
            removeViewLocked(index, immediate);
            if (curView == view) {
                return;
            }
        ...
        }
    }

我們看到,刪除視圖最終交給了 removeViewLocked 函數(shù):

    // android.view.WindowManagerGlobal
    private void removeViewLocked(int index, boolean immediate) {
        ViewRootImpl root = mRoots.get(index);
        View view = root.getView();
        ...
        // 檢查是否需要延遲刪除
        boolean deferred = root.die(immediate);
        if (view != null) {
            view.assignParent(null);
            if (deferred) {
                mDyingViews.add(view);
            }
        }
    }

removeViewLocked 函數(shù)會(huì)調(diào)用 ViewRootImpl 的 die() 函數(shù),該函數(shù)返回一個(gè) boolean 值主籍,標(biāo)識(shí)是否需要延遲刪除习贫?如果需要延遲,則將 view 加入 mDyingViews 這個(gè)集合中千元。

接著我們看看 ViewRootImpl 的 die 函數(shù):

    // android.view.ViewRootImpl
    boolean die(boolean immediate) {
        // Make sure we do execute immediately if we are in the middle of a traversal or the damage
        // done by dispatchDetachedFromWindow will cause havoc on return.
        if (immediate && !mIsInTraversal) {
            doDie();
            return false;
        }

        if (!mIsDrawing) {
            destroyHardwareRenderer();
        } else {
            ...
        }
        mHandler.sendEmptyMessage(MSG_DIE);
        return true;
    }
    

    public void handleMessage(Message msg) {
        ...
        case MSG_DIE:
            doDie();
            break;
        ...
    }

我們看到苫昌,所謂的延遲刪除,其實(shí)就是使用了 Handler 消息隊(duì)列機(jī)制幸海,來調(diào)用 doDie() 函數(shù)祟身;
如果是非延遲刪除,則立即調(diào)用 doDie() 函數(shù):

    // android.view.ViewRootImpl
    void doDie() {
        // 必須在主線程
        checkThread();
        synchronized (this) {
            if (mRemoved) {
                return;
            }
            mRemoved = true;
            if (mAdded) {
                // 發(fā)送 DetachedFromWindow 消息事件
                dispatchDetachedFromWindow();
            }

            ...
            mAdded = false;
        }
        WindowManagerGlobal.getInstance().doRemoveView(this);
    }

doDie() 函數(shù)會(huì)發(fā)送 DetachedFromWindow 消息給 View涕烧;然后調(diào)用 WindowManagerGlobal 的 doRemoveView() 函數(shù):

    // android.view.WindowManagerGlobal
    void doRemoveView(ViewRootImpl root) {
        synchronized (mLock) {
            final int index = mRoots.indexOf(root);
            if (index >= 0) {
                mRoots.remove(index);
                mParams.remove(index);
                final View view = mViews.remove(index);
                mDyingViews.remove(view);
            }
        }
        ...
    }

doRemoveView() 函數(shù)的功能一目了然月而,即從 mRoots, mParams, mDyingViews 中將 數(shù)據(jù)移除掉。

五议纯、總結(jié)

  1. 使用懸浮窗時(shí)父款,要注意聲明權(quán)限(個(gè)別機(jī)型有單獨(dú)的懸浮窗權(quán)限管理,如小米)瞻凤;
  2. 懸浮窗是使用 WindowManager 添加到屏幕上的憨攒,WindowManaget.LayoutParams 可以定制懸浮窗的表現(xiàn)和行為;
  3. WindowManager 的實(shí)際實(shí)現(xiàn)是 WindowManagerGlobal;
  4. WindowManagerGlobal 使用列表的形式保存了窗口的關(guān)鍵數(shù)據(jù)阀参,并配合 ViewRootImpl 類完成窗口的添加肝集、更新、刪除操作蛛壳;
  5. 關(guān)于 WindowManager 的部分不要局限于懸浮窗杏瞻,其實(shí)它是整個(gè) Android 系統(tǒng)的窗口管理器。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末衙荐,一起剝皮案震驚了整個(gè)濱河市捞挥,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌忧吟,老刑警劉巖砌函,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異溜族,居然都是意外死亡讹俊,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門煌抒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來仍劈,“玉大人,你說我怎么就攤上這事摧玫《龋” “怎么了绑青?”我有些...
    開封第一講書人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長屋群。 經(jīng)常有香客問我闸婴,道長,這世上最難降的妖魔是什么芍躏? 我笑而不...
    開封第一講書人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任邪乍,我火速辦了婚禮,結(jié)果婚禮上对竣,老公的妹妹穿的比我還像新娘庇楞。我一直安慰自己,他們只是感情好否纬,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開白布吕晌。 她就那樣靜靜地躺著,像睡著了一般临燃。 火紅的嫁衣襯著肌膚如雪睛驳。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評(píng)論 1 284
  • 那天膜廊,我揣著相機(jī)與錄音乏沸,去河邊找鬼。 笑死爪瓜,一個(gè)胖子當(dāng)著我的面吹牛蹬跃,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播铆铆,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼蝶缀,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了薄货?” 一聲冷哼從身側(cè)響起扼劈,我...
    開封第一講書人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎菲驴,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體骑冗,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡赊瞬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了贼涩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片巧涧。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖遥倦,靈堂內(nèi)的尸體忽然破棺而出谤绳,到底是詐尸還是另有隱情占锯,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布缩筛,位于F島的核電站消略,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏瞎抛。R本人自食惡果不足惜艺演,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望桐臊。 院中可真熱鬧胎撤,春花似錦、人聲如沸断凶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽认烁。三九已至肿男,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間砚著,已是汗流浹背次伶。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留稽穆,地道東北人冠王。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像舌镶,于是被迫代替她去往敵國和親柱彻。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容