本文由 Luzhuo 编写,转发请保留该信息.
原文: https://blog.csdn.net/Rozol/article/details/86658357
WindowManager 是窗体对象, 可以实现本应用 View 飘浮于其他应用之上的效果.
获取窗体对象
WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
获取窗体对象的大小
Display display = wm.getDefaultDisplay();
添加/删除/更新 View
wm.addView(view, params); // 向窗体添加View
wm.removeView(view); // 向窗体移除View
wm.updateViewLayout(view, params); // 让窗体更新View位置
需要在布局里添加弹出浮窗的权限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
Android6.0+ 需要用户打开浮窗的权限
// Android6.0 需要窗口权限, 此段代码为打开允许窗口权限页, 不加需要用户自己手动打开
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!Settings.canDrawOverlays(this)) {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName()));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
} else {
// TODO 执行窗体相关代码
}
} else {
// TODO 执行窗体相关代码
}
开启浮窗的权限后, 程序打开浮窗后, 通知栏也会有通知信息.
Android8.0 需要处理的权限类型
// Android8.0 permission denied for window type 2002
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
}
Flags 参数:
FLAG_ALLOW_LOCK_WHILE_SCREEN_ON // 允许在屏幕开启的时候锁定屏幕
FLAG_DIM_BEHIND // 所有在这个window之后的会变暗(使用dimAmount属性来控制变暗的程度)
**FLAG_NOT_FOCUSABLE** // 不接受任何输入
**FLAG_NOT_TOUCHABLE** // 不接受触摸事件
**FLAG_NOT_TOUCH_MODAL** // Window区域外的事件传递给下层的Window,区域内的事件自己处理
FLAG_TOUCHABLE_WHEN_WAKING
FLAG_KEEP_SCREEN_ON // 当window是可见的, 则保持设备屏幕不关闭也不变暗
**FLAG_LAYOUT_IN_SCREEN** // 无视其他的装饰(如状态栏)
**FLAG_LAYOUT_NO_LIMITS** // 允许window扩展值屏幕之外
FLAG_FULLSCREEN // 隐藏所有的装饰物(如状态栏)
FLAG_FORCE_NOT_FULLSCREEN // 强制显示屏幕上的一些装饰(如状态栏)
FLAG_SECURE // 防止被截屏,或显示在其他屏幕上
FLAG_SCALED // 合成屏幕时, 进行缩放
**FLAG_IGNORE_CHEEK_PRESSES** // 当用户把脸贴在屏幕上,它会过滤不需要的点击事件
**FLAG_LAYOUT_INSET_DECOR** // 不会被装饰物(如状态栏)掩盖(只能配合 FLAG_LAYOUT_IN_SCREEN 一起使用 / setFlags(int, int))
FLAG_ALT_FOCUSABLE_IM
FLAG_WATCH_OUTSIDE_TOUCH // 点击事件如果发生在window之外的范围,会接收到一个MotionEvent.ACTION_OUTSIDE (其他手势都不会接收)
**FLAG_SHOW_WHEN_LOCKED** // 可以在锁屏界面上显示
FLAG_SHOW_WALLPAPER
FLAG_TURN_SCREEN_ON // 当window被添加或显示, 屏幕会点亮
FLAG_DISMISS_KEYGUARD // 自动解锁(无密码)锁屏界面
**FLAG_SPLIT_TOUCH** // 多点触控
FLAG_HARDWARE_ACCELERATED // 启动硬件加速
FLAG_LOCAL_FOCUS_MODE
**FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS** // 该window负责绘制状态栏的背景
剩下的也就 type, 和一些常用的设置, 没什么好讲的.
type这东西, 在 api26+ 只要不是系统级别的, 都是使用 TYPE_APPLICATION_OVERLAY
这个类型, 官方文档把不是系统级别的类型都标注了这个:
但是, 在低版本中 TYPE_APPLICATION_OVERLAY
会不起作用, 所以使用案例中的代码方式处理即可.
Android6.0开启手动开启权限那个, 我写Activity里了, 这里就不粘贴了, 直接贴出Service的代码吧
package me.luzhuo.windowmanagerdemo;
import android.app.Service;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.os.Build;
import android.os.IBinder;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.ImageView;
public class WindowManagerService extends Service implements View.OnTouchListener {
private final WindowManager.LayoutParams params = new WindowManager.LayoutParams();
private WindowManager wm;
private View view;
private int screenHeight;
private int screenWidth;
@Override
public void onCreate() {
showView();
super.onCreate();
}
private void showView() {
// 获取窗体对象
wm = (WindowManager) getSystemService(WINDOW_SERVICE);
screenHeight = wm.getDefaultDisplay().getHeight();
screenWidth = wm.getDefaultDisplay().getWidth();
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
params.format = PixelFormat.TRANSLUCENT;
// Android8.0 permission denied for window type 2002
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
}
params.gravity = Gravity.TOP + Gravity.LEFT;
view = View.inflate(this, R.layout.window_view, null);
ImageView imageView = view.findViewById(R.id.iv);
imageView.setOnTouchListener(this);
// 向窗体添加view, 需要添加窗体权限(android.permission.SYSTEM_ALERT_WINDOW)
wm.addView(view, params);
}
private int startX;
private int startY;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
startX = (int) event.getRawX();
startY = (int) event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
int moveX = (int) event.getRawX();
int moveY = (int) event.getRawY();
int disX = moveX - startX;
int disY = moveY - startY;
params.x = params.x + disX;
params.y = params.y + disY;
// 容错处理
if (params.x < 0) {
params.x = 0;
}
if (params.y < 0) {
params.y = 0;
}
if (params.x > screenWidth - view.getWidth()) {
params.x = screenWidth - view.getWidth();
}
if (params.y > screenHeight - view.getHeight()) {
params.y = screenHeight - view.getHeight();
}
// 告知窗体View位置更新
wm.updateViewLayout(view, params);
startX = (int) event.getRawX();
startY = (int) event.getRawY();
break;
case MotionEvent.ACTION_UP:
break;
}
return true; // 不响应点击返回true
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onDestroy() {
// 向窗体移除view
if (wm != null && view != null) {
wm.removeView(view);
}
super.onDestroy();
}
}
效果图:
大家可能会经常用到 Toast 或者 Dialog, 这些控件都能显示在桌面上, 这是因为这些控件用的也是 WindowManager.
这是 Toast 的源码:
private static class TN extends ITransientNotification.Stub {
private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
private static final int SHOW = 0;
private static final int HIDE = 1;
private static final int CANCEL = 2;
final Handler mHandler;
int mGravity;
int mX, mY;
float mHorizontalMargin;
float mVerticalMargin;
View mView;
View mNextView;
int mDuration;
WindowManager mWM;
String mPackageName;
static final long SHORT_DURATION_TIMEOUT = 4000;
static final long LONG_DURATION_TIMEOUT = 7000;
TN(String packageName, @Nullable Looper looper) {
// XXX This should be changed to use a Dialog, with a Theme.Toast
// defined that sets up the layout params appropriately.
final WindowManager.LayoutParams params = mParams;
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.format = PixelFormat.TRANSLUCENT;
params.windowAnimations = com.android.internal.R.style.Animation_Toast;
params.type = WindowManager.LayoutParams.TYPE_TOAST;
params.setTitle("Toast");
params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
mPackageName = packageName;
}
public void handleShow(IBinder windowToken) {
if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
return;
}
if (mView != mNextView) {
// remove the old view if necessary
handleHide();
mView = mNextView;
Context context = mView.getContext().getApplicationContext();
String packageName = mView.getContext().getOpPackageName();
if (context == null) {
context = mView.getContext();
}
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
final Configuration config = mView.getContext().getResources().getConfiguration();
final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
mParams.gravity = gravity;
if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
mParams.horizontalWeight = 1.0f;
}
if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
mParams.verticalWeight = 1.0f;
}
mParams.x = mX;
mParams.y = mY;
mParams.verticalMargin = mVerticalMargin;
mParams.horizontalMargin = mHorizontalMargin;
mParams.packageName = packageName;
mParams.hideTimeoutMilliseconds = mDuration ==
Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
mParams.token = windowToken;
if (mView.getParent() != null) {
mWM.removeView(mView);
}
// ...
try {
mWM.addView(mView, mParams);
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
}
}
public void handleHide() {
if (mView != null) {
if (mView.getParent() != null) {
mWM.removeViewImmediate(mView);
}
mView = null;
}
}
}
这是Dialog的源码:
private final WindowManager mWindowManager;
Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
final Window w = new PhoneWindow(mContext);
mWindow = w;
w.setCallback(this);
w.setOnWindowDismissedCallback(this);
w.setOnWindowSwipeDismissedCallback(() -> {
if (mCancelable) {
cancel();
}
});
w.setWindowManager(mWindowManager, null, null);
w.setGravity(Gravity.CENTER);
mListenersHandler = new ListenersHandler(this);
}
public void show() {
if (mShowing) {
if (mDecor != null) {
if (mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
mWindow.invalidatePanelMenu(Window.FEATURE_ACTION_BAR);
}
mDecor.setVisibility(View.VISIBLE);
}
return;
}
mCanceled = false;
if (!mCreated) {
dispatchOnCreate(null);
} else {
final Configuration config = mContext.getResources().getConfiguration();
mWindow.getDecorView().dispatchConfigurationChanged(config);
}
mDecor = mWindow.getDecorView();
WindowManager.LayoutParams l = mWindow.getAttributes();
mWindowManager.addView(mDecor, l);
}