Android懸浮窗操作使用總結(jié)

前陣子公司項(xiàng)目中需要大量使用Android懸浮窗去實(shí)現(xiàn)一些功能疏遏,對(duì)公司之前一團(tuán)糟的代碼結(jié)構(gòu)和面對(duì)國(guó)產(chǎn)Android奇(沙)葩(雕)的機(jī)型適配(對(duì)猪叙,我說(shuō)的奇(沙)葩(雕)機(jī)型就是Vivo)讓我不得不去思考一個(gè)完整的懸浮窗解決方案。在這個(gè)背景下完成了項(xiàng)目任務(wù)后挪凑,做一個(gè)總結(jié),回顧開(kāi)發(fā)的問(wèn)題和經(jīng)驗(yàn)總結(jié)。

不如首先搞個(gè)Demo纺酸!體驗(yàn)Demo地址:https://github.com/SunJenry/FloatWindow

image.png

1.添加懸浮窗原理

添加懸浮窗的原理就是獲取WindowManager,然后調(diào)用WindowManager的 addView(View view, ViewGroup.LayoutParams params)方法即可址否。View就是需要顯示的懸浮窗餐蔬, ViewGroup.LayoutParams包含了是全屏、非全屏和是否需要攔截觸摸事件的參數(shù)佑附。 考慮的懸浮窗存在全屏和非全屏樊诺、攔截觸摸事件和不攔截觸摸事件的情況可以?xún)蓛山M合產(chǎn)生4種情況:
1.全屏攔截觸摸事件
2.全屏不攔截觸摸事件
3.非全屏(wrap_content)攔截觸摸事件
4.非全屏(wrap_content)不攔截觸摸事件

綜合對(duì)懸浮窗增加、移除音同、隱藏词爬、是否可以可鍵盤(pán)交互等等操作可以提出一個(gè)基類(lèi)AbsFloatBase(大家先粗略建立一個(gè)映像,下面的文章會(huì)針對(duì)具體的方法做詳細(xì)解釋?zhuān)?/p>

public abstract class AbsFloatBase {

    public static final String TAG = "AbsFloatBase";

    static final int FULLSCREEN_TOUCHABLE = 1;
    static final int FULLSCREEN_NOT_TOUCHABLE = 2;
    static final int WRAP_CONTENT_TOUCHABLE = 3;
    static final int WRAP_CONTENT_NOT_TOUCHABLE = 4;

    WindowManager.LayoutParams mLayoutParams;

    View mInflate;
    Context mContext;
    WindowManager mWindowManager;
    private boolean mAdded;
    //設(shè)置隱藏時(shí)是否是INVISIBLE
    private boolean mInvisibleNeed = false;
    private boolean mRequestFocus = false;
    int mGravity = Gravity.CENTER_HORIZONTAL | Gravity.CENTER_VERTICAL;
    int mViewMode = WRAP_CONTENT_NOT_TOUCHABLE;
    Handler mHandler = new Handler(Looper.getMainLooper());

    public AbsFloatBase(Context context) {
        mContext = context;

        create();
    }

    /**
     * 設(shè)置隱藏View的方式是否為Invisible权均,默認(rèn)為Gone
     *
     * @param invisibleNeed 是否是Invisible
     */
    public void setInvisibleNeed(boolean invisibleNeed) {
        mInvisibleNeed = invisibleNeed;
    }

    /**
     * 懸浮窗是否需要獲取焦點(diǎn)缸夹,通常獲取焦點(diǎn)后痪寻,懸浮窗可以和軟鍵盤(pán)發(fā)生交互,被覆蓋的應(yīng)用失去焦點(diǎn)虽惭。
     * 例如:游戲?qū)⑹ケ尘耙魳?lè)
     *
     * @param requestFocus
     */
    public void requestFocus(boolean requestFocus) {
        mRequestFocus = requestFocus;
    }

    @CallSuper
    public void create() {
        mWindowManager = (WindowManager) mContext.getApplicationContext().getSystemService(WINDOW_SERVICE);
    }

