Window 是什么

  • Window 是一个相对概念上的载体,他并不是一个真正的容器,仅仅负责管理依赖 window 上面的 View 的一个类,充当管理者。
  • Window 是一个抽象类,在源码实现中,只有一个具体的实现类,phoneWindow
  • 创建 Window 需要通过 WindowManager 创建
  • WindowManager 是外界访问 Window 的入口
  • Window 具体实现位于 WindowManagerService 中
  • WindowManager 和 WindowManagerService 的交互是通过 IPC 完成

Activity 和 Window 之间的关系

  • Activity 只负责生命周期和事件处理
  • Window 只控制视图
  • 一个 Activity 包含一个 Window ,如果 Activity 没有 Window ,那就相当于 Service
  • AMS 统一调度所有应用程序的 Activity
  • WMS 控制所有 Window 的显示与隐藏以及要显示的位置

WindowManager

用来在应用与window之间的管理接口,管理窗口顺序,消息等。

WindowManagerService

简称 Wms,WindowManagerService 管理窗口的创建、更新和删除,显示顺序等,是 WindowManager 真正的实现类。它运行在 System_server 进程,作为服务端,客户端(应用程序)通过IPC调用和它进行交互。

Token

这里提到的Token主是指窗口令牌(Window Token),是一种特殊的Binder令牌,Wms用它唯一标识系统中的一个窗口。

Window 中的 Type

