封装一个通用的PopupWindow

作者: 躬行之 | 来源:发表于2018-10-17 19:24 被阅读15次

    上篇文章是关于建造者设计模式的,今天顺便封装一个通用的 PopupWindow 来实践一下, 同时也方便以后使用 PopupWindow,本文将从下面几个方面来介绍 PopupWindow 及其封装,具体如下:

    1. 概述
    2. 常用方法
    3. 基本使用
    4. 封装 PopupWindow
    5. 使用封装后的PopupWindow
    6. 显示效果

    概述

    PopupWindow 表示一个弹窗,类似于 AlertDialog,相较 AlertDialog 来说 PopupWindow 使用起来更灵活,可有任意指定要显示的位置,当然能够灵活的使用必然在某一层面有所牺牲,如 PopupWindow 相较 AlertDialog 没有默认的布局,每次都得专门创建弹窗的布局,这一点来说 AlertDialog 就比较方便了,所以在开发中没有最好的解决方案,要根据具体的需求选择最合适的解决方案。

    常用设置

    PopupWindow 的创建,具体如下:

     //构造方法
     public PopupWindow (Context context)  
     public PopupWindow(View contentView)  
     public PopupWindow(View contentView, int width, int height)  
     public PopupWindow(View contentView, int width, int height, boolean focusable) 
    

    PopupWindow 的常用属性设置,具体如下:

     //设置View(必须)
     window.setContentView(contentView);
     //设置宽(必须)
     window.setWidth(WindowManager.LayoutParams.MATCH_PARENT);
     //设置高(必须)
     window.setHeight(WindowManager.LayoutParams.WRAP_CONTENT);
     //设置背景
     window.setBackgroundDrawable(new ColorDrawable(Color.GRAY));
     //设置PopupWindow之外的触摸事件
     window.setOutsideTouchable(true);
     //设置PopupWindow消失的监听器
     window.setOnDismissListener(this);
     //设置PopupWindow上的触摸事件
     window.setTouchable(true);
     //设置PopupWindow弹出动画
     window.setAnimationStyle(R.style.PopupWindowTranslateTheme);
    

    PopupWindow 的显示有两种设置方式,一种是基于坐标,另一种是基于某个 View ,具体如下:

    //基于坐标,参数(当前窗口的某个 View,位置,起始坐标x, 起始坐标y)
    void showAtLocation (View parent, int gravity, int x, int y)
    //基于某个View,参数(附着的View,x 方向的偏移量,y 方向的偏移量)
    void showAsDropDown (View anchor, int xoff, int yoff, int gravity) 
    void showAsDropDown (View anchor, int xoff, int yoff)
    void showAsDropDown (View anchor) 
    
    

    基本使用

    PopupWindow 的主要内容基本如上,下面使用原生的 PopupWindow 实现一个弹窗,下面是关键代码,具体如下:

    //创建PopupWindow
    PopupWindow window = new PopupWindow(this);
    //设置显示View
    window.setContentView(contentView);
    //设置宽高
    window.setWidth(WindowManager.LayoutParams.MATCH_PARENT);
    window.setHeight(WindowManager.LayoutParams.WRAP_CONTENT);
    //设置背景
    window.setBackgroundDrawable(new ColorDrawable(Color.GRAY));
    //设置PopupWindow之外的触摸事件
    window.setOutsideTouchable(true);
    //设置PopupWindow消失的监听器
    window.setOnDismissListener(new PopupWindow.OnDismissListener() {
        @Override
        public void onDismiss() {
            //监听PopupWindow的消失
        }
    });
    //设置PopupWindow上的触摸事件
    window.setTouchable(true);
    //设置PopupWindow弹出动画
    window.setAnimationStyle(R.style.PopupWindowTranslateTheme);
    window.showAtLocation(btnTarget, Gravity.BOTTOM | Gravity.CENTER, 0, 0);
    

    下面是显示效果,具体如下:

    PopupWindow.gif

    封装 PopupWindow

    这里对 PopupWindow 的封装主要是对 PopupWindow 常用摆放位置做进一步封装,使 PopupWindow 的调用更加灵活、简洁。

    在封装过程中遇到的问题是不能正确获取到 PopupWindow 的宽高,正确获取宽高的方法是先对 PopupWindow 进行测量,然后再获取其宽高,具体如下:

    //获取PopupWindow的宽高
    mPopupWindow.getContentView().measure(
        View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
        View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
    int popupWidth = mPopupWindow.getContentView().getMeasuredWidth();
    int popupHeight = mPopupWindow.getContentView().getMeasuredHeight();
    

    对 PopupWindow 的封装使用了建造者设计模式,下面看一下 PopupWindow 的默认配置,具体如下:

    public Builder(Context context) {
        this.context = context;
        this.popupWindow = new PopupWindow(context);
        //默认PopupWindow响应触摸事件
        this.outsideTouchable = true;
        //默认响应触摸事件
        this.touchable = true;
        //默认背景透明
        this.backgroundDrawable = new ColorDrawable(Color.TRANSPARENT);
        //默认宽高为WRAP_CONTENT
        this.width  = WindowManager.LayoutParams.WRAP_CONTENT;
        this.height = WindowManager.LayoutParams.WRAP_CONTENT;
        //默认Gravity为Gravity.CENTER
        this.gravity = Gravity.CENTER;
        this.layoutId = -1;
        //默认偏移量为0
        this.offsetX = 0;
        this.offsetY = 0;
        //...
    }
    

    由于宽高、背景、是否可点击等相关属性已经设置了默认值,使用时根据自己的需求设置相关属性,如 PopupWindow 的动画等,所以这些设置肯定是非必须的,那么那些事创建时必须的呢。

    下面是对 PopupWindow 封装类 MPopupWindow 的初始化,具体如下:

    private void setPopupWindowConfig(MPopupWindow window) {
            if (contentView != null && layoutId != -1){
                throw new MException("setContentView and setLayoutId can't be used together.", "0");
            }else if (contentView == null && layoutId == -1){
                throw new MException("contentView or layoutId can't be null.", "1");
            }
    
            if (context == null) {
                throw new MException("context can't be null.", "2");
            } else {
                window.mContext = this.context;
            }
    
            window.mWidth  = this.width;
            window.mHeight = this.height;
            window.mView = this.contentView;
            window.mLayoutId = layoutId;
            window.mPopupWindow = this.popupWindow;
            window.mOutsideTouchable   = this.outsideTouchable;
            window.mBackgroundDrawable = this.backgroundDrawable;
            window.mOnDismissListener  = this.onDismissListener;
            window.mAnimationStyle = this.animationStyle;
            window.mTouchable = this.touchable;
            window.mOffsetX = this.offsetX;
            window.mOffsetY = this.offsetY;
            window.mGravity = this.gravity;
        }
    }
    

    显然,这里可以看出 context 和 contentView 或 layoutId 是必须需要设置的,如果没有设置相应的会有错误提示,当然在封装中也对 contentView 和 layoutId 不能同时使用做了限制和如果使用了两者的错误提示。

    下面是对外提供的显示 PopupWindow 的方法,根据不同的枚举类型将 PopupWindow 显示在不同的位置,具体如下:

    public void showPopupWindow(View v, LocationType type) {
        if (mView!=null){
            mPopupWindow.setContentView(mView);
        }else if (mLayoutId != -1){
            View contentView = LayoutInflater.from(mContext).inflate(mLayoutId, null);
            mPopupWindow.setContentView(contentView);
        }
        mPopupWindow.setWidth(mWidth);
        mPopupWindow.setHeight(mHeight);
        mPopupWindow.setBackgroundDrawable(mBackgroundDrawable);
        mPopupWindow.setOutsideTouchable(mOutsideTouchable);
        mPopupWindow.setOnDismissListener(mOnDismissListener);
        mPopupWindow.setAnimationStyle(mAnimationStyle);
        mPopupWindow.setTouchable(mTouchable);
        //获取目标View的坐标
        int[] locations = new int[2];
        v.getLocationOnScreen(locations);
        int left = locations[0];
        int top  =  locations[1];
        //获取PopupWindow的宽高
        mPopupWindow.getContentView().measure(
                View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
                View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
        int popupWidth = mPopupWindow.getContentView().getMeasuredWidth();
        int popupHeight = mPopupWindow.getContentView().getMeasuredHeight();
    
        switch (type) {
            case TOP_LEFT:
                mPopupWindow.showAtLocation(v,Gravity.NO_GRAVITY,left - popupWidth + mOffsetX,top - popupHeight + mOffsetY);
                break;
            case TOP_CENTER:
                int offsetX = (v.getWidth() - popupWidth) / 2;
                mPopupWindow.showAtLocation(v,Gravity.NO_GRAVITY,left + offsetX + mOffsetX,top - popupHeight + mOffsetY);
                break;
            case TOP_RIGHT:
                mPopupWindow.showAtLocation(v,Gravity.NO_GRAVITY,left + v.getWidth() + mOffsetX,top - popupHeight + mOffsetY);
                break;
    
            case BOTTOM_LEFT:
                mPopupWindow.showAsDropDown(v, -popupWidth + mOffsetX,mOffsetY);
                break;
            case BOTTOM_CENTER:
                int offsetX1 = (v.getWidth() - popupWidth) / 2;
                mPopupWindow.showAsDropDown(v,offsetX1 + mOffsetX,mOffsetY);
                break;
            case BOTTOM_RIGHT:
                mPopupWindow.showAsDropDown(v, v.getWidth() + mOffsetX,mOffsetY);
                break;
    
            case LEFT_TOP:
                mPopupWindow.showAtLocation(v, Gravity.NO_GRAVITY, left - popupWidth + mOffsetX, top - popupHeight + mOffsetY);
                break;
            case LEFT_BOTTOM:
                mPopupWindow.showAtLocation(v, Gravity.NO_GRAVITY, left - popupWidth + mOffsetX, top + v.getHeight() + mOffsetY);
                break;
            case LEFT_CENTER:
                int offsetY = (v.getHeight() - popupHeight) / 2;
                mPopupWindow.showAtLocation(v, Gravity.NO_GRAVITY,left - popupWidth + mOffsetX,top + offsetY + mOffsetY);
                break;
    
            case RIGHT_TOP:
                mPopupWindow.showAtLocation(v, Gravity.NO_GRAVITY, left + v.getWidth() + mOffsetX,top - popupHeight + mOffsetY);
                break;
            case RIGHT_BOTTOM:
                mPopupWindow.showAtLocation(v, Gravity.NO_GRAVITY, left + v.getWidth() + mOffsetX,top + v.getHeight() + mOffsetY);
                break;
            case RIGHT_CENTER:
                int offsetY1 = (v.getHeight() - popupHeight) / 2;
                mPopupWindow.showAtLocation(v, Gravity.NO_GRAVITY,left + v.getWidth() + mOffsetX,top + offsetY1 + mOffsetY);
                break;
            case FROM_BOTTOM:
                mPopupWindow.showAtLocation(v,mGravity,mOffsetX,mOffsetY);
                break;
        }
    }
    

    使用封装后的PopupWindow

    下面是使用封装后的 PopupWindow,只需四行代码就可以显示一个默认的 PopupWindow 了,具体如下:

    private void showPopupWindow(MPopupWindow.LocationType type) {
        MPopupWindow popupWindow = new MPopupWindow
                .Builder(this)
                .setLayoutId(R.layout.popup_window_layout)
                .build();
        popupWindow.showPopupWindow(btnTarget, type);
    }
    

    由于默认 PopupWindow 背景是透明的,建议测试时设置背景。

    显示效果:

    下面是 PopupWindow 在各个位置的显示,具体如下:

    MPopupWindow.gif

    可以选择关注微信公众号:jzman-blog 获取最新更新,一起交流学习,公众号回复 MPopupWindow 获取源码链接。

    零点小筑

    相关文章

      网友评论

      • 码锻:版本问题呢?
        躬行之:@断头人 印象中没有,找时间看一下:blush:
        码锻:@零点小筑 7.0和7.0以下的好像,show的位置不会在你设置的问题,并且方法也不一样
        躬行之:具体什么版本问题,我测试一下。:smile:

      本文标题:封装一个通用的PopupWindow

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