    @CallSuper
    public synchronized void show() {
        if (mInflate == null)
            throw new IllegalStateException("FloatView can not be null");

        if (mAdded) {
            mInflate.setVisibility(View.VISIBLE);
            return;
        }

        getLayoutParam(mViewMode);

        mInflate.setVisibility(View.VISIBLE);

        try {
            mWindowManager.addView(mInflate, mLayoutParams);
            mAdded = true;
        } catch (Exception e) {
            Log.e(TAG, "添加懸浮窗失斚鹄唷!Q看健9嘶!4殷浴研侣!");
           Toast.makeText(mContext, "添加懸浮窗失敗E谂酢J睢!E乜巍D┦摹!請(qǐng)檢查懸浮窗權(quán)限", Toast.LENGTH_SHORT).show();
        }
    }

    @CallSuper
    public void hide() {
        if (mInflate != null) {
            mInflate.setVisibility(View.INVISIBLE);
        }
    }

    @CallSuper
    public void gone() {
        if (mInflate != null) {
            mInflate.setVisibility(View.GONE);
        }
    }

    @CallSuper
    public void remove() {
        if (mInflate != null && mWindowManager != null) {
            if (mInflate.isAttachedToWindow()) {
                mWindowManager.removeView(mInflate);
            }
            mAdded = false;
        }

        if (mHandler != null) {
            mHandler.removeCallbacksAndMessages(null);
        }
    }

    @CallSuper
    protected View inflate(@LayoutRes int layout) {
        mInflate = View.inflate(mContext, layout, null);
        return mInflate;
    }

    @SuppressWarnings("unchecked")
    protected <T extends View> T findView(@IdRes int id) {
        if (mInflate != null) {
            return (T) mInflate.findViewById(id);
        }
        return null;
    }


    /**
     * 獲取懸浮窗LayoutParam
     *
     * @param mode
     */
    protected void getLayoutParam(int mode) {
        switch (mode) {
            case FULLSCREEN_TOUCHABLE:
                mLayoutParams = FloatWindowParamManager.getFloatLayoutParam(true, true);
                break;

            case FULLSCREEN_NOT_TOUCHABLE:
                mLayoutParams = FloatWindowParamManager.getFloatLayoutParam(true, false);
                break;

            case WRAP_CONTENT_NOT_TOUCHABLE:
                mLayoutParams = FloatWindowParamManager.getFloatLayoutParam(false, false);
                break;

            case WRAP_CONTENT_TOUCHABLE:
                mLayoutParams = FloatWindowParamManager.getFloatLayoutParam(false, true);
                break;
        }

        if (mRequestFocus) {
            mLayoutParams.flags = mLayoutParams.flags & ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        }

        mLayoutParams.gravity = mGravity;
    }

    /**
     * 獲取可見(jiàn)性
     *
     * @return
     */
    public boolean getVisibility() {
        if (mInflate != null && mInflate.getVisibility() == View.VISIBLE) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * 改變可見(jiàn)性
     */
    public void toggleVisibility() {
        if (mInflate != null) {
            if (getVisibility()) {
                if (mInvisibleNeed) {
                    hide();
                } else {
                    gone();
                }
            } else {
                show();
            }
        }
    }
}

2.ABSFloatBase使用

這里建議下載上面提供的Demo結(jié)合會(huì)更方便理解书蚪。那么通過(guò)繼承ABSFloatBase實(shí)現(xiàn)一個(gè)簡(jiǎn)單懸浮窗吧喇澡。以下圖懸浮窗為例:

image.png

具體代碼如下:

public class FloatPermissionDetectView extends AbsFloatBase {

    public FloatPermissionDetectView(Context context) {
        super(context);
    }

    @Override
    public void create() {
        super.create();

        mViewMode = WRAP_CONTENT_TOUCHABLE;
        mGravity = Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL;

        inflate(R.layout.main_layout_float_permission_detect);

        findView(R.id.btn_close).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                remove();
            }
        });
    }
}