Window 类型 含义 Window 层级
应用 Window 对应着一个 Activity 1-99
子 Window 不能单独存在,需要附属在父 Window 中(比如 Dialog 就是子 Window) 1000-1999
系统 Window 需要声明权限才能创建 Window ,比如 Toast 、状态栏、悬浮窗 2000-2999
  • 应用窗口(1~99)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    //第一个应用窗口
    public static final int FIRST_APPLICATION_WINDOW = 1;
    //所有程序窗口的base窗口,其他应用程序窗口都显示在它上面
    public static final int TYPE_BASE_APPLICATION   = 1;
    //所有Activity的窗口,只能配合Activity在当前APP使用
    public static final int TYPE_APPLICATION        = 2;
    //目标应用窗口未启动之前的那个窗口
    public static final int TYPE_APPLICATION_STARTING = 3;
    //最后一个应用窗口
    public static final int LAST_APPLICATION_WINDOW = 99;
  • 子窗口(1000~1999)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    //第一个子窗口
    public static final int FIRST_SUB_WINDOW        = 1000;
    // 面板窗口,显示于宿主窗口的上层,只能配合Activity在当前APP使用
    public static final int TYPE_APPLICATION_PANEL  = FIRST_SUB_WINDOW;
    // 媒体窗口(例如视频),显示于宿主窗口下层
    public static final int TYPE_APPLICATION_MEDIA  = FIRST_SUB_WINDOW+1;
    // 应用程序窗口的子面板,只能配合Activity在当前APP使用(PopupWindow默认就是这个Type)
    public static final int TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW+2;
    // 对话框窗口,只能配合Activity在当前APP使用
    public static final int TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW+3;
    // 在媒体窗口上显示叠加的窗口
    public static final int TYPE_APPLICATION_MEDIA_OVERLAY  = FIRST_SUB_WINDOW+4;
    // 最后一个子窗口
    public static final int LAST_SUB_WINDOW         = 1999;
  • 系统窗口(2000~2999)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    
        //系统窗口,非应用程序创建
        public static final int FIRST_SYSTEM_WINDOW     = 2000;
        //状态栏,只能有一个状态栏,位于屏幕顶端,其他窗口都位于它下方
        public static final int TYPE_STATUS_BAR         = FIRST_SYSTEM_WINDOW;
        //搜索栏,只能有一个搜索栏,位于屏幕上方
        public static final int TYPE_SEARCH_BAR         = FIRST_SYSTEM_WINDOW+1; 
            
        //电话窗口,它用于电话交互(特别是呼入),置于所有应用程序之上,状态栏之下,属于悬浮窗(并且给一个Activity的话按下HOME键会出现看不到桌面上的图标异常情况)
        public static final int TYPE_PHONE              = FIRST_SYSTEM_WINDOW+2;
            
        //系统警告提示窗口,出现在应用程序窗口之上,属于悬浮窗, 但是会被禁止
        public static final int TYPE_SYSTEM_ALERT       = FIRST_SYSTEM_WINDOW+3;
            
        //信息窗口,用于显示Toast, 不属于悬浮窗, 但有悬浮窗的功能, 缺点是在Android2.3上无法接收点击事件
        public static final int TYPE_TOAST              = FIRST_SYSTEM_WINDOW+5;
        //
        public static final int TYPE_KEYGUARD           = FIRST_SYSTEM_WINDOW+4;
        //锁屏窗口
        public static final int TYPE_KEYGUARD           = FIRST_SYSTEM_WINDOW+4;
        //系统顶层窗口,显示在其他一切内容之上,此窗口不能获得输入焦点,否则影响锁屏
        public static final int TYPE_SYSTEM_OVERLAY     = FIRST_SYSTEM_WINDOW+6;
        //电话优先,当锁屏时显示,此窗口不能获得输入焦点,否则影响锁屏
        public static final int TYPE_PRIORITY_PHONE     = FIRST_SYSTEM_WINDOW+7;
        //系统对话框窗口
        public static final int TYPE_SYSTEM_DIALOG      = FIRST_SYSTEM_WINDOW+8;
        //锁屏时显示的对话框
        public static final int TYPE_KEYGUARD_DIALOG    = FIRST_SYSTEM_WINDOW+9;
        //系统内部错误提示,显示在任何窗口之上
        public static final int TYPE_SYSTEM_ERROR       = FIRST_SYSTEM_WINDOW+10;
        //内部输入法窗口,显示于普通UI之上,应用程序可重新布局以免被此窗口覆盖
        public static final int TYPE_INPUT_METHOD       = FIRST_SYSTEM_WINDOW+11;
        //内部输入法对话框,显示于当前输入法窗口之上
        public static final int TYPE_INPUT_METHOD_DIALOG= FIRST_SYSTEM_WINDOW+12;
        //墙纸窗口
        public static final int TYPE_WALLPAPER          = FIRST_SYSTEM_WINDOW+13;
        //状态栏的滑动面板
        public static final int TYPE_STATUS_BAR_PANEL   = FIRST_SYSTEM_WINDOW+14;
        //安全系统覆盖窗口,这些窗户必须不带输入焦点,否则会干扰键盘
        public static final int TYPE_SECURE_SYSTEM_OVERLAY = FIRST_SYSTEM_WINDOW+15;
        //最后一个系统窗口
        public static final int LAST_SYSTEM_WINDOW      = 2999;
    
    2.窗口flags显示属性在WindowManager中也有定义
    
        //窗口特征标记
        public int flags;
        //当该window对用户可见的时候,允许锁屏
        public static final int FLAG_ALLOW_LOCK_WHILE_SCREEN_ON     = 0x00000001;
        //窗口后面的所有内容都变暗
        public static final int FLAG_DIM_BEHIND        = 0x00000002;
        //Flag:窗口后面的所有内容都变模糊
        public static final int FLAG_BLUR_BEHIND        = 0x00000004;
        //窗口不能获得焦点
        public static final int FLAG_NOT_FOCUSABLE      = 0x00000008;
        //窗口不接受触摸屏事件
        public static final int FLAG_NOT_TOUCHABLE      = 0x00000010;
        //即使在该window在可获得焦点情况下,允许该窗口之外的点击事件传递到当前窗口后面的的窗口去
        public static final int FLAG_NOT_TOUCH_MODAL    = 0x00000020;
        //当手机处于睡眠状态时,如果屏幕被按下,那么该window将第一个收到触摸事件
        public static final int FLAG_TOUCHABLE_WHEN_WAKING = 0x00000040;
        //当该window对用户可见时,屏幕处于常亮状态
        public static final int FLAG_KEEP_SCREEN_ON     = 0x00000080;
        //让window占满整个手机屏幕,不留任何边界
        public static final int FLAG_LAYOUT_IN_SCREEN   = 0x00000100;
        //允许窗口超出整个手机屏幕
        public static final int FLAG_LAYOUT_NO_LIMITS   = 0x00000200;
        //window全屏显示
        public static final int FLAG_FULLSCREEN      = 0x00000400;
        //恢复window非全屏显示
        public static final int FLAG_FORCE_NOT_FULLSCREEN   = 0x00000800;
        //开启窗口抖动
        public static final int FLAG_DITHER             = 0x00001000;
        //安全内容窗口,该窗口显示时不允许截屏
        public static final int FLAG_SECURE             = 0x00002000;
        //锁屏时显示该窗口
        public static final int FLAG_SHOW_WHEN_LOCKED = 0x00080000;
        //系统的墙纸显示在该窗口之后
        public static final int FLAG_SHOW_WALLPAPER = 0x00100000;
        //当window被显示的时候,系统将把它当做一个用户活动事件,以点亮手机屏幕
        public static final int FLAG_TURN_SCREEN_ON = 0x00200000;
        //该窗口显示,消失键盘
        public static final int FLAG_DISMISS_KEYGUARD = 0x00400000;
        //当该window在可以接受触摸屏情况下,让因在该window之外,而发送到后面的window的触摸屏可以支持split touch
        public static final int FLAG_SPLIT_TOUCH = 0x00800000;
        //对该window进行硬件加速,该flag必须在Activity或Dialog的Content View之前进行设置
        public static final int FLAG_HARDWARE_ACCELERATED = 0x01000000;
        //让window占满整个手机屏幕,不留任何边界
        public static final int FLAG_LAYOUT_IN_OVERSCAN = 0x02000000;
        //透明状态栏
        public static final int FLAG_TRANSLUCENT_STATUS = 0x04000000;
        //透明导航栏
        public static final int FLAG_TRANSLUCENT_NAVIGATION = 0x08000000;

