PopupWindow源码分析

本文探讨PopupWindow与Dialog的区别,并深入分析PopupWindow的源码。PopupWindow允许用户在不失去Activity焦点的情况下进行交互,而Dialog则不具备此特性。PopupWindow通过将View添加到窗口实现显示,而非创建新的window对象。在使用时,需要注意PopupWindow需在特定事件中或新线程内显示。文章重点解析了PopupWindow的构造函数和setInputMethodMode()方法,揭示其内部工作机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


说到弹窗,大家可能会立马想到diaolg,其实还有popupwindow,popupwindow是一种很重要的显示控件,这里首先介绍一下它与dialog的简单区别:

1.PopWindow实质就是弹出式菜单,它不会使依赖的activity组件失去焦点,PopupWindow弹出后可以继续 与依赖的Activity进行交互,然而dialog却不行。

2.Dialog在构造函数里面都会去新建window,这样就可设置callback回调,所以dialog可以处理很多监听事件,然而popupwindow里面没有window对象,它仅仅是把view添加到窗口中去,其实popupwindow就是用来在指定的位置显示一个view。

3.PopupWindow是一个阻 塞的对话框,如果你直接在Activity的onCreate等方法中显示它则会报错,所以PopupWindow必须在某个事件中显示地或者是开启一个 新线程去调用。

下面就直接看看pop的源码分析吧。

首先看看它的构造函数,这里我们选择比较常见的一种来分析

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


构造函数里面主要是初始化了一下参数,比如获取view的context,windowmanager以及设置contentview,其中contentView需要显示的布局。

接下来看看
setInputMethodMode()

public void setInputMethodMode(int mode) {

        mode = mMzHelper.setInputMethodMode(mode);
        mInputMethodMode = mode;
    }

该方法主要是设置pop出现之前键盘的显示。其中主要有三种mode
INPUT_METHOD_NOT_NEEDED,INPUT_METHOD_NEEDED,INPUT_METHOD_FROM_FOCUSABLE。

public void setElevation(float elevation) {
        mElevation = elevation;
    }

该方法是设置pop背景边框阴影的,系统默认的是16。
接下来也是最重要的,就是popwindow的显示过程。
显示经常用的有以下几种方法:
public void showAsDropDown(View anchor)
showAsDropDown(View anchor, int xoff, int yoff)
showAtLocation(View parent, int gravity, int x, int y)
由于显示的原理都一样,只是参数不一样,这里就分析第二种方法:

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

        registerForScrollChanged(anchor, xoff, yoff, gravity);
        //anchor是Activity中PopWindow准备依附的View,这个View的token实质也是Activity的Window中的token,也即Activity的token
        //第一步   初始化WindowManager.LayoutParams
        WindowManager.LayoutParams p = createPopupLayout(anchor.getWindowToken());
        //第二步
        preparePopup(p);
        ......
        //第三步
        invokePopup(p);
    }


该方法中的参数: View anchor是pop所依赖的显示anchor,
xoff: PopupWindow相对于anchor的左下角x轴方向的偏移大小
yoff: PopupWindow相对于anchor的左下角y轴方向的偏移大小。
如果当前pop处于显示状态或者显示的view为空就返回。这里分析一下比较重要的方法,
registerForScrollChanged(anchor, xoff, yoff, gravity);



 private void registerForScrollChanged(View anchor, int xoff, int yoff, int gravity) {
        unregisterForScrollChanged();

        mAnchor = new WeakReference<View>(anchor);
        ViewTreeObserver vto = anchor.getViewTreeObserver();
        if (vto != null) {
            vto.addOnScrollChangedListener(mOnScrollChangedListener);
        }

        mAnchorXoff = xoff;
        mAnchorYoff = yoff;
        mAnchoredGravity = gravity;
    }

从字面意思不难看出,该方法是与滑动滑动监听相关的函数。因为有可能anchor会滑动或者显示的pop会有滑动的需要,这里就是增加一个全局的监听操作,增加之前先注销掉之前已有的监听,主要是防止ViewTreeObserver失效。在这里使用WeakReference ,它是一个弱引用对anchor,防止anchor这个类已经无用了,anchor仍然无法回收内存,这样做可以有效的防止内存泄露。
该监听的回调方法如下


  private final OnScrollChangedListener mOnScrollChangedListener =
        new OnScrollChangedListener() {
            @Override
            public void onScrollChanged() {
                final View anchor = mAnchor != null ? mAnchor.get() : null;
                
                if (!(anchor != null && anchor.getParent() == null)) {
                    //@}
                    if (anchor != null && mPopupView != null) {
                        final WindowManager.LayoutParams p = (WindowManager.LayoutParams)
                                mPopupView.getLayoutParams();

                        updateAboveAnchor(findDropDownPosition(anchor, p, mAnchorXoff, mAnchorYoff,
                                mAnchoredGravity));
                        update(p.x, p.y, -1, -1, true);
                    }
                }
            }
        };

接着往下看,方法createPopupLayout()。


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

        p.gravity = Gravity.START | Gravity.TOP;
        p.width = mLastWidth = mWidth;
        p.height = mLastHeight = mHeight;
        ...
        p.token = token;
        p.softInputMode = mSoftInputMode;

        return p;
    }

该方法就是为popwindow设置显示参数,(1)创建一个WindowManager.LayoutParams的实例p  (2)设置一些参数设置了gravity,width,heigt,还有输入法与pop交互状态关系
接下来利用 createPopupLayout返回的参数p为pop的显示做一些准备工作,


private void preparePopup(WindowManager.LayoutParams p) {
          ...

        if (mBackground != null) {
            final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams();
            int height = ViewGroup.LayoutParams.MATCH_PARENT;
            if (layoutParams != null &&
                    layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
                height = ViewGroup.LayoutParams.WRAP_CONTENT;
            }
            PopupViewContainer popupViewContainer = new PopupViewContainer(mContext);
            PopupViewContainer.LayoutParams listParams = new PopupViewContainer.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT, height
            );
            popupViewContainer.setBackground(mBackground);
            popupViewContainer.addView(mContentView, listParams);

            mPopupView = popupViewContainer;
        } else {
            mPopupView = mContentView;
        }

        mPopupView.setElevation(mElevation);
        mPopupViewInitialLayoutDirectionInherited =
                (mPopupView.getRawLayoutDirection() == View.LAYOUT_DIRECTION_INHERIT);
        mPopupWidth = p.width;
        mPopupHeight = p.height;
    }

这里首先会判断设置背景是否为空,如果为空,直接把 mContentView设置为pop的,如果不为空就会进行一系列的操作。首先创建一个PopuViewContainner,它是FrameLayout的一个子类,把背景设置给PopuViewContainner,然后再把mContentView放进去,然后把PopuViewContainner赋值给mPopupView,这里把width,high同时设置进去了。这里有必要对 PopupViewContainer子类进行详细说明:private class PopupViewContainer extends FrameLayout {
      
private class PopupViewContainer extends FrameLayout {
        ......
        @Override
        protected int[] onCreateDrawableState(int extraSpace) {
            ......
        }

        @Override
        public boolean dispatchKeyEvent(KeyEvent event) {
            ......
        }

        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            if (mTouchInterceptor != null && mTouchInterceptor.onTouch(this, ev)) {
                return true;
            }
            return super.dispatchTouchEvent(ev);
        }

        @Override
        public boolean onTouchEvent(MotionEvent event) {
            ......
            if(xxx) {
                dismiss();
            }
            ......
        }

        @Override
        public void sendAccessibilityEvent(int eventType) {
            ......
        }
    }

可以看见,这个PopupViewContainer是一个PopWindow的内部私有类,它继承了FrameLayout,在其中重写了Key和 Touch事件的分发处理逻辑。同时查阅PopupView可以发现,PopupView类自身没有重写Key和Touch事件的处理,所以如果没有将传 入的View对象放入封装的ViewGroup中,则点击Back键或者PopWindow以外的区域PopWindow是不会消失的(其实PopWindow中没有向Activity及Dialog一样new新的Window,所以不会有新的callback设置,也就没法处理各种事件了)。
所以应用在使用pop的时候会出现点击back或者屏幕以外的区域pop不会消失的问题。
这些都准备完毕了,那么接下来就是显示pop的过程的了。

 private void invokePopup(WindowManager.LayoutParams p) {
        if (mContext != null) {
            p.packageName = mContext.getPackageName();
        }
        mPopupView.setFitsSystemWindows(mLayoutInsetDecor);
        setLayoutDirectionFromAnchor();
        mWindowManager.addView(mPopupView, p);
    }

以看见,这里使用了Activity的WindowManager将我们的PopWindow进行了显示。
到此可以发现,PopWindow的实质无非也是使用WindowManager的addView、updateViewLayout、 removeView进行一些操作展示。与Dialog不同的地方是没有新new Window而已(也就没法设置callback,无法消费事件,也就是前面说的PopupWindow弹出后可以继续与依赖的Activity进行交互 的原因)。
这就是pop首次显示的基本流程了,如果pop有数据参数更新,下次显示的时候会调用update() 方法,调用过程与首次显示基本一致。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值