在需要顯示的地方調(diào)用:

 mFloatPermissionDetectView = new FloatPermissionDetectView(getApplicationContext());
 mFloatPermissionDetectView.show();

即可。怎么樣殊校,簡(jiǎn)單吧晴玖。先對(duì)這里面的代碼做個(gè)簡(jiǎn)單的解釋?zhuān)?br> 1.首先重載create(),在這里設(shè)置自定義布局为流,對(duì)應(yīng)的代碼就是:inflate(R.layout.main_layout_float_permission_detect);
2.mViewMode = WRAP_CONTENT_TOUCHABLE;用來(lái)設(shè)置當(dāng)前懸浮窗是wrap_content并且攔截觸摸事件的呕屎,與之相對(duì)應(yīng)的還有FULLSCREEN_TOUCHABLEFULLSCREEN_NOT_TOUCHABLE敬察、WRAP_CONTENT_NOT_TOUCHABLE秀睛,看名字就理解這些參數(shù)是什么作用,我就不一一解釋了静汤。
3.mGravity = Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL;當(dāng)懸浮窗是非全屏類(lèi)型的琅催,可以設(shè)置懸浮窗是展示在屏幕的上下左右、還是中間虫给。其他的類(lèi)型大家可以自行探索藤抡。這里面基類(lèi)中默認(rèn)就是居中類(lèi)型的,所以這里相當(dāng)于沒(méi)有設(shè)置抹估。(為什么相當(dāng)于沒(méi)有設(shè)置我還寫(xiě)缠黍?因?yàn)槲蚁胝故疽幌虏僮鳌#?br>

image.png

4.調(diào)用findView(int id)方法找到布局中的控件id药蜻,這部分和Activity中一樣瓷式,就不詳細(xì)解釋了替饿。至于獲取到控件之后嘛,當(dāng)然是嘿嘿嘿咯贸典。比如上面點(diǎn)擊之后調(diào)用remove()方法移除懸浮窗视卢。
5.在需要的地方new一個(gè)對(duì)象,調(diào)用show()方法廊驼,即可展示懸浮窗据过。

其他幾種類(lèi)型可以自行去感受下,在不攔截觸摸事件的類(lèi)型中妒挎,懸浮窗下面的控件是可以接收到觸摸事件的绳锅。

3.和輸入法交互

image.png
image.png

按上面的方法寫(xiě)出來(lái)的懸浮窗是無(wú)法呼出輸入法的,這是因?yàn)閃indowManager的 addView(View view, ViewGroup.LayoutParams params)ViewGroup.LayoutParams params設(shè)置了WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE酝掩,這個(gè)Flag不會(huì)影響被覆蓋應(yīng)用的行為鳞芙,如果需要在懸浮窗中呼出輸入法就需要去除這個(gè)Flag,這時(shí)就需要調(diào)用基類(lèi)的requestFocus(true);方法期虾。和Demo中相對(duì)應(yīng)的就是InputWindow原朝,具體代碼如下:

public class InputWindow extends AbsFloatBase {
    public InputWindow(Context context) {
        super(context);
    }

    @Override
    public void create() {
        super.create();

        mViewMode = WRAP_CONTENT_TOUCHABLE;

        inflate(R.layout.main_layout_input_window);

        requestFocus(true);

        findView(R.id.btn_close).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                remove();
            }
        });
    }
}

調(diào)用requestFocus(true);這個(gè)方法會(huì)發(fā)生什么呢?除了可以呼出鍵盤(pán)外彻消,被覆蓋的應(yīng)用失去焦點(diǎn)竿拆,就不能再處理輸入事件宙拉,比如如果覆蓋在游戲上宾尚,游戲會(huì)暫停背景音樂(lè)。

4.關(guān)于懸浮窗權(quán)限

如果你安裝完應(yīng)用直接點(diǎn)擊后幾個(gè)按鈕想要添加懸浮窗的話(huà)谢澈,會(huì)發(fā)現(xiàn)沒(méi)有添加任何懸浮窗煌贴,而且會(huì)得到Toast報(bào)錯(cuò)。(Vivo手機(jī)除外哈锥忿,具體原因稍候解釋?zhuān)?br>

image.png

這是因?yàn)樘砑討腋〈靶枰獞腋〈皺?quán)限牛郑,準(zhǔn)確的講就是允許顯示在其他應(yīng)用的上層的權(quán)限。

image.png

如果沒(méi)有獲取該權(quán)限敬鬓,點(diǎn)擊檢查懸浮窗權(quán)限就會(huì)彈出跳轉(zhuǎn)該頁(yè)面的懸浮窗淹朋,確定后就會(huì)跳轉(zhuǎn)到該頁(yè)面。當(dāng)然在國(guó)內(nèi)魔改ROM的悲慘狀況下钉答,這個(gè)頁(yè)面被以各種形式展現(xiàn)到各個(gè)地方:

小米MIUI.png
Vivo.png

還有各種各樣的樣式我就不一一例舉了础芍。至于跳轉(zhuǎn)到這個(gè)頁(yè)面的方法也是各不相同,好在Demo里面已經(jīng)處理了大部分機(jī)型了数尿,根據(jù)目前公司的反饋仑性,已經(jīng)可以處理相當(dāng)多的機(jī)型了∮冶模考慮到百萬(wàn)級(jí)的安裝量诊杆,和幾十款測(cè)試機(jī)型歼捐,勉強(qiáng)湊合用吧,實(shí)在不行就只能自行手動(dòng)打開(kāi)了晨汹。對(duì)不起豹储,在國(guó)產(chǎn)ROM面前,我淘这,敗了颂翼。


image.png

那么怎么檢查應(yīng)用是否有允許顯示在其他應(yīng)用的上層的權(quán)限呢?Demo中提供的FloatWindowParamManager.checkPermission(getApplicationContext());方法就可以了慨灭。當(dāng)然如果事情是這樣朦乏,那么世界該是多么美好!然鵝氧骤,在Vivo面前呻疹,我,又?jǐn)×耍?br>

image.png

對(duì)于大部分機(jī)型而言筹陵,這個(gè)方法算得上是一個(gè)不錯(cuò)的解決方案刽锤,但是遇到Vivo這樣的奇(沙)葩(雕),會(huì)遇到一些難言的困惑朦佩。比如在Vivo手機(jī)上并思,當(dāng)你點(diǎn)擊Button檢查權(quán)限時(shí),FloatWindowParamManager.checkPermission(getApplicationContext());一直返回true语稠,但是當(dāng)應(yīng)用處于后臺(tái)時(shí)宋彼,試圖添加懸浮窗又會(huì)失敗仙畦!已經(jīng)添加的懸浮窗會(huì)消失输涕!經(jīng)過(guò)一番令人驚奇的操作后發(fā)現(xiàn),原來(lái)Vivo在應(yīng)用前臺(tái)可見(jiàn)時(shí)會(huì)默認(rèn)給予應(yīng)用這個(gè)權(quán)限(實(shí)際這個(gè)懸浮窗權(quán)限開(kāi)關(guān)沒(méi)有打開(kāi)也行)慨畸,但是當(dāng)應(yīng)用處于后臺(tái)時(shí)只有這個(gè)懸浮穿權(quán)限開(kāi)關(guān)真的打開(kāi)時(shí)才能添加顯示懸浮窗莱坎!所以,只有真的打開(kāi)了懸浮窗權(quán)限開(kāi)關(guān)才算是達(dá)到了我們的目的4缡俊i苁病! 這么說(shuō)有點(diǎn)繞脖子弱卡,但是我相信你是懂我的乃正!

image.png

所以,對(duì)于Vivo的操作就顯得非常特別谐宙,重點(diǎn)在于確保App處于后臺(tái)時(shí)確實(shí)獲取到了懸浮窗權(quán)限烫葬!具體到Demo里面的代碼體現(xiàn)在:

    private Handler mHandler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(Message msg) {
            int what = msg.what;
            switch (what) {
                case HANDLER_DETECT_PERMISSION:
                    if (FloatWindowParamManager.checkPermission(getApplicationContext())) {
                        //對(duì)沙雕VIVO機(jī)型特殊處理,應(yīng)用處于后臺(tái)檢查懸浮窗權(quán)限成功才能確認(rèn)真的獲取了懸浮窗權(quán)限
                        if (RomUtils.isVivoRom() && AppUtils.isAppForeground()) {
                            Log.e(TAG, "懸浮窗權(quán)限檢查成功,但App處于前臺(tái)狀態(tài),特殊機(jī)型會(huì)允許App獲取權(quán)限搭综,特殊機(jī)型就是指Vivo這個(gè)沙雕");
                            mHandler.sendEmptyMessageDelayed(HANDLER_DETECT_PERMISSION, 500);
                            return;
                        }

                        mHandler.removeMessages(HANDLER_DETECT_PERMISSION);
                        Log.e(TAG, "懸浮窗權(quán)限檢查成功");
                        showFloatPermissionWindow();
                    } else {
                        Log.e(TAG, "懸浮窗權(quán)限檢查失敗");
                        mHandler.sendEmptyMessageDelayed(HANDLER_DETECT_PERMISSION, 500);
                    }
                    break;
            }
        }
    };

至于為什么要用Handler不停地循環(huán)檢查垢箕,這是為了在打開(kāi)權(quán)限后可以立刻顯示懸浮窗,具體效果看Demo就知道了兑巾。不過(guò)部分機(jī)型在打開(kāi)懸浮窗開(kāi)關(guān)的時(shí)候并不能立刻顯示懸浮窗条获,只有刷新頁(yè)面,比如點(diǎn)擊返回的時(shí)候才能顯示蒋歌。哎帅掘,我再次認(rèn)輸遼。如果哪位大佬有解決方案堂油,可以教一下我修档。

2019年2月20號(hào)新增9.0下懸浮窗不能進(jìn)入劉海區(qū)域的解決辦法:

//劉海屏延伸到劉海里面
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                layoutParams.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
            }

如果不需要懸浮窗延伸到劉海里面去除這幾行代碼即可。
PS:華為Mate20劉海區(qū)域觸控?zé)o效府框,所以需要在該區(qū)域有觸控控件的需要避開(kāi)劉海區(qū)域吱窝。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市迫靖,隨后出現(xiàn)的幾起案子院峡,更是在濱河造成了極大的恐慌,老刑警劉巖系宜,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件照激,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡盹牧,警方通過(guò)查閱死者的電腦和手機(jī)俩垃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)欢策,“玉大人吆寨,你說(shuō)我怎么就攤上這事赏淌〔瓤埽” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵六水,是天一觀的道長(zhǎng)俺孙。 經(jīng)常有香客問(wèn)我,道長(zhǎng)掷贾,這世上最難降的妖魔是什么睛榄? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮想帅,結(jié)果婚禮上场靴,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好旨剥,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布咧欣。 她就那樣靜靜地躺著,像睡著了一般轨帜。 火紅的嫁衣襯著肌膚如雪魄咕。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,370評(píng)論 1 302
  • 那天蚌父,我揣著相機(jī)與錄音哮兰,去河邊找鬼。 笑死苟弛,一個(gè)胖子當(dāng)著我的面吹牛喝滞,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播膏秫,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼囤躁,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了荔睹?” 一聲冷哼從身側(cè)響起狸演,我...
    開(kāi)封第一講書(shū)人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎僻他,沒(méi)想到半個(gè)月后宵距,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡吨拗,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年满哪,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片劝篷。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡哨鸭,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出娇妓,到底是詐尸還是另有隱情像鸡,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布哈恰,位于F島的核電站只估,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏着绷。R本人自食惡果不足惜蛔钙,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望荠医。 院中可真熱鬧吁脱,春花似錦桑涎、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至紧显,卻和暖如春讲衫,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背孵班。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工涉兽, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人篙程。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓枷畏,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親虱饿。 傳聞我的和親對(duì)象是個(gè)殘疾皇子拥诡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354

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