Android弹窗

FrameWork层源码分析之popupWindow

2019-11-17  本文已影响0人  暴走的小青春

上文讲述了dialog的创建流程之后,接下来讲一下popwindow的创建以及和dialog的不同之处
首先看任何代码都要带着疑问去看,不然很容易一头雾水,先说几个问题
1.popwindow的显示分2种,一种是showAsDropDown和showAtLocation
其各自实现的原理
2.popwindow的动画是和dialog一样是window的动画么
3.popwindow的点击是如何处理的?
4.popwindow中和dialog一样新建了一个phonewindow,同时又用附属的activity的windowManager添加的么?
首先简单看下显示的代码

View mPopView = getLayoutInflater().inflate(R.layout.popwindow_layout, null);
                // 将转换的View放置到 新建一个popuwindow对象中
                mPopupWindow = new PopupWindow(mPopView,
                        WindowManager.LayoutParams.WRAP_CONTENT,
                        WindowManager.LayoutParams.WRAP_CONTENT);
                // 点击popuwindow外让其消失
                mPopupWindow.setOutsideTouchable(true);

                 mPopupWindow.showAsDropDown(mCenterTx, Gravity.CENTER, 0, 0);

可以看到用法和dialog如出一辙,主要看下其构造和show的逻辑吧

  public PopupWindow(View contentView, int width, int height, boolean focusable) {
        if (contentView != null) {
            mContext = contentView.getContext();
            mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
        }

        setContentView(contentView);
        setWidth(width);
        setHeight(height);
        setFocusable(focusable);
    }

可以看到用的还是依附的activity的windowManager,然后设置了几个属性。
接下来看下关键的showAsDropDown方法

public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
        if (isShowing() || mContentView == null) {
            return;
        }

        TransitionManager.endTransitions(mDecorView);

        registerForScrollChanged(anchor, xoff, yoff, gravity);

        mIsShowing = true;
        mIsDropdown = true;
//关键点一
        final WindowManager.LayoutParams p = createPopupLayoutParams(anchor.getWindowToken());
        preparePopup(p);关键点一

        final boolean aboveAnchor = findDropDownPosition(anchor, p, xoff, yoff, gravity);
        updateAboveAnchor(aboveAnchor);
//关键点二
        invokePopup(p);
    }

关键点一:

 public IBinder getWindowToken() {
        return mAttachInfo != null ? mAttachInfo.mWindowToken : null;
    }

无论是popwindow还是dialog时,show的时候都要绑定所属activity的token,而mAttachInfo这个对象是viewRootImp初始化的时候新建的
而初始化的时机就是WindowManagerGlobal.addview时候所创建的
所以这个token是为null的,在onCreate时候
接下来看下createPopupLayoutParams方法

private WindowManager.LayoutParams createPopupLayoutParams(IBinder token) {
        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();

        // These gravity settings put the view at the top left corner of the
        // screen. The view is then positioned to the appropriate location by
        // setting the x and y offsets to match the anchor's bottom-left
        // corner.
        p.gravity = Gravity.START | Gravity.TOP;
        p.flags = computeFlags(p.flags);
        p.type = mWindowLayoutType;
        p.token = token;
        p.softInputMode = mSoftInputMode;
        p.windowAnimations = computeAnimationResource();

        if (mBackground != null) {
            p.format = mBackground.getOpacity();
        } else {
            p.format = PixelFormat.TRANSLUCENT;
        }

        if (mHeightMode < 0) {
            p.height = mLastHeight = mHeightMode;
        } else {
            p.height = mLastHeight = mHeight;
        }

        if (mWidthMode < 0) {
            p.width = mLastWidth = mWidthMode;
        } else {
            p.width = mLastWidth = mWidth;
        }

        // Used for debugging.
        p.setTitle("PopupWindow:" + Integer.toHexString(hashCode()));

        return p;
    }

很显然,用的动画也是windowManger里属性的动画

来看下关键点二

private void preparePopup(WindowManager.LayoutParams p) {
        if (mContentView == null || mContext == null || mWindowManager == null) {
            throw new IllegalStateException("You must specify a valid content view by "
                    + "calling setContentView() before attempting to show the popup.");
        }

        // The old decor view may be transitioning out. Make sure it finishes
        // and cleans up before we try to create another one.
        if (mDecorView != null) {
            mDecorView.cancelTransitions();
        }

        // When a background is available, we embed the content view within
        // another view that owns the background drawable.
        if (mBackground != null) {
            mBackgroundView = createBackgroundView(mContentView);
            mBackgroundView.setBackground(mBackground);
        } else {
            mBackgroundView = mContentView;
        }

        mDecorView = createDecorView(mBackgroundView);

        // The background owner should be elevated so that it casts a shadow.
        mBackgroundView.setElevation(mElevation);

        // We may wrap that in another view, so we'll need to manually specify
        // the surface insets.
        final int surfaceInset = (int) Math.ceil(mBackgroundView.getZ() * 2);
        p.surfaceInsets.set(surfaceInset, surfaceInset, surfaceInset, surfaceInset);
        p.hasManualSurfaceInsets = true;

        mPopupViewInitialLayoutDirectionInherited =
                (mContentView.getRawLayoutDirection() == View.LAYOUT_DIRECTION_INHERIT);
        mPopupWidth = p.width;
        mPopupHeight = p.height;
    }

