BasePopupWindow解析

815 阅读3分钟

最近在实现PopupWindow的时候发现一个很好用的开源库----BasePopupWindow

没有使用封装的开源库之前,我们定义一个PopupWindowe正常的步骤是


private var popWindow = PopupWindow()

popView = LayoutInflater.from(mContext).inflate(R.layout.gis_select_pop_layout,null)
popWindow.apply {
    contentView = popView
    isOutsideTouchable = true//必须设置背景
    isTouchable = true
    setBackgroundDrawable(BitmapDrawable())
}
popView?.lists?.setOnItemClickListener { parent, view, position, id ->
    val currentSelectedInfo  = mShowList[position]
    if(currentSelectedInfo.key == selectedInfo?.key){
        //当前选择的项和上次选择的项重复,则变为取消操作
        if(!mCancel){
            popWindow.dismiss()
            mSelectListener?.onSelected(selectedInfo)
            return@setOnItemClickListener
        }
        currentSelectedInfo.isSelected = false
        selectResult.text = ""
        selectedInfo = null
    } else{
        //选择当前项
        currentSelectedInfo.isSelected = true
        if(selectedInfo!=null){
            selectedInfo?.isSelected = false
        }
        selectedInfo = currentSelectedInfo
        selectResult.text = currentSelectedInfo.title

        for (item in mAdapter?.list!!){
            (item as SelectInfo).isSelected = item.key == selectedInfo?.key
        }
    }

所有的配置项都在activity中实现,额外增加了activity的代码量,所以很早就有大神就封装了关于PopupWindow实现的开源库----BasePopupWindow

首先看一下BasePopupWindow的构造函数,可以看到构造器可以传入的参数是很全面的,注释也解释的很清楚,传入什么参数就和该参数的window层级一致,无法显示在其他Window上方。

public BasePopupWindow(Context context) {
    this(context, 0, 0);
}

public BasePopupWindow(Context context, int width, int height) {
    this(context, width, height, 0);
}

public BasePopupWindow(Fragment fragment) {
    this(fragment, 0, 0);
}

public BasePopupWindow(Fragment fragment, int width, int height) {
    this(fragment, width, height, 0);
}

public BasePopupWindow(Dialog dialog) {
    this(dialog, 0, 0);
}

public BasePopupWindow(Dialog dialog, int width, int height) {
    this(dialog, width, height, 0);
}

示例:

在fragment创建点击事件弹出一个dialog,在dialog内部创建点击事件弹出PopupWindow,传入的参数为Context,运行后的效果是弹出的PopupWindow在dialog后面一层级,而且dialog销毁了该PopupWindow还没有销毁

image.png

修改传入的参数为Dialog,运行后的效果是弹出的PopupWindow和dialog同层级

image.png

然后我们看下BasePopupWindow初始化做的工作,可以看到根据传入的参数不同在BasePopupHelper里面返回其activity,如果传入的参数为空就返回其顶层的activity,根据返回的activity绑定其生命周期(防止内存泄露);初始化BasePopupHelper对象;初始化View

BasePopupWindow(Object ownerAnchorParent, int width, int height, int flag) {
    this.ownerAnchorParent = ownerAnchorParent;
    Activity act = BasePopupHelper.findActivity(ownerAnchorParent);
    //double check
    if (act == null) {
        throw new NullPointerException("无法从context处获得Activity,请确保您的Context是否为Activity");
    }

    if (act instanceof LifecycleOwner) {
        bindLifecycleOwner((LifecycleOwner) act);
    } else {
        listenForLifeCycle(act);
    }
    onCreateConstructor(ownerAnchorParent, width, height);
    mContext = act;
    mHelper = new BasePopupHelper(this);
    initView(width, height);
}
@Nullable
static Activity findActivity(Object parent) {
    return findActivity(parent, true);
}

@Nullable
static Activity findActivity(Object parent, boolean returnTopIfNull) {
    Activity act = null;
    if (parent instanceof Context) {
        act = PopupUtils.getActivity((Context) parent);
    } else if (parent instanceof Fragment) {
        act = ((Fragment) parent).getActivity();
    } else if (parent instanceof Dialog) {
        act = PopupUtils.getActivity(((Dialog) parent).getContext());
    }
    if (act == null && returnTopIfNull) {
        act = BasePopupSDK.getInstance().getTopActivity();
    }
    return act;
}

再进入到初始化view的方法中,可以看到调用了onCreateContentView用来获取ContentView,如果自定义了宽高设置popupWindow的宽高,这里初始化PopupWindowProxy,PopupWindowProxy传入的参数是BasePopupContextWrapper,我们都知道ContextWrapper是Context的装饰类,这里将context和BasePopupHelper对象传进去组成PopupWindow的装饰类(装饰器模式用来动态给对象增加额外的功能),BasePopupContextWrapper是为了构建PopupWindow的WindowManager代理类WindowManagerProxy,最后将初始化的BasePopupContextWrapper对象作为参数传入PopupWindow的代理类PopupWindowProxy,BasePopupHelper类用来处理BasePopupWindow的绘制和动画的逻辑, 最后当ContentView创建的时候回调onViewCreated方法。

void initView(int width, int height) {
    mContentView = onCreateContentView();
    mHelper.setContentRootId(mContentView);
    mDisplayAnimateView = onCreateAnimateView();
    if (mDisplayAnimateView == null) {
        mDisplayAnimateView = mContentView;
    }

    setWidth(width);
    setHeight(height);

    //默认占满全屏
    mPopupWindowProxy = new PopupWindowProxy(new PopupWindowProxy.BasePopupContextWrapper(getContext(), mHelper));
    mPopupWindowProxy.setContentView(mContentView);
    mPopupWindowProxy.setOnDismissListener(this);
    setPopupAnimationStyle(0);

    mHelper.preMeasurePopupView(mContentView, width, height);

    if (mContentView != null) {
        onViewCreated(mContentView);
    }
}
static class BasePopupContextWrapper extends ContextWrapper implements ClearMemoryObject {
    BasePopupHelper helper;
    WindowManagerProxy mWindowManagerProxy;

    public BasePopupContextWrapper(Context base, BasePopupHelper helper) {
        super(base);
        this.helper = helper;
    }

    @Override
    public Object getSystemService(String name) {
        if (TextUtils.equals(name, Context.WINDOW_SERVICE)) {
            if (mWindowManagerProxy != null) {
                return mWindowManagerProxy;
            }
            WindowManager windowManager = (WindowManager) super.getSystemService(name);
            mWindowManagerProxy = new WindowManagerProxy(windowManager, helper);
            return mWindowManagerProxy;
        }
        return super.getSystemService(name);
    }

    @Override
    public void clear(boolean destroy) {
        if (mWindowManagerProxy != null) {
            mWindowManagerProxy.clear(destroy);
        }
        if (destroy) {
            helper = null;
            mWindowManagerProxy = null;
        }
    }
}

最后看下BasePopupWindow代理类PopupWindowProxy里面实现了哪些方法,设置弹出窗口获取焦点、点击外部消失、设置背景、设置输入法模式等

public PopupWindowProxy(BasePopupContextWrapper context) {
    super(context);
    mBasePopupContextWrapper = context;
    setFocusable(true);
    setOutsideTouchable(true);
    setBackgroundDrawable(new ColorDrawable());
    setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
}

关于BasePopupWindow的具体使用以及开放的Api参考: www.yuque.com/razerdp/bas…