banner
jzman

jzman

Coding、思考、自觉。
github

汎用のPopupWindowを封装する

上篇文章はビルダー設計パターンに関するもので、今日は一般的な PopupWindow を封装して実践し、今後 PopupWindow を使用する際に便利にします。本記事では、PopupWindow とその封装について以下のいくつかの側面から紹介します。具体的には次の通りです:

  1. 概要
  2. よく使うメソッド
  3. 基本的な使用法
  4. PopupWindow の封装
  5. 封装後の PopupWindow の使用
  6. 表示効果

概要#

PopupWindow はアラートダイアログに似たポップアップウィンドウを表し、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 の表示には 2 つの設定方法があります。一つは座標に基づくもので、もう一つは特定の 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);

以下は表示効果の具体例です:

image

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.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 を表示するのにわずか 4 行のコードが必要です。具体的には以下の通りです:

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 が各位置に表示された例です。具体的には以下の通りです:

image

GitLab アドレス MPopupWindow を添付します。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。