可以看到popwindow并没有像dialog一样,新建了一个phonewindow,那它是window么?肯定是,只不过它的windowManager.layoutParams的属性要自己配置,而且它也没有常规setContentView的这么多的嵌套
那既然没有新建phonewindow,那它的点击事件的回掉又从那里来呢?

 private class PopupDecorView extends FrameLayout {
        private TransitionListenerAdapter mPendingExitListener;

        public PopupDecorView(Context context) {
            super(context);
        }

        @Override
        public boolean dispatchKeyEvent(KeyEvent event) {
            if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
                if (getKeyDispatcherState() == null) {
                    return super.dispatchKeyEvent(event);
                }

                if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
                    final KeyEvent.DispatcherState state = getKeyDispatcherState();
                    if (state != null) {
                        state.startTracking(event, this);
                    }
                    return true;
                } else if (event.getAction() == KeyEvent.ACTION_UP) {
                    final KeyEvent.DispatcherState state = getKeyDispatcherState();
                    if (state != null && state.isTracking(event) && !event.isCanceled()) {
                        dismiss();
                        return true;
                    }
                }
                return super.dispatchKeyEvent(event);
            } else {
                return super.dispatchKeyEvent(event);
            }
        }
....

可以看到它的点击事件全部有自己的PopupDecorView重写了,并没有遵循传统的decorview的事件的传递方式,所以也没有了回掉
关键点三

 private void invokePopup(WindowManager.LayoutParams p) {
        if (mContext != null) {
            p.packageName = mContext.getPackageName();
        }

        final PopupDecorView decorView = mDecorView;
        decorView.setFitsSystemWindows(mLayoutInsetDecor);

        setLayoutDirectionFromAnchor();

        mWindowManager.addView(decorView, p);

        if (mEnterTransition != null) {
            decorView.requestEnterTransition(mEnterTransition);
        }
    }

这里还是用activity的windowManager添加的,后续的操作和dialog的显示是一样的,这里就不再特别分析了,有个关于token的地方要着重说下
window的adjustLayoutParamsForSubWindow方法

void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
        CharSequence curTitle = wp.getTitle();
        if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
            wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
            if (wp.token == null) {
                View decor = peekDecorView();
                if (decor != null) {
                    wp.token = decor.getWindowToken();
                }
            }
            if (curTitle == null || curTitle.length() == 0) {
                String title;
                if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA) {
                    title="Media";
                } else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA_OVERLAY) {
                    title="MediaOvr";
                } else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_PANEL) {
                    title="Panel";
                } else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL) {
                    title="SubPanel";
                } else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL) {
                    title="AboveSubPanel";
                } else if (wp.type == WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG) {
                    title="AtchDlg";
                } else {
                    title=Integer.toString(wp.type);
                }
                if (mAppName != null) {
                    title += ":" + mAppName;
                }
                wp.setTitle(title);
            }
        } else {
            if (wp.token == null) {
                wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
            }
            if ((curTitle == null || curTitle.length() == 0)
                    && mAppName != null) {
                wp.setTitle(mAppName);
            }
        }
        if (wp.packageName == null) {
            wp.packageName = mContext.getPackageName();
        }
        if (mHardwareAccelerated) {
            wp.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
        }
    }

可以看到由于popwindow是子窗口类型,token为null的情况下会调用activity的decorview的token,当然在oncreate时也为null了,而dialog就不同了,默认给到的就是null,走了else的逻辑直接把mApptoken也就是activity的token给到了,这也是为何popwindow不能再oncreate显示而dialog可以的最根本原因

总结

先来说下几个问题
1.显示的位置其实和WindowManager.layoutParams有关showAsDropDown其实就是算出锚地的位置然后放到其下方,而showAtLocation没有此段逻辑
2.动画的显示,其实也是依靠WindowManager.layoutParam的属性,可以说一样的
3.点击区域的处理,这个和dialog有所不同,由于不是系统的decorview,所以重写了分发的方法。
4.源码中表明了都是windowManager添加的,但是popwindow并没有新建phonewinow,其实新建一个子window只要windowManager和decorview就能新建,新建了phonewindow默认的属性就是应用类型的window而已,这也是两者最大的不同之处
很多人说popwindow比dialog都要“轻”,但是又说不出个所以然来,其实无非就是子window上不能新建子window,而dialog上可以添加子window罢了

注意点:

1.popwindow和dialog都会存在activity的token不存在了,而不能显示的情况.要做好基类的统一处理
2.popwinow一般用在依靠某个view显示的位置情况,dialog则不然

上一篇下一篇

猜你喜欢

热点阅读