banner
jzman

jzman

Coding、思考、自觉。
github

Encapsulate a generic PopupWindow

The previous article was about the Builder design pattern. Today, I will encapsulate a generic PopupWindow to practice and also facilitate future use of PopupWindow. This article will introduce PopupWindow and its encapsulation from the following aspects:

  1. Overview
  2. Common Methods
  3. Basic Usage
  4. Encapsulating PopupWindow
  5. Using the Encapsulated PopupWindow
  6. Display Effects

Overview#

PopupWindow represents a pop-up window, similar to AlertDialog. Compared to AlertDialog, PopupWindow is more flexible to use, allowing any specified display position. However, this flexibility comes at a cost; for example, PopupWindow does not have a default layout like AlertDialog, so a layout for the pop-up must be created each time. In this regard, AlertDialog is more convenient. Therefore, there is no best solution in development; the most suitable solution should be chosen based on specific needs.

Common Settings#

The creation of PopupWindow is as follows:

 // Constructor methods
 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) 

Common property settings for PopupWindow are as follows:

 // Set View (required)
 window.setContentView(contentView);
 // Set width (required)
 window.setWidth(WindowManager.LayoutParams.MATCH_PARENT);
 // Set height (required)
 window.setHeight(WindowManager.LayoutParams.WRAP_CONTENT);
 // Set background
 window.setBackgroundDrawable(new ColorDrawable(Color.GRAY));
 // Set touch events outside PopupWindow
 window.setOutsideTouchable(true);
 // Set listener for PopupWindow dismissal
 window.setOnDismissListener(this);
 // Set touch events on PopupWindow
 window.setTouchable(true);
 // Set PopupWindow animation
 window.setAnimationStyle(R.style.PopupWindowTranslateTheme);

There are two ways to display PopupWindow: one is based on coordinates, and the other is based on a specific View, as follows:

// Based on coordinates, parameters (a View of the current window, position, starting coordinate x, starting coordinate y)
void showAtLocation (View parent, int gravity, int x, int y)
// Based on a specific View, parameters (attached View, x direction offset, y direction offset)
void showAsDropDown (View anchor, int xoff, int yoff, int gravity) 
void showAsDropDown (View anchor, int xoff, int yoff)
void showAsDropDown (View anchor) 

Basic Usage#

The main content of PopupWindow is basically as above. Below is the implementation of a pop-up window using the native PopupWindow, with the key code as follows:

// Create PopupWindow
PopupWindow window = new PopupWindow(this);
// Set display View
window.setContentView(contentView);
// Set width and height
window.setWidth(WindowManager.LayoutParams.MATCH_PARENT);
window.setHeight(WindowManager.LayoutParams.WRAP_CONTENT);
// Set background
window.setBackgroundDrawable(new ColorDrawable(Color.GRAY));
// Set touch events outside PopupWindow
window.setOutsideTouchable(true);
// Set listener for PopupWindow dismissal
window.setOnDismissListener(new PopupWindow.OnDismissListener() {
    @Override
    public void onDismiss() {
        // Listen for PopupWindow dismissal
    }
});
// Set touch events on PopupWindow
window.setTouchable(true);
// Set PopupWindow animation
window.setAnimationStyle(R.style.PopupWindowTranslateTheme);
window.showAtLocation(btnTarget, Gravity.BOTTOM | Gravity.CENTER, 0, 0);

Below is the display effect:

image

Encapsulating PopupWindow#

The encapsulation of PopupWindow mainly focuses on further encapsulating common placement positions of PopupWindow, making its invocation more flexible and concise.

The problem encountered during encapsulation is that the width and height of PopupWindow cannot be correctly obtained. The correct method to obtain the width and height is to measure the PopupWindow first and then retrieve its dimensions, as follows:

// Get PopupWindow's width and height
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();

The encapsulation of PopupWindow uses the Builder design pattern. Below is the default configuration of PopupWindow:

public Builder(Context context) {
    this.context = context;
    this.popupWindow = new PopupWindow(context);
    // Default PopupWindow responds to touch events
    this.outsideTouchable = true;
    // Default responds to touch events
    this.touchable = true;
    // Default background is transparent
    this.backgroundDrawable = new ColorDrawable(Color.TRANSPARENT);
    // Default width and height are WRAP_CONTENT
    this.width  = WindowManager.LayoutParams.WRAP_CONTENT;
    this.height = WindowManager.LayoutParams.WRAP_CONTENT;
    // Default Gravity is Gravity.CENTER
    this.gravity = Gravity.CENTER;
    this.layoutId = -1;
    // Default offset is 0
    this.offsetX = 0;
    this.offsetY = 0;
    //...
}

Since the width, height, background, and clickability-related properties have default values, they can be set according to specific needs, such as PopupWindow animations, so these settings are not mandatory. However, what is essential during creation?

Below is the initialization of the PopupWindow encapsulation class 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;
    }
}

Clearly, context and either contentView or layoutId must be set. If the corresponding values are not set, an error message will be displayed. Additionally, restrictions have been placed on not using both contentView and layoutId simultaneously, along with error prompts for using both.

Below is the method provided for displaying PopupWindow, which shows PopupWindow in different positions based on different enumeration types:

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);
    // Get the coordinates of the target View
    int[] locations = new int[2];
    v.getLocationOnScreen(locations);
    int left = locations[0];
    int top  =  locations[1];
    // Get PopupWindow's width and height
    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;
    }
}

Using the Encapsulated PopupWindow#

Below is the usage of the encapsulated PopupWindow, which requires only four lines of code to display a default PopupWindow:

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

Since the default PopupWindow background is transparent, it is recommended to set a background during testing.

Display Effects:#

Below are the display effects of PopupWindow in various positions:

image

Here is the GitLab address MPopupWindow.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.