Android 封裝一個通用的PopupWindow

_小馬快跑_發表於2017-12-15

*本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出

先上效果圖:

GIF.gif

完整程式碼地址已上傳Github:CommonPopupWindow

PopupWindow這個類用來實現一個彈出框,可以使用任意佈局的View作為其內容,這個彈出框是懸浮在當前activity之上的。

一般PopupWindow的使用:

//準備PopupWindow的佈局View
View popupView = LayoutInflater.from(this).inflate(R.layout.popup, null);
//初始化一個PopupWindow,width和height都是WRAP_CONTENT
PopupWindow popupWindow = new PopupWindow(
   ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
//設定PopupWindow的檢視內容
popupWindow.setContentView(popupView);
//點選空白區域PopupWindow消失,這裡必須先設定setBackgroundDrawable,否則點選無反應
popupWindow.setBackgroundDrawable(new ColorDrawable(0x00000000));
popupWindow.setOutsideTouchable(true);
//設定PopupWindow動畫
popupWindow.setAnimationStyle(R.style.AnimDown);
//設定是否允許PopupWindow的範圍超過螢幕範圍
popupWindow.setClippingEnabled(true);
//設定PopupWindow消失監聽
popupWindow.setOnDismissListener(new PopupWindow.OnDismissListener() {
    @Override
    public void onDismiss() {

    }
 });
//PopupWindow在targetView下方彈出
popupWindow.showAsDropDown(targetView);
複製程式碼

上面就是PopupWindow通常需要設定的各個方法,不難,但是稍微有點繁瑣,有些是可以複用的,所以封裝了一個通用的CommonPopupWindow:

CommonPopupWindow繼承自PopupWindow,擁有PopupWindow的各個屬性方法,使用類似建造者模式,和AlertDialog的使用方式差不多,CommonPopupWindow使用舉例:

CommonPopupWindow popupWindow = new CommonPopupWindow.Builder(this)
         //設定PopupWindow佈局
        .setView(R.layout.popup_down) 
         //設定寬高
        .setWidthAndHeight(ViewGroup.LayoutParams.MATCH_PARENT, 
                           ViewGroup.LayoutParams.WRAP_CONTENT)
         //設定動畫
        .setAnimationStyle(R.style.AnimDown)
         //設定背景顏色,取值範圍0.0f-1.0f 值越小越暗 1.0f為透明
        .setBackGroundLevel(0.5f)
         //設定PopupWindow裡的子View及點選事件 
        .setViewOnclickListener(new CommonPopupWindow.ViewInterface() {
            @Override
            public void getChildView(View view, int layoutResId) {
                TextView tv_child = (TextView) view.findViewById(R.id.tv_child);
                tv_child.setText("我是子View");
            }
        })
         //設定外部是否可點選 預設是true
        .setOutsideTouchable(true)
         //開始構建
        .create();
//彈出PopupWindow
popupWindow.showAsDropDown(view);
複製程式碼
  • CommonPopupWindow 設定背景:

這裡使用的是WindowManager.LayoutParams.alpha屬性,看下官網解釋:**An alpha value to apply to this entire window. An alpha of 1.0 means fully opaque and 0.0 means fully transparent .**alpha 值適用於整個Window,α為1.0時表示完全不透明而0.0表示完全透明,預設是1.0,當PopupWindow彈出時通過設定alpha在(0.0,1.0)之間設定灰色背景,當PopupWindow消失時恢復預設值。

private void setBackGroundLevel(float level) {
    mWindow = ((Activity) context).getWindow();
    WindowManager.LayoutParams params = mWindow.getAttributes();
    params.alpha = level;
    mWindow.setAttributes(params);
    }
複製程式碼
  • 計算CommonPopupWindow 寬高:
//設定測量模式為UNSPECIFIED可以確保測量不受父View的影響
int w = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
int h = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
view.measure(w, h);
//得到測量寬度
int mWidth=view.getMeasuredWidth();
//得到測量高度
int mHeight=view.getMeasuredHeight();
複製程式碼

注:在測量寬高時遇到一種情況,如圖所示:

uncertain.png

如果設定TextView 的 android:layout_width="wrap_content",那麼測量不出TextView 準確的height,當設定width為某個確定值時,也能得到準確的height了。

  • CommonPopupWindow 設定動畫:

如設定向右動畫:

.setAnimationStyle(R.style.AnimHorizontal);
複製程式碼

在style.xml檔案中設定:

<style name="AnimHorizontal" parent="@android:style/Animation">
     <item name="android:windowEnterAnimation">@anim/push_scale_left_in</item>
     <item name="android:windowExitAnimation">@anim/push_scale_left_out</item>
 </style>
複製程式碼

android:windowEnterAnimation、android:windowExitAnimation分別為Popupwindow彈出和消失動畫

進入動畫為anim目錄下的 push_scale_left_in.xml

<scale xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="200"
    android:fromXScale="0.0"
    android:fromYScale="1.0"
    android:interpolator="@android:anim/accelerate_decelerate_interpolator"
    android:toXScale="1.0"
    android:toYScale="1.0" />
複製程式碼

消失動畫為 push_scale_left_out.xml

<scale xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="200"
    android:fromXScale="1.0"
    android:fromYScale="1.0"
    android:interpolator="@android:anim/accelerate_decelerate_interpolator"
    android:toXScale="0.0"
    android:toYScale="1.0" />
複製程式碼
  • CommonPopupWindow 彈出:

因為CommonPopupWindow繼承自PopupWindow,所以可以直接使用PopupWindow中的彈出方法,常用的下面三種:

public void showAsDropDown(View anchor)

public void showAsDropDown(View anchor, int xoff, int yoff)

public void showAtLocation(View parent, int gravity, int x, int y)
複製程式碼

其中,showAsDropDown是顯示在參照物anchor的周圍,xoff、yoff分別是X軸、Y軸的偏移量,如果不設定xoff、yoff,預設是顯示在anchor的下方;showAtLocation是設定在父控制元件的位置,如設定Gravity.BOTTOM表示在父控制元件底部彈出,xoff、yoff也是X軸、Y軸的偏移量。

如上面向右彈出例子,分別使用showAsDropDown和showAtLocation來實現:

right.png

showAsDropDown:

popupWindow.showAsDropDown(view, view.getWidth(), -view.getHeight());
複製程式碼

showAsDropDown預設展示在button的下面,通過改變X軸和Y軸的偏移量(X軸向右偏移button的寬度,Y軸向上偏移button的高度),實現在Button右邊彈出。

showAtLocation:

int[] positions = new int[2];
view.getLocationOnScreen(positions);
popupWindow.showAtLocation(findViewById(android.R.id.content), 
         Gravity.START| Gravity.TOP , positions[0] + view.getWidth(), positions[1]);
複製程式碼

使用了View的getLocationOnScreen方法來獲得View在螢幕中的座標位置,傳入的引數必須是一個有2個整數的陣列,分別代表View的X、Y座標,即是View的左上角的座標,這裡的View是Button,知道了Button左上角的座標,就可以得到要展示的PopupWindow的左上角的座標為(positions[0] + view.getWidth(), positions[1]),從而實現在Button右邊彈出。

最後貼一下程式碼 CommonPopupWindow.java:

public class CommonPopupWindow extends PopupWindow {
 final PopupController controller;

 @Override
 public int getWidth() {
     return controller.mPopupView.getMeasuredWidth();
 }

 @Override
 public int getHeight() {
     return controller.mPopupView.getMeasuredHeight();
 }

 public interface ViewInterface {
     void getChildView(View view, int layoutResId);
 }

 private CommonPopupWindow(Context context) {
     controller = new PopupController(context, this);
 }

 @Override
 public void dismiss() {
     super.dismiss();
     controller.setBackGroundLevel(1.0f);
 }

 public static class Builder {
     private final PopupController.PopupParams params;
     private ViewInterface listener;

     public Builder(Context context) {
         params = new PopupController.PopupParams(context);
     }

     /**
      * @param layoutResId 設定PopupWindow 佈局ID
      * @return Builder
      */
     public Builder setView(int layoutResId) {
         params.mView = null;
         params.layoutResId = layoutResId;
         return this;
     }

     /**
      * @param view 設定PopupWindow佈局
      * @return Builder
      */
     public Builder setView(View view) {
         params.mView = view;
         params.layoutResId = 0;
         return this;
     }

     /**
      * 設定子View
      *
      * @param listener ViewInterface
      * @return Builder
      */
     public Builder setViewOnclickListener(ViewInterface listener) {
         this.listener = listener;
         return this;
     }

     /**
      * 設定寬度和高度 如果不設定 預設是wrap_content
      *
      * @param width 寬
      * @return Builder
      */
     public Builder setWidthAndHeight(int width, int height) {
         params.mWidth = width;
         params.mHeight = height;
         return this;
     }

     /**
      * 設定背景灰色程度
      *
      * @param level 0.0f-1.0f
      * @return Builder
      */
     public Builder setBackGroundLevel(float level) {
         params.isShowBg = true;
         params.bg_level = level;
         return this;
     }

     /**
      * 是否可點選Outside消失
      *
      * @param touchable 是否可點選
      * @return Builder
      */
     public Builder setOutsideTouchable(boolean touchable) {
         params.isTouchable = touchable;
         return this;
     }

     /**
      * 設定動畫
      *
      * @return Builder
      */
     public Builder setAnimationStyle(int animationStyle) {
         params.isShowAnim = true;
         params.animationStyle = animationStyle;
         return this;
     }

     public CommonPopupWindow create() {
         final CommonPopupWindow popupWindow = new CommonPopupWindow(params.mContext);
         params.apply(popupWindow.controller);
         if (listener != null && params.layoutResId != 0) {
            listener.getChildView(popupWindow.controller.mPopupView, params.layoutResId);
         }
         CommonUtil.measureWidthAndHeight(popupWindow.controller.mPopupView);
         return popupWindow;
        }
    }
}
複製程式碼

PopupController.java:

class PopupController {
    private int layoutResId;//佈局id
    private Context context;
    private PopupWindow popupWindow;
    View mPopupView;//彈窗佈局View
    private View mView;
    private Window mWindow;

    PopupController(Context context, PopupWindow popupWindow) {
        this.context = context;
        this.popupWindow = popupWindow;
    }

    public void setView(int layoutResId) {
        mView = null;
        this.layoutResId = layoutResId;
        installContent();
    }

    public void setView(View view) {
        mView = view;
        this.layoutResId = 0;
        installContent();
    }

    private void installContent() {
        if (layoutResId != 0) {
            mPopupView = LayoutInflater.from(context).inflate(layoutResId, null);
        } else if (mView != null) {
            mPopupView = mView;
        }
        popupWindow.setContentView(mPopupView);
    }

    /**
     * 設定寬度
     *
     * @param width  寬
     * @param height 高
     */
    private void setWidthAndHeight(int width, int height) {
        if (width == 0 || height == 0) {
            //如果沒設定寬高,預設是WRAP_CONTENT
            popupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
            popupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
        } else {
            popupWindow.setWidth(width);
            popupWindow.setHeight(height);
        }
    }


    /**
     * 設定背景灰色程度
     *
     * @param level 0.0f-1.0f
     */
    void setBackGroundLevel(float level) {
        mWindow = ((Activity) context).getWindow();
        WindowManager.LayoutParams params = mWindow.getAttributes();
        params.alpha = level;
        mWindow.setAttributes(params);
    }


    /**
     * 設定動畫
     */
    private void setAnimationStyle(int animationStyle) {
        popupWindow.setAnimationStyle(animationStyle);
    }

    /**
     * 設定Outside是否可點選
     *
     * @param touchable 是否可點選
     */
    private void setOutsideTouchable(boolean touchable) {
        popupWindow.setBackgroundDrawable(new ColorDrawable(0x00000000));//設定透明背景
        popupWindow.setOutsideTouchable(touchable);//設定outside可點選
        popupWindow.setFocusable(touchable);
    }


    static class PopupParams {
        public int layoutResId;//佈局id
        public Context mContext;
        public int mWidth, mHeight;//彈窗的寬和高
        public boolean isShowBg, isShowAnim;
        public float bg_level;//螢幕背景灰色程度
        public int animationStyle;//動畫Id
        public View mView;
        public boolean isTouchable = true;

        public PopupParams(Context mContext) {
            this.mContext = mContext;
        }

        public void apply(PopupController controller) {
            if (mView != null) {
                controller.setView(mView);
            } else if (layoutResId != 0) {
                controller.setView(layoutResId);
            } else {
                throw new IllegalArgumentException("PopupView's contentView is null");
            }
            controller.setWidthAndHeight(mWidth, mHeight);
            controller.setOutsideTouchable(isTouchable);//設定outside可點選
            if (isShowBg) {
                //設定背景
                controller.setBackGroundLevel(bg_level);
            }
            if (isShowAnim) {
                controller.setAnimationStyle(animationStyle);
            }
        }
    }
}
複製程式碼

相關文章