悬浮窗案例实现

控制 Window 主要是通过 ViewManager 这个类来管理的,看看该类有什么方法

1
2
3
4
5
6
public interface ViewManager
{
    public void addView(View view, ViewGroup.LayoutParams params);
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
    public void removeView(View view);
}

添加 View

可以看到该类只有这 3 个方法,添加 View 的视图,更新 View 视图,移除视图等方法。首先看看代码具体是如何实现的,从上面代码得知,添加 View 和更新视图都需要 ViewGroup.LayoutParams ,和一个 View,看看具体代码是如何实现的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
				//创建suspensionview
        suspensionView = new SuspensionView(mContext);

        //创建一个布局参数
        layoutParams = new WindowManager.LayoutParams();
        layoutParams.width = suspensionView.width;
        layoutParams.height = suspensionView.height;

        // gravity 设置对其方式
        layoutParams.x += ScreenSizeUtil.getScreenWidth();
        layoutParams.y += ScreenSizeUtil.getScreenHeight() - ScreenSizeUtil.dp2px(10);
        layoutParams.gravity = Gravity.TOP | Gravity.LEFT;

        // 设置 Window type
        layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION;
        // 设置 flags 窗口不能获得焦点 | 即使在该window在可获得焦点情况下,允许该窗口之外的点击事件传递到当前窗口后面的的窗口去
        layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | 				 WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;

        //添加view
        windowManager.addView(suspensionView, layoutParams);
  1. 其中有几种参数是必须要设置:layoutParams 、gravity、type、flags
  2. 这里最重要的就是 type 类型的参数,设置不同 type 类型相对应不一样的层级,比如可以设置全局的悬浮窗,悬浮在其他应用之上,Android 8.0 以上是 TYPE_APPLICATION_OVERLAY 类型,8.0 以下是 TYPE_SYSTEM_ALERT 类型,还需要申请对应的悬浮窗权限

更新视图

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
case MotionEvent.ACTION_MOVE:
     float x = event.getRawX() - moveX;
     float y = event.getRawY() - moveY;
     //计算偏移量,刷新视图
     layoutParams.x += x;
     layoutParams.y += y;

     windowManager.updateViewLayout(suspensionView, layoutParams);
     moveX = event.getRawX();
     moveY = event.getRawY();
          break;

更新视图主要修改 x,y 的值,然后调用 updateViewLayout 即可

删除视图

1
 windowManager.removeView(suspensionView);

完整代码

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
public class SuspensionWindowUtil {

    private SuspensionView suspensionView;
    private WindowManager windowManager;
    private WindowManager.LayoutParams layoutParams;
    private Context mContext;
    private ValueAnimator valueAnimator;
    private int direction;
    private final int LEFT = 0;
    private final int RIGHT = 1;

    public SuspensionWindowUtil(Context context) {
        mContext = context;
        windowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
    }

    public void showSuspensionView() {

        //创建suspensionview
        suspensionView = new SuspensionView(mContext);

        //创建一个布局参数
        layoutParams = new WindowManager.LayoutParams();
        layoutParams.width = suspensionView.width;
        layoutParams.height = suspensionView.height;

        //gravity
        layoutParams.x += ScreenSizeUtil.getScreenWidth();
        layoutParams.y += ScreenSizeUtil.getScreenHeight() - ScreenSizeUtil.dp2px(10);
        layoutParams.gravity = Gravity.TOP | Gravity.LEFT;

        // 设置 Window type
        layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION;
        // 设置 flags
        layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
        // 设置 format
         layoutParams.format = PixelFormat.RGBA_8888;

        suspensionView.setOnTouchListener(touchListener);

        //添加view
        windowManager.addView(suspensionView, layoutParams);
    }


    public void hideSuspensionView() {
        if (suspensionView != null) {
            windowManager.removeView(suspensionView);
            stopAnim();
        }
    }


    View.OnTouchListener touchListener = new View.OnTouchListener() {
        float startX;
        float startY;
        float moveX;
        float moveY;

        @Override
        public boolean onTouch(View v, MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    startX = event.getRawX();
                    startY = event.getRawY();

                    moveX = event.getRawX();
                    moveY = event.getRawY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    float x = event.getRawX() - moveX;
                    float y = event.getRawY() - moveY;
                    //计算偏移量,刷新视图
                    layoutParams.x += x;
                    layoutParams.y += y;
                    windowManager.updateViewLayout(suspensionView, layoutParams);
                    moveX = event.getRawX();
                    moveY = event.getRawY();
                    break;
                case MotionEvent.ACTION_UP:
                    //判断松手时View的横坐标是靠近屏幕哪一侧,将View移动到依靠屏幕
                    float endX = event.getRawX();
                    float endY = event.getRawY();
                    if (endX < ScreenSizeUtil.getScreenWidth() / 2) {
                        direction = LEFT;
                        endX = 0;
                    } else {
                        direction = RIGHT;
                        endX = ScreenSizeUtil.getScreenWidth() - suspensionView.width;
                    }
                    if (moveX != startX) {
                        starAnim((int) moveX, (int) endX, direction);
                    }
                    //如果初始落点与松手落点的坐标差值超过5个像素,则拦截该点击事件
                    //否则继续传递,将事件交给OnClickListener函数处理
                    if (Math.abs(startX - moveX) > 5) {
                        return true;
                    }
                    break;
            }
            return false;
        }
    };


    private void starAnim(int startX, int endX, final int direction) {
        if (valueAnimator != null) {
            valueAnimator.cancel();
            valueAnimator = null;
        }
        valueAnimator = ValueAnimator.ofInt(startX, endX);
        valueAnimator.setDuration(500);
        valueAnimator.setRepeatCount(0);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                if (direction == LEFT) {
                    layoutParams.x = (int) animation.getAnimatedValue() - suspensionView.width / 2;
                } else {
                    layoutParams.x = (int) animation.getAnimatedValue();
                }
                if (suspensionView != null) {
                    windowManager.updateViewLayout(suspensionView, layoutParams);
                }
            }
        });
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.start();
    }

    private void stopAnim() {
        if (valueAnimator != null) {
            valueAnimator.cancel();
            valueAnimator = null;
        }
    }
}