Android WindowManager

Android WindowManager


本文由 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 执行窗体相关代码
    }

开启浮窗的权限后, 程序打开浮窗后, 通知栏也会有通知信息.
Android WindowManager_第1张图片

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;
    }

WindowManager 的 LayoutParams 相关属性

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);
}

你可能感兴趣的:(android)