弹窗

[源码分析]悬浮框工具条实现

2019-11-11  本文已影响0人  萝卜小青菜丶

有几个关于window的基础知识需要知道

1.每个Window都对应了一个View和一个ViewRootImpl
2.View是Window存在的实体
3.Window的具体实现位于WindowManagerService中
4.WindowManager是外界访问Window的入口
5.WindowManager和WindowManagerService交互是一个IPC过程
6.Window的添加过程实际上是一次IPC的调用(为什么会有token的原因)

通常有3中window类型

1.Application windows

取值范围从FIRST_APPLICATION_WINDOW(Constant Value: 1 (0x00000001))到 LAST_APPLICATION_WINDOW(Constant Value: 99 (0x00000063))
这种window是普通的顶层window.
这些种类的window的token必须设置成Activity的token(如果这个token是null,那么需要你来提供)

2.Sub-windows

取值范围从FIRST_SUB_WINDOW(Constant Value: 1000 (0x000003e8))到 LAST_SUB_WINDOW(Constant Value: 1999 (0x000007cf))
这种window一般都和其他顶层window关联在一起,
这种window的token必须是关联的window的token

3.System windows

取值范围为从 FIRST_SYSTEM_WINDOW(Constant Value: 2000 (0x000007d0)) 到 LAST_SYSTEM_WINDOW(Constant Value: 2999 (0x00000bb7))
这种window是特殊的window类型,一般是系统用户特殊目的使用的
这种window不应该被普通程序使用,想要使用他们必须拥有特别的权限
(也就是说从api23开始不要想通过这种创建这种window的方式构造悬浮窗了,系统默认就不允许)

type有如下类型

源码android.viewWindowManager.java文件中可以看到,并有相关的解释,总得来说有个原则,type值越大则显示的越靠上层,上面的这些type常量都是系统中各种UI默认的使用的值,如果要达到你想要达到的效果甚至可以自己设置想要的int值,比如想要覆盖在状态栏之上,就设置个大于2001且小于2999的值就行。具体内容如下:

/**
 * Start of types of sub-windows.  The {@link #token} of these windows
 * must be set to the window they are attached to.  These types of
 * windows are kept next to their attached window in Z-order, and their
 * coordinate space is relative to their attached window.
 */
public static final int FIRST_SUB_WINDOW = 1000;

/**
 * Window type: a panel on top of an application window.  These windows
 * appear on top of their attached window.
 */
public static final int TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW;

/**
 * Window type: window for showing media (such as video).  These windows
 * are displayed behind their attached window.
 */
public static final int TYPE_APPLICATION_MEDIA = FIRST_SUB_WINDOW + 1;

...

/**
 * Window type: a above sub-panel on top of an application window and it's
 * sub-panel windows. These windows are displayed on top of their attached window
 * and any {@link #TYPE_APPLICATION_SUB_PANEL} panels.
 * @hide
 */
public static final int TYPE_APPLICATION_ABOVE_SUB_PANEL = FIRST_SUB_WINDOW + 5;

/**
 * End of types of sub-windows.
 */
public static final int LAST_SUB_WINDOW = 1999;

/**
 * Start of system-specific window types.  These are not normally
 * created by applications.
 */
public static final int FIRST_SYSTEM_WINDOW     = 2000;

/**
 * Window type: the status bar.  There can be only one status bar
 * window; it is placed at the top of the screen, and all other
 * windows are shifted down so they are below it.
 * In multiuser systems shows on all users' windows.
 */
public static final int TYPE_STATUS_BAR         = FIRST_SYSTEM_WINDOW;

/**
 * Window type: the search bar.  There can be only one search bar
 * window; it is placed at the top of the screen.
 * In multiuser systems shows on all users' windows.
 */
public static final int TYPE_SEARCH_BAR         = FIRST_SYSTEM_WINDOW+1;

...

/**
 * Window type: Application overlay windows are displayed above all activity windows
 * (types between {@link #FIRST_APPLICATION_WINDOW} and {@link #LAST_APPLICATION_WINDOW})
 * but below critical system windows like the status bar or IME.
 * <p>
 * The system may change the position, size, or visibility of these windows at anytime
 * to reduce visual clutter to the user and also manage resources.
 * <p>
 * Requires {@link android.Manifest.permission#SYSTEM_ALERT_WINDOW} permission.
 * <p>
 * The system will adjust the importance of processes with this window type to reduce the
 * chance of the low-memory-killer killing them.
 * <p>
 * In multi-user systems shows only on the owning user's screen.
 */
public static final int TYPE_APPLICATION_OVERLAY = FIRST_SYSTEM_WINDOW + 38;

/**
 * End of types of system windows.
 */
public static final int LAST_SYSTEM_WINDOW      = 2999;

/**
 * @hide
 * Used internally when there is no suitable type available.
 */
public static final int INVALID_WINDOW_TYPE = -1;

自定义一个View,在初始化时,添加属性,就能使其可以悬浮

windowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
wmLayoutParams = new WindowManager.LayoutParams();
//4.4以上toast能获取到焦点,能处理事件,但以下则不能
//4.4以上system_error程序退到后台就不能悬浮于系统上
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    wmLayoutParams.type = WindowManager.LayoutParams.TYPE_TOAST;
} else {
    wmLayoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
}
wmLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
wmLayoutParams.format = PixelFormat.RGBA_8888;
wmLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
wmLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
wmLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;

重写onInterceptTouchEvent、onTouchEvent方法添加控制处理

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    if (isCanTouchMove()) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                xDownInScreen = event.getRawX();
                yDownInScreen = event.getRawY() - getStatusBarHeight();
                break;
            case MotionEvent.ACTION_MOVE:
                xInScreen = event.getRawX();
                yInScreen = event.getRawY() - getStatusBarHeight();
                float dx = Math.abs(xInScreen - xDownInScreen);
                float dy = Math.abs(yInScreen - yDownInScreen);
                if (dx < 10f && dy < 10f) {
                    return false;
                } else {
                    //拦截,交给自己的onTouch事件处理
                    return true;
                }
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                xDownInScreen = 0;
                yDownInScreen = 0;
                break;
        }
    }
    return super.onInterceptTouchEvent(event);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    if (isCanTouchMove()) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                xInScreen = event.getRawX();
                yInScreen = event.getRawY() - getStatusBarHeight();
                break;
            case MotionEvent.ACTION_MOVE:
                xInScreen = event.getRawX();
                yInScreen = event.getRawY() - getStatusBarHeight();
                wmLayoutParams.x = (int) (xInScreen - getWidth() / 2);
                wmLayoutParams.y = (int) (yInScreen - getHeight() / 2);
                updatePosition();
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                updatePosition();
                if (alignSide)
                    alignSide();
                break;
            default:
                break;
        }
    }
    return super.onTouchEvent(event);
}

添加更新位置方法

private void updatePosition() {
    windowManager.updateViewLayout(this, wmLayoutParams);
}

public void updatePosition(int x, int y) {
    if (x >= 0)
        wmLayoutParams.x = x;
    if (y >= 0)
        wmLayoutParams.y = y;
    // 刷新
    updatePosition();
}

模拟手机系统悬浮框,可靠边处理

@Override
protected void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    alignSide();
}

/**
 * 靠边
 */
private void alignSide() {
    int width = getResources().getDisplayMetrics().widthPixels;
    int height = getResources().getDisplayMetrics().heightPixels;
    if (-1 != alignGravity) {
        switch (alignGravity) {
            case Gravity.LEFT:
                wmLayoutParams.x = 0;
                break;
            case Gravity.TOP:
                wmLayoutParams.y = 0;
                break;
            case Gravity.RIGHT:
                wmLayoutParams.x = width;
                break;
            case Gravity.BOTTOM:
                wmLayoutParams.y = height;
                break;
            default:
                break;
        }

    } else {
        //以屏幕中点为原点,横向为X轴,纵向为Y轴计算
        if (xInScreen < width / 2) {//第二、三象限
            if (yInScreen < height / 2) {//第二象限
                if (xInScreen < yInScreen) {
                    wmLayoutParams.x = 0;
                } else {
                    wmLayoutParams.y = 0;
                }
            } else {//第三象限
                if (xInScreen < height - yInScreen) {
                    wmLayoutParams.x = 0;
                } else {
                    wmLayoutParams.y = height;
                }
            }
        } else {//第一、四象限
            if (yInScreen < height / 2) {//第一象限
                if (width - xInScreen < yInScreen) {
                    wmLayoutParams.x = width;
                } else {
                    wmLayoutParams.y = 0;
                }
            } else {//第四象限
                if (width - xInScreen < height - yInScreen) {
                    wmLayoutParams.x = width;
                } else {
                    wmLayoutParams.y = height;
                }
            }
        }
    }
    updatePosition();
}

然后添加创建方法和移除方法:

public boolean addToWindow() {
        boolean result;
        if (windowManager != null) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                if (!isAttachedToWindow()) {
                    windowManager.addView(this, wmLayoutParams);
                    result = true;
                } else {
                    result = false;
                }
            } else {
                try {
                    if (getParent() == null) {
                        windowManager.addView(this, wmLayoutParams);
                    }
                    result = true;
                } catch (Exception e) {
                    result = false;
                }
            }
        } else {
            result = false;
        }
        if (result && null != addedListener) {
            addedListener.onAddedToWindow(getPosition().x, getPosition().y);
        }
        return result;
    }

    public boolean removeFromWindow() {
        boolean result;
        if (windowManager != null) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                if (isAttachedToWindow()) {
                    windowManager.removeViewImmediate(this);
                    result = true;
                } else {
                    result = false;
                }
            } else {
                try {
                    if (getParent() != null) {
                        windowManager.removeViewImmediate(this);
                    }
                    result = true;
                } catch (Exception e) {
                    result = false;
                }
            }
        } else {
            result = false;
        }
        if (result && null != removedListener) {
            removedListener.onRemovedToWindow(getPosition().x, getPosition().y);
        }
        return result;
    }

接下来就可以愉快的创建它了,如果在service中启动它的话,那么它就是一个依赖于应用的全局悬浮框

if (null == floatView) {
    floatView = new FloatView(this);
}
floatView.addToWindow();
WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
int width = wm.getDefaultDisplay().getWidth();
int height = wm.getDefaultDisplay().getHeight();
floatView.updatePosition(width, height);

附上github,view类完整代码:https://github.com/YJF-Lib/android-floatview

怎么创建的,为什么不需要权限,分析一下源码

首先,自定义的view,在创建时使用的是windowManager.addView(this, wmLayoutParams)方法,所以先看一下WindowManager类:
WindowManager继承ViewManager

@SystemService(Context.WINDOW_SERVICE)
public interface WindowManager extends ViewManager 
{
    ...
}

而addView是定义在ViewManager的接口

/** Interface to let you add and remove child views to an Activity. To get an instance
  * of this class, call {@link android.content.Context#getSystemService(java.lang.String) Context.getSystemService()}.
  */
public interface ViewManager
{
    /**
     * Assign the passed LayoutParams to the passed View and add the view to the window.
     * <p>Throws {@link android.view.WindowManager.BadTokenException} for certain programming
     * errors, such as adding a second view to a window without removing the first view.
     * <p>Throws {@link android.view.WindowManager.InvalidDisplayException} if the window is on a
     * secondary {@link Display} and the specified display can't be found
     * (see {@link android.app.Presentation}).
     * @param view The view to be added to this window.
     * @param params The LayoutParams to assign to view.
     */
    public void addView(View view, ViewGroup.LayoutParams params);
    public void updateViewLayout(View view, ViewGroup.LayoutParams params);
    public void removeView(View view);
}

在WindowManagerImpl继承了WindowManager接口并实现addView方法

public final class WindowManagerImpl implements WindowManager {
    private final Display mDisplay;
    private final WindowManagerGlobal mGlobal;
    private final Window mParentWindow;

    public WindowManagerImpl(Display var1) {
        this(var1, (Window)null);
    }

    private WindowManagerImpl(Display var1, Window var2) {
        this.mGlobal = WindowManagerGlobal.getInstance();
        this.mDisplay = var1;
        this.mParentWindow = var2;
    }

    public void addView(View var1, LayoutParams var2) {
        this.mGlobal.addView(var1, var2, this.mDisplay, this.mParentWindow);
    }
    ...
}

mGlobal是WindowManagerGlobal的实例,所以调用的的是WindowManagerGlobal.addView(),代码中创建了一个ViewRootImpl实例root,并且调用root.setView传入view

public void addView(View view, android.view.ViewGroup.LayoutParams params, Display display, Window parentWindow) {
    
    ...
    
    ViewRootImpl root;
    synchronized(this.mLock) {
    
        ...
        
        root = new ViewRootImpl(view.getContext(), display);
        view.setLayoutParams(wparams);
        this.mViews.add(view);
        this.mRoots.add(root);
        this.mParams.add(wparams);
    }

    try {
        root.setView(view, wparams, panelParentView);
    } catch (RuntimeException var15) {
        Object var18 = this.mLock;
        synchronized(this.mLock) {
            index = this.findViewLocked(view, false);
            if (index >= 0) {
                this.removeViewLocked(index, true);
            }
        }
        throw var15;
    }   
}

在ViewRootImpl.setView里最关键的代码是:

public void setView(View view, LayoutParams attrs, View panelParentView) {
    synchronized(this) {
        if (this.mView == null) {
            
            ...

            int res;
            try {
                this.mOrigWindowType = this.mWindowAttributes.type;
                this.mAttachInfo.mRecomputeGlobalAttributes = true;
                this.collectViewAttributes();
                res = this.mWindowSession.addToDisplay(this.mWindow, this.mSeq, this.mWindowAttributes, this.getHostVisibility(), this.mDisplay.getDisplayId(), this.mAttachInfo.mContentInsets, this.mAttachInfo.mStableInsets, this.mInputChannel);
            } catch (RemoteException var20) {
                this.mAdded = false;
                this.mView = null;
                this.mAttachInfo.mRootView = null;
                this.mInputChannel = null;
                this.mFallbackEventHandler.setView((View)null);
                this.unscheduleTraversals();
                this.setAccessibilityFocus((View)null, (AccessibilityNodeInfo)null);
                throw new RuntimeException("Adding window failed", var20);
            } finally {
                if (restore) {
                    attrs.restore();
                }

            }
            
            ...

        }

    }
}

mWindowSession的类型是IWindowSession,mWindow的类型是IWindow.Stub,这句代码就是利用AIDL进行IPC,实际被调用的是Session.addToDisplay:

@Override
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
        int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
        Rect outOutsets, InputChannel outInputChannel) {
    return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
            outContentInsets, outStableInsets, outOutsets, outInputChannel);
}

由addToDisplay调用WindowManagerService的addWindow:

public int addWindow(Session session, IWindow client, int seq,
        WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
        Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
        InputChannel outInputChannel) {
    int[] appOp = new int[1];
    int res = mPolicy.checkAddPermission(attrs, appOp);
    if (res != WindowManagerGlobal.ADD_OKAY) {
        return res;
    }

    ...

    synchronized(mWindowMap) {
        ...

        mPolicy.adjustWindowParamsLw(win.mAttrs);
        win.setShowToOwnerOnlyLocked(mPolicy.checkShowToOwnerOnly(attrs));

        res = mPolicy.prepareAddWindowLw(win, attrs);
        if (res != WindowManagerGlobal.ADD_OKAY) {
            return res;
        }

        ...
    }

   ...

    return res;
}

从这方法中,第一段就能看到使用mPolicy.checkAddPermission(attrs, appOp)检查权限,mPolicy是标记为final的成员变量:

final WindowManagerPolicy mPolicy =PolicyManager.makeNewWindowManager();

继续看PolicyManager.makeNewWindowManager,实际是Policy.makeNewWindowManager()

public WindowManagerPolicy makeNewWindowManager(){
    return new PhoneWindowManager();
}

现在我们知道mPolicy实际上是PhoneWindowManager,那么intres =mPolicy.checkAddPermission(attrs, appOp);实际调用的代码是PhoneWindowManager.checkAddPermission(),可以看到TYPE_TOAST不检查permission,TYPE_PHONE检查SYSTEM_ALERT_WINDOW权限

/** {@inheritDoc} */
@Override
public int checkAddPermission(WindowManager.LayoutParams attrs, int[] outAppOp) {
    
    ...

    if (!isSystemAlertWindowType(type)) {
        switch (type) {
            case TYPE_TOAST:
                // Only apps that target older than O SDK can add window without a token, after
                // that we require a token so apps cannot add toasts directly as the token is
                // added by the notification system.
                // Window manager does the checking for this.
                outAppOp[0] = OP_TOAST_WINDOW;
                return ADD_OKAY;
            case TYPE_DREAM:
            case TYPE_INPUT_METHOD:
            case TYPE_WALLPAPER:
            case TYPE_PRESENTATION:
            case TYPE_PRIVATE_PRESENTATION:
            case TYPE_VOICE_INTERACTION:
            case TYPE_ACCESSIBILITY_OVERLAY:
            case TYPE_QS_DIALOG:
                // The window manager will check these.
                return ADD_OKAY;
        }
        return mContext.checkCallingOrSelfPermission(INTERNAL_SYSTEM_WINDOW)
                == PERMISSION_GRANTED ? ADD_OKAY : ADD_PERMISSION_DENIED;
    }

    ...
}

接着刚才addWindow方法的分析,继续看下面一句,:

public int addWindow(Session session, IWindow client, int seq,
        WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
        Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
        InputChannel outInputChannel) {
    int[] appOp = new int[1];
    int res = mPolicy.checkAddPermission(attrs, appOp);
    if (res != WindowManagerGlobal.ADD_OKAY) {
        return res;
    }

    ...

    synchronized(mWindowMap) {
        ...

        mPolicy.adjustWindowParamsLw(win.mAttrs);
        win.setShowToOwnerOnlyLocked(mPolicy.checkShowToOwnerOnly(attrs));

        res = mPolicy.prepareAddWindowLw(win, attrs);
        if (res != WindowManagerGlobal.ADD_OKAY) {
            return res;
        }

        ...
    }

   ...

    return res;
}

还是在PhoneWindowManager里的方法adjustWindowParamsLw(),给出了三个版本的实现,一个是2.0到2.3.7实现的版本,一个是4.0.1到4.3.1实现的版本,一个是4.4实现的版本:

//Android 2.0 - 2.3.7 PhoneWindowManager
public void adjustWindowParamsLw(WindowManager.LayoutParams attrs) {
  switch (attrs.type) {
    case TYPE_SYSTEM_OVERLAY:
    case TYPE_SECURE_SYSTEM_OVERLAY:
    case TYPE_TOAST:
      // These types of windows can't receive input events.
      attrs.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
          | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
      break;
  }
}
//Android 4.0.1 - 4.3.1 PhoneWindowManager
public void adjustWindowParamsLw(WindowManager.LayoutParams attrs) {
  switch (attrs.type) {
    case TYPE_SYSTEM_OVERLAY:
    case TYPE_SECURE_SYSTEM_OVERLAY:
    case TYPE_TOAST:
      // These types of windows can't receive input events.
      attrs.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
          | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
      attrs.flags &= ~WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
      break;
  }
}
//Android 4.4 PhoneWindowManager
@Override
public void adjustWindowParamsLw(WindowManager.LayoutParams attrs) {
  switch (attrs.type) {
    case TYPE_SYSTEM_OVERLAY:
    case TYPE_SECURE_SYSTEM_OVERLAY:
      // These types of windows can't receive input events.
      attrs.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
          | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
      attrs.flags &= ~WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
      break;
  }
}

小结

在4.0.1以前,当我们使用TYPE_TOAST, Android会偷偷给我们加上FLAG_NOT_FOCUSABLE和FLAG_NOT_TOUCHABLE,4.0.1开始,会额外再去掉FLAG_WATCH_OUTSIDE_TOUCH,这样真的是什么事件都没了.而4.4开始,TYPE_TOAST被移除了,所以从4.4开始,使用TYPE_TOAST的同时还可以接收触摸事件和按键事件了,而4.4以前只能显示出来,不能交互。

上一篇下一篇

猜你喜欢

热点阅读