如果想實現一個在桌面顯示的懸浮窗,用Dialog、PopupWindow、Toast等已經不能實現了,他們基本都是在Activity之上顯示的,如果想實現在桌面顯示的懸浮窗效果,需要用到WindowManager來實現了。
先上效果圖:
使用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" />
權限並且在開啓懸浮窗時動態判斷權限,如果沒有此權限需要跳到設置頁面去設置,看下官方文檔的說明:
分析
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_PHONE
和TYPE_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.x
及layoutParams.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開發藝術探索》