美文网首页弹窗
[源码分析]悬浮框工具条实现

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

作者: 萝卜小青菜丶 | 来源:发表于2019-11-11 17:14 被阅读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以前只能显示出来,不能交互。

    相关文章

      网友评论

        本文标题:[源码分析]悬浮框工具条实现

        本文链接:https://www.haomeiwen.com/subject/zjzzbctx.html