不需要權(quán)限的懸浮窗

需求開發(fā)中有時(shí)會(huì)遇到需要在別的應(yīng)用上顯示自己的內(nèi)容的情況慰枕,比如展示一個(gè)消息通知按鈕呛踊,允許用戶通過這個(gè)按鈕直接進(jìn)入我們的應(yīng)用砾淌。然而展示懸浮窗需要申請 SYSTEM_ALERT_WINDOW 權(quán)限。

產(chǎn)品角度出發(fā)肯定是不向用戶展示這個(gè)權(quán)限更好谭网,那么以下是具體的實(shí)現(xiàn)方法汪厨。首先介紹的是普遍方法,然后針對 MIUI8 這個(gè)特殊的系統(tǒng)做單獨(dú)的處理愉择。

需要補(bǔ)充的是對于4.4以下的系統(tǒng)劫乱,不添加權(quán)限的情況下最多只能展示而不能響應(yīng)點(diǎn)擊。所以這部分需要區(qū)分處理锥涕。

Github鏈接:https://github.com/ZhengPhoenix/CustomFloatWindow

動(dòng)態(tài)添加懸浮窗

這里介紹在service中添加懸浮窗到WindowManager的過程

  1. 普通懸浮窗添加代碼流程如下

/**
 * 創(chuàng)建懸浮窗
 */
private void createFloatView() {
    LayoutInflater layoutInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    mFloatView = layoutInflater.inflate(R.layout.floating_entrance, null);

    wm = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
    sParams = new WindowManager.LayoutParams();

    // 設(shè)置window type
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        sParams.type = WindowManager.LayoutParams.TYPE_TOAST;
    } else {
        sParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
    }


    /*
     * 如果設(shè)置為params.type = WindowManager.LayoutParams.TYPE_PHONE; 那么優(yōu)先級(jí)會(huì)降低一些,
     * 即拉下通知欄不可見
     */
    sParams.format = PixelFormat.RGBA_8888; // 設(shè)置圖片格式衷戈,效果為背景透明

    // 設(shè)置Window flag
    sParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
            | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;

    // 設(shè)置懸浮窗的長得寬
    sParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
    sParams.height = WindowManager.LayoutParams.WRAP_CONTENT;

    //計(jì)算高度
    DisplayMetrics metric = new DisplayMetrics();
    wm.getDefaultDisplay().getMetrics(metric);
    int yPosition = Math.round(wm.getDefaultDisplay().getHeight() / 3);

    sParams.gravity = Gravity.RIGHT | Gravity.TOP;
    sParams.x = 0;
    sParams.y = yPosition;

    AnimationDrawable drawable = (AnimationDrawable) ((ImageView) mFloatView.findViewById(R.id.floating_entrance_anim)).getDrawable();
    drawable.start();

    wm.addView(mFloatView, sParams);
    mFloatView.postDelayed(new Runnable() {
        @Override
        public void run() {
            try {
                wm.removeView(mFloatView);
            } catch (Exception e) {
                //incase floating window has already dismissed
            }
            FloatingWindowService.this.stopSelf();
        }
    }, DURATION * 1000);
    isAdded = true;
}

這個(gè)流程先inflate了一個(gè)自定義的view R.layout.floating_entrance ,然后獲取WindowManagerService层坠,再添加該view到WM殖妇。這個(gè)view在1000ms后會(huì)自動(dòng)移除自己,同時(shí)它可以響應(yīng)用戶的點(diǎn)擊事件窿春。

為了對4.4以下的系統(tǒng)做兼容拉一,添加了這部分代碼采盒,同時(shí)也在AndroidManifest里申請了相應(yīng)的權(quán)限

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

// 設(shè)置window type
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    sParams.type = WindowManager.LayoutParams.TYPE_TOAST;
} else {
    sParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
}

這里的問題是,4.4以下的系統(tǒng)如果設(shè)置為toast類型蔚润,則不能響應(yīng)點(diǎn)擊事件磅氨。必須設(shè)置為 TYPE_SYSTEM_ALERT 才可以。

接下來需要設(shè)置懸浮窗的屬性嫡纠,讓它能夠響應(yīng)事件

//設(shè)置Window flag

sParams.flags= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL

| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;```

完成到這里之后烦租,就可以根據(jù)自己需要設(shè)置窗口位置,動(dòng)畫等效果了除盏。

最后添加view到wm就可以

wm.addView(mFloatView,sParams);




2.MIUI8懸浮窗添加流程

上面這種方式在MIUI8中是無效的叉橱。原因在于MIUI8對懸浮窗的權(quán)限做了限制,除非打開懸浮窗權(quán)限SYSTEM_ALERT_WINDOW者蠕,否則無法通過這種方式展示懸浮窗窃祝。

然而經(jīng)過測試發(fā)現(xiàn)普通的Toast在沒有權(quán)限的情況下是可以顯示的,當(dāng)然這毫不意外踱侣。那么這就有空子可鉆了粪小,我們可以自定義一個(gè)view給Toast,然后再將這個(gè)toast顯示出來。這里需要解決幾個(gè)問題抡句,首先是Toast的觸摸響應(yīng)探膊,其次是它的某些方法是private的,包括需要修改的LayoutParameters對象也是private待榔,這部分需要用反射來獲得逞壁。

/**

  • Created by zhenghui on 2016/8/24.

  • This is a class created to make sure custom floating view

  • could be shown in MIUI8
    */
    public class ExToast {
    private static final String TAG = "ExToast";

    public static final int LENGTH_ALWAYS = 0;
    public static final int LENGTH_SHORT = 2;
    public static final int LENGTH_LONG = 4;

    private Toast toast;
    private Context mContext;
    private int mDuration = LENGTH_SHORT;
    private int animations = -1;
    private boolean isShow = false;

    private Object mTN;
    private Method show;
    private Method hide;
    private WindowManager mWM;
    private WindowManager.LayoutParams params;
    private View mView;

    private Handler handler = new Handler();

    public ExToast(Context context){
    this.mContext = context;
    if (toast == null) {
    toast = new Toast(mContext);
    }
    LayoutInflater inflate = (LayoutInflater)
    mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    mView = inflate.inflate(R.layout.floating_entrance, null);
    }

    private Runnable hideRunnable = new Runnable() {
    @Override
    public void run() {
    hide();
    }
    };

    /**

    • Show the view for the specified duration.
      */
      public void show(){
      if (isShow) return;
      toast.setView(mView);
      initTN();
      try {
      show.invoke(mTN);
      } catch (InvocationTargetException e) {
      e.printStackTrace();
      } catch (IllegalAccessException e) {
      e.printStackTrace();
      }
      isShow = true;

      if (mDuration > LENGTH_ALWAYS) {
      handler.postDelayed(hideRunnable, mDuration * 1000);
      }
      }

    /**

    • Close the view if it's showing, or don't show it if it isn't showing yet.

    • You do not normally have to call this. Normally view will disappear on its own

    • after the appropriate duration.
      */
      public void hide(){
      if(!isShow) return;
      try {
      hide.invoke(mTN);
      } catch (InvocationTargetException e) {
      e.printStackTrace();
      } catch (IllegalAccessException e) {
      e.printStackTrace();
      }

      isShow = false;
      }

    public void setView(View view) {
    toast.setView(view);
    }

......

public static ExToast makeText(Context context, CharSequence text, int duration) {
    Toast toast = Toast.makeText(context,text,Toast.LENGTH_SHORT);
    ExToast exToast = new ExToast(context);
    exToast.toast = toast;
    exToast.mDuration = duration;

    return exToast;
}

public static ExToast makeText(Context context, int resId, int duration)
        throws Resources.NotFoundException {
    return makeText(context, context.getResources().getText(resId), duration);
}

public void setText(int resId) {
    setText(mContext.getText(resId));
}

public void setText(CharSequence s) {
    toast.setText(s);
}

public int getAnimations() {
    return animations;
}

public void setAnimations(int animations) {
    this.animations = animations;
}

private void initTN() {
    try {
        Field tnField = toast.getClass().getDeclaredField("mTN");
        tnField.setAccessible(true);
        mTN = tnField.get(toast);
        show = mTN.getClass().getMethod("show");
        hide = mTN.getClass().getMethod("hide");

        Field tnParamsField = mTN.getClass().getDeclaredField("mParams");
        tnParamsField.setAccessible(true);
        params = (WindowManager.LayoutParams) tnParamsField.get(mTN);
        params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;

        /**設(shè)置動(dòng)畫*/
        if (animations != -1) {
            params.windowAnimations = animations;
        }

        /**調(diào)用tn.show()之前一定要先設(shè)置mNextView*/
        Field tnNextViewField = mTN.getClass().getDeclaredField("mNextView");
        tnNextViewField.setAccessible(true);
        tnNextViewField.set(mTN, toast.getView());

        mWM = (WindowManager)mContext.getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

public void setListener(View.OnClickListener listener) {
    mView.setOnClickListener(listener);
}

}


這個(gè)類封裝了所有需要的方法和對象,使用時(shí)只需要簡單的實(shí)例化和調(diào)用show和dismiss方法就可以锐锣。

需要關(guān)注的是 initTN() 方法腌闯,這里面除了獲取了show和hide,重點(diǎn)還獲取了 params 對象刺下,并向它設(shè)置了可觸摸的屬性绑嘹。

```params.flags= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL

| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;```

對的就是這么扯竟然是 WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE ...

下面是調(diào)用這個(gè)封裝好的 ExToast 的 方法,

/**

  • 創(chuàng)建 MIUI8 懸浮窗
    */
    private void createFloatViewForMiUi() {
    wm = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
    //計(jì)算高度
    DisplayMetrics metric = new DisplayMetrics();
    wm.getDefaultDisplay().getMetrics(metric);
    int yPosition = Math.round(wm.getDefaultDisplay().getHeight() / 3);
    mFloatViewMIUI = new ExToast(getApplicationContext());
    mFloatViewMIUI.setDuration(DURATION);
    mFloatViewMIUI.setAnimations(R.style.float_search);
    mFloatViewMIUI.setGravity(Gravity.RIGHT | Gravity.TOP, 0, yPosition);
    mFloatViewMIUI.show();
    isAdded = true;
    }



到這里就可以把懸浮窗顯示出來了橘茉。最后想要隱藏或者去除的話針對一般的和MIUI8的情況做對應(yīng)處理就行工腋,

比如調(diào)用  wm.removeView 和 mFloatViewMIUI.hide。

關(guān)于不需要權(quán)限展示懸浮窗的思路和代碼就是這樣了畅卓,但是MIUI一直在更新中擅腰,不排除有新的版本堵上了這個(gè)漏洞,所以如果有使用不了的情況也是很正常的╮(╯▽╰)╭
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末翁潘,一起剝皮案震驚了整個(gè)濱河市趁冈,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖渗勘,帶你破解...
    沈念sama閱讀 218,036評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件沐绒,死亡現(xiàn)場離奇詭異,居然都是意外死亡旺坠,警方通過查閱死者的電腦和手機(jī)乔遮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來取刃,“玉大人蹋肮,你說我怎么就攤上這事¤盗疲” “怎么了坯辩?”我有些...
    開封第一講書人閱讀 164,411評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長崩侠。 經(jīng)常有香客問我漆魔,道長,這世上最難降的妖魔是什么却音? 我笑而不...
    開封第一講書人閱讀 58,622評(píng)論 1 293
  • 正文 為了忘掉前任有送,我火速辦了婚禮,結(jié)果婚禮上僧家,老公的妹妹穿的比我還像新娘。我一直安慰自己裸删,他們只是感情好八拱,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,661評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著涯塔,像睡著了一般肌稻。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上匕荸,一...
    開封第一講書人閱讀 51,521評(píng)論 1 304
  • 那天爹谭,我揣著相機(jī)與錄音,去河邊找鬼榛搔。 笑死诺凡,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的践惑。 我是一名探鬼主播腹泌,決...
    沈念sama閱讀 40,288評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼尔觉!你這毒婦竟也來了凉袱?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,200評(píng)論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎专甩,沒想到半個(gè)月后钟鸵,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,644評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡涤躲,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,837評(píng)論 3 336
  • 正文 我和宋清朗相戀三年棺耍,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片篓叶。...
    茶點(diǎn)故事閱讀 39,953評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡烈掠,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出缸托,到底是詐尸還是另有隱情左敌,我是刑警寧澤,帶...
    沈念sama閱讀 35,673評(píng)論 5 346
  • 正文 年R本政府宣布俐镐,位于F島的核電站矫限,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏佩抹。R本人自食惡果不足惜叼风,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,281評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望棍苹。 院中可真熱鬧无宿,春花似錦、人聲如沸枢里。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽栏豺。三九已至彬碱,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間奥洼,已是汗流浹背巷疼。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留灵奖,地道東北人嚼沿。 一個(gè)月前我還...
    沈念sama閱讀 48,119評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像瓷患,于是被迫代替她去往敵國和親伏尼。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,901評(píng)論 2 355

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