Android使用WindowManger實現桌面懸浮窗

如果想實現一個在桌面顯示的懸浮窗,用Dialog、PopupWindow、Toast等已經不能實現了,他們基本都是在Activity之上顯示的,如果想實現在桌面顯示的懸浮窗效果,需要用到WindowManager來實現了。

先上效果圖:

1535282985081mzhh.gif

使用WindowManager實現

  • 添加一個懸浮窗:
        sys_view = new SmallWindowView(mContext);
        sys_view.setText("50%");
        sys_view.setOnTouchListener(this);
        windowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
        int screenWidth = 0, screenHeight = 0;
        if (windowManager != null) {
            //獲取屏幕的寬和高
            Point point = new Point();
            windowManager.getDefaultDisplay().getSize(point);
            screenWidth = point.x;
            screenHeight = point.y;
            layoutParams = new WindowManager.LayoutParams();
//            layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
//            layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
            layoutParams.width = 200;
            layoutParams.height = 200;
            //設置type
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                //26及以上必須使用TYPE_APPLICATION_OVERLAY   @deprecated TYPE_PHONE
                layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
            } else {
                layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
            }
            //設置flags
            layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
            layoutParams.gravity = Gravity.START | Gravity.TOP;
            //背景設置成透明
            layoutParams.format = PixelFormat.TRANSPARENT;
            layoutParams.x = screenWidth;
            layoutParams.y = screenHeight / 2;
            //將View添加到屏幕上
            windowManager.addView(sys_view, layoutParams);
        }
  • 更新懸浮窗位置:
windowManager.updateViewLayout(sys_view, layoutParams);
  • 關閉懸浮窗:
windowManager.removeView(sys_view);

通過上面的代碼就可以實現一個桌面懸浮窗功能了。

注意:在6.0以上,需要在Manifest.xml中聲明
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />權限並且在開啓懸浮窗時動態判斷權限,如果沒有此權限需要跳到設置頁面去設置,看下官方文檔的說明:

SYSTEM_ALERT_WINDOW

分析

1、添加懸浮窗:
通過Context.getSystemService(Context.WINDOW_SERVICE)獲得一個WindowManager(以下簡稱VM), VM是外界訪問Window的入口,Activity、Dialog、Toast等其視圖都是依附在Window之上的,Window是View的直接管理者,VM繼承自ViewManager,其添加、刷新、刪除方法也是來自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);
}

VM有一個靜態內部類WindowManager.LayoutParams ,Window的各個屬性在這個內部類中設置:

  • LayoutParams.TYPE
    如果TargetSdkVersion<26 ,那麼可以直接使用LayoutParams.TYPE_PHONE或者LayoutParams.TYPE_SYSTEM_ALERT,在TargetSdkVersion>=26時,TYPE_PHONETYPE_SYSTEM_ALERT都已經廢棄了,需要使用TYPE_APPLICATION_OVERLAY來標識TYPE。

  • LayoutParams.FLAGS
    FLAGS表示Window的屬性,通過FLAGS可以控制Window的顯示特性,常用的幾個特性:
    LayoutParams.FLAG_NOT_TOUCH_MODAL : 使用了此標識,可以將點擊事件傳遞到懸浮窗以外的區域,反之其他區域的Window將接收不到事件。
    LayoutParams.FLAG_NOT_FOCUSABLE : 表示懸浮窗Window不需要獲取焦點,也不需要獲取各種輸入事件,事件會直接傳遞給下層的具有焦點的Window
    LayoutParams.FLAG_SHOW_WHEN_LOCKED : 此模式可以讓Window顯示在鎖屏的界面上

  • LayoutParams.FORMAT
    懸浮窗Window的背景格式,一般設置成PixelFormat.TRANSPARENT透明即可

  • LayoutParams.X & LayoutParams.Y
    懸浮窗Window在屏幕上的座標值,可以根據X&Y的值來刷新Window在屏幕上的位置

  • LayoutParams.Width & LayoutParams.Height
    懸浮窗Window的寬度和高度

2、更新懸浮窗位置:
在View的OnTouchEvent中或OnTouch中更新layoutParams.xlayoutParams.y的值並通過windowManager.updateViewLayout()重新設置懸浮窗Window在屏幕中的位置,如下:

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        int mInScreenX = (int) event.getRawX();
        int mInScreenY = (int) event.getRawY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastX = (int) event.getRawX();
                mLastY = (int) event.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                layoutParams.x += mInScreenX - mLastX;
                layoutParams.y += mInScreenY - mLastY;
                mLastX = mInScreenX;
                mLastY = mInScreenY;
                windowManager.updateViewLayout(sys_view, layoutParams);
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return true;
    }

3、刪除懸浮窗:
刪除比較簡單,直接調用windowManager.removeView(view)把view從Window中刪除即可。

問題

在6.0以上使用時,需要動態申請該懸浮窗權限,如下:

//判斷有沒有懸浮窗權限,沒有去申請
if(!Settings.canDrawOverlays(context)){
     Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                Uri.parse("package:" + context.getPackageName()));
     context.startActivityForResult(intent, REQUEST_CODE);
}

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        switch (requestCode) {
            case REQUEST_CODE:
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return;
                if (!WindowUtil.canOverDraw(this)) {
                    toast("懸浮窗權限未開啓,請在設置中手動打開");
                    return;
                }
                WindowController.getInstance().showThumbWindow();
                break;
        }
    }

通過Settings.canDrawOverlays(context)判斷是否有懸浮窗權限,如果沒有,跳轉到設置頁面去設置,並在onActivityResult ()中得到申請結果,看似很完美,但在實際測試中,發現在8.0以上的手機上有問題,即使在設置中同意了權限,8.0的手機Settings.canDrawOverlays(context)總是返回false,不過在關閉頁面重新調用此方法時,又返回的true,感覺是有一定的延遲,google了一下,發現別人同樣遇到了這個問題,貌似已經給google提交了bug單,可以看此博客:
http://paskov.vmsoft-bg.com/settings-candrawoverlays-allays-returns-false-on-android-o/ ,不過博客中的解決方法用我的8.0手機(HUAWEI MATE10)依然不起作用,暫時還沒深入研究,有解決此問題的還希望不吝賜教。

以上例子的源碼地址:https://github.com/crazyqiang

引用:
【1】https://developer.android.com/reference/android/Manifest.permission#SYSTEM_ALERT_WINDOW
【2】 《Android開發藝術探索》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章