Android技能樹 — PopupWindow小結(jié)

前言:

關(guān)于下拉選擇框,估計(jì)大家都有很多選擇,我在以前的文章:項(xiàng)目需求討論-HyBrid模式需求改造
上寫過(guò)下拉框選擇這一塊,正好用的Spinner。

這次正好又有一個(gè)下拉框的需求,所以這次我使用了PopupWindow來(lái)實(shí)現(xiàn)的。然后想到其實(shí)PopupWindow很多地方都會(huì)用到,但是一直沒(méi)有好好的總結(jié)過(guò),所以就想到了寫本文,而且本文也十分的基礎(chǔ)和簡(jiǎn)單,大家也很好理解。

主要分為三部分:

  1. PopupWindow的使用
  2. PopupWindow工具類的封裝
  3. PopupWindow源碼分析

正文

我們知道上來(lái)直接給一大串的源碼,很少有人會(huì)繼續(xù)看下去,所以我們就自己先寫個(gè)下拉選擇框demo來(lái)進(jìn)行演示。

所以我們可以先來(lái)看下我們需要的下拉框樣式:(為了隨便舉個(gè)例子,所以設(shè)計(jì)的比較丑):

我們可以一步步來(lái)看如何實(shí)現(xiàn):

1.基礎(chǔ)使用教程

既然要跳出下面的彈框,而且本文說(shuō)過(guò)要使用PopupWindow,所以就是實(shí)現(xiàn)一個(gè)PopupWindow即可,十分簡(jiǎn)單。

1.1 實(shí)例化PopupWindow對(duì)象

既然實(shí)例化PopupWindow對(duì)象,所以我們看下它的構(gòu)造函數(shù):

public PopupWindow() {
    this(null, 0, 0);
}

public PopupWindow(View contentView) {
    this(contentView, 0, 0);
}

public PopupWindow(int width, int height) {
    this(null, width, height);
}

public PopupWindow(View contentView, int width, int height) {
    this(contentView, width, height, false);
}



/**
    @param contentView the popup content
    @param width the popup's width
    @param height the popup's height
    @param focusable true if the popup can be focused, false otherwise
*/

public PopupWindow(View contentView, int width, int height, boolean focusable) {
    if (contentView != null) {
        mContext = contentView.getContext();
        mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
    }

    setContentView(contentView);
    setWidth(width);
    setHeight(height);
    setFocusable(focusable);
}

我們可以看到不管你用的哪個(gè)構(gòu)造函數(shù),最終一定是調(diào)用了最后一個(gè)構(gòu)造函數(shù):PopupWindow(View contentView, int width, int height, boolean focusable)

也就是說(shuō)我們要告訴PopupWindow這些內(nèi)容:

  1. 顯示的contentView
  2. PopupWindow要顯示的寬和高,
  3. PopupWindow是否有獲取焦點(diǎn)的能力(默認(rèn)false)。

假設(shè)我們用的第四個(gè)構(gòu)造函數(shù)

View contentView = LayoutInflater.from(MainActivity.this).inflate(R.layout.popuplayout, null);
PopupWindow popupWindow = new PopupWindow(contentView,ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT,true);

1.2 PopupWindow相關(guān)設(shè)置方法

當(dāng)然我們也可以使用第一個(gè)構(gòu)造函數(shù)生成對(duì)象,然后通過(guò)相應(yīng)的SetXXXX方法,設(shè)置各種參數(shù)。

我們來(lái)看下一些常用的Set方法:

設(shè)置contentView, 寬和高,獲取焦點(diǎn)能力:

popupWindow.setContentView(contentView);
popupWindow.setHeight(height);
popupWindow.setWidth(width);
popupWindow.setFocusable(true);

點(diǎn)擊窗體外消失:

// 需要設(shè)置一下PopupWindow背景,點(diǎn)擊外邊消失才起作用
popupWindow.setBackgroundDrawable(new BitmapDrawable(getResources(),(Bitmap) null));
// 點(diǎn)擊窗外可取消
popupWindow.setTouchable(true);
popupWindow.setOutsideTouchable(true);

關(guān)于窗體會(huì)被軟件盤遮擋:

// 設(shè)置pop被鍵盤頂上去,而不是遮擋
popupWindow.setSoftInputMode(PopupWindow.INPUT_METHOD_NEEDED);
popupWindow.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);

popupwindow添加各種動(dòng)畫效果(平移,縮放,透明等):

popupWindow.setAnimationStyle(R.style.popwindow_anim_style);

動(dòng)畫的style:

<style name="AnimDown" parent="@android:style/Animation">
    <item name="android:windowEnterAnimation">@anim/push_scale_in</item>
    <item name="android:windowExitAnimation">@anim/push_scale_out</item>
</style>

具體的動(dòng)畫:

<!-- 顯示動(dòng)畫-->
<?xml version="1.0" encoding="utf-8"?><!-- 左上角擴(kuò)大-->
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:shareInterpolator="true">

    <scale xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="200"
        android:fromXScale="1.0"
        android:fromYScale="0.0"
        android:interpolator="@android:anim/accelerate_decelerate_interpolator"
        android:toXScale="1.0"
        android:toYScale="1.0" />
</set>
<!-- 隱藏動(dòng)畫-->
<?xml version="1.0" encoding="utf-8"?><!-- 左上角擴(kuò)大-->
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:shareInterpolator="true">

    <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="1.0"
        android:toYScale="0.001" />
</set>

1.3 PopupWindow顯示出來(lái)

主要是使用showXXXX方法來(lái)實(shí)現(xiàn),而這個(gè)方法也有好幾個(gè):

我們先來(lái)看showAsDropDownshowAtLocation的區(qū)別:
很多人估計(jì)用的更多的是showAsDropDown,它們的最大區(qū)別簡(jiǎn)單來(lái)說(shuō)是showAsDropDown是相對(duì)于某個(gè)控件,然后PopupWindow顯示在這個(gè)控件的下方;而showAtLocation是相對(duì)于屏幕,可以通過(guò)設(shè)置Gravity來(lái)指定PopupWindow顯示在屏幕的那個(gè)位置。

比如我們現(xiàn)在先看showAsDropDown:

//PopupWindow會(huì)顯示我們傳入的這個(gè)View的下方,平切是左邊對(duì)齊
//(也就是view控件的左下角與popupWindow的左上角對(duì)齊)
showAsDropDown(View)
//PopupWindow還是在這個(gè)View的下方,
//但是額外可以設(shè)置x,y的偏移值,x,y表示坐標(biāo)偏移量
showAsDropDown(View,int,int);

比如我們代碼寫為:showAsDropDown(View,50,50);X軸和Y軸都偏移了50。


//PopupWindow可以額外設(shè)定Gravity,默認(rèn)就是Gravity.Left。
//同時(shí)設(shè)置為Top和Bottom沒(méi)啥效果,因?yàn)槭窃谶@個(gè)View的下方。
showAsDropDown(View,int,int,int);

比如我們代碼寫為:popupWindow.showAsDropDown(v,0,0,Gravity.RIGHT);變成了View的右下角與PopupWindow的左上角對(duì)齊了。

我們?cè)賮?lái)看showAtLocation:
因?yàn)檫@個(gè)方法是PopupWindow的顯示相對(duì)于屏幕,所以傳入的View也是只要這個(gè)屏幕的就可以,因?yàn)檫@個(gè)View的傳入也只是為了拿到Window Token。

//這個(gè)方法最后還是等于調(diào)用了另外一個(gè)showAtLocation方法,
//傳入view只是為了拿到token
//x,y同樣是x和y軸的偏移值
public void showAtLocation(View parent, int gravity, int x, int y) {
    showAtLocation(parent.getWindowToken(), gravity, x, y);
}

public void showAtLocation(IBinder token, int gravity, int x, int y){
    .......
}

比如我們寫入的代碼是:popupWindow.showAtLocation(view, Gravity.RIGHT | Gravity.BOTTOM, 0, 0);

如果我們?cè)O(shè)置為:popupWindow.showAtLocation(view, Gravity.TOP, 0, 0);

我們發(fā)現(xiàn)PopupWindow并沒(méi)有在statusbar的上面。如果我們想要覆蓋statusbar呢,可以再加一句:popupWindow.setClippingEnabled(false);

所以基本使用估計(jì)大家都會(huì)了。我們來(lái)總結(jié)下代碼:


1.4 總結(jié)PopupWindow初級(jí)使用代碼


LayoutInflater mLayoutInflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
//自定義布局
ViewGroup view = (ViewGroup) mLayoutInflater.inflate(R.layout.window, null, true);
PopupWindow popupWindow = new PopupWindow(view, LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT, true);

//是否需要點(diǎn)擊PopupWindow外部其他界面時(shí)候消失
mPopWindow.setBackgroundDrawable(new BitmapDrawable());
mPopWindow.setOutsideTouchable(true);

//設(shè)置touchable和focusable
mPopWindow.setFocusable(true);
mPopWindow.setTouchable(true);

/**
然后比如在某個(gè)按鈕的點(diǎn)擊事件中顯示PopupWindow
切記不能直接在比如onCreate中直接調(diào)用顯示popupWindow,
會(huì)直接拋出異常,原因后面源碼解析會(huì)提到
*/
btn.setOnclickListener(v -> {
    if (popupWindow != null) {
        popupWindow.showAsDropDown(v);
    }
})

2.PopupWindow工具類封裝

我在以前寫過(guò)Dialog的封裝文章:

項(xiàng)目需求討論-Android 自定義Dialog實(shí)現(xiàn)步驟及封裝

我們這次來(lái)對(duì)PopupWindow來(lái)進(jìn)行封裝,我們還是像上面的文章那樣,使用Builder模式。

我們先來(lái)看我們要注意哪些因素要考慮:

  1. contentView ,這里有二種可能,一是用戶只是傳了R.layout.xxx進(jìn)來(lái),二是用戶傳了具體的View對(duì)象進(jìn)來(lái)。
  2. PopupWindow的寬和高。 (可能需要傳入Px值,可能是dp值,可能是R.dimen.xxx值,如果不傳入,就默認(rèn)為Wrap_Content,也就是會(huì)顯示你傳入的contentView的寬高)
  3. 是否需要顯示動(dòng)畫,如果需要顯示動(dòng)畫,那么具體的style參數(shù)
  4. focusable,touchable 的設(shè)置
  5. 是否設(shè)置點(diǎn)擊外部讓PopupWindow消失
  6. 設(shè)置里面的某個(gè)View的點(diǎn)擊事件

所以初步我們可以寫成這樣:

public class CustomPopupWindow extends PopupWindow {

    private CustomPopupWindow(Builder builder) {
        super(builder.context);

        builder.view.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
        setContentView(builder.view);
        setHeight(builder.height == 0?ViewGroup.LayoutParams.WRAP_CONTENT:builder.height);
        setWidth(builder.width == 0?ViewGroup.LayoutParams.WRAP_CONTENT:builder.width);
        if (builder.cancelTouchout) {
            setBackgroundDrawable(new ColorDrawable(0x00000000));//設(shè)置透明背景
            setOutsideTouchable(builder.cancelTouchout);//設(shè)置outside可點(diǎn)擊
        }
        setFocusable(builder.isFocusable);
        setTouchable(builder.isTouchable);

        if(builder.animStyle != 0){
            setAnimationStyle(builder.animStyle);
        }
    }

    public static final class Builder {

        private Context context;
        private int height, width;
        private boolean cancelTouchout;
        private boolean isFocusable = true;
        private boolean isTouchable = true;
        private View view;
        private int animStyle;

        public Builder(Context context) {
            this.context = context;
        }

        public Builder view(int resView) {
            view = LayoutInflater.from(context).inflate(resView, null);
            return this;
        }

        public Builder view(View resVew){
            view = resVew;
            return this;
        }

        public Builder heightpx(int val) {
            height = val;
            return this;
        }

        public Builder widthpx(int val) {
            width = val;
            return this;
        }

        public Builder heightdp(int val) {
            height = dip2px(context, val);
            return this;
        }

        public Builder widthdp(int val) {
            width = dip2px(context, val);
            return this;
        }

        public Builder heightDimenRes(int dimenRes) {
            height = context.getResources().getDimensionPixelOffset(dimenRes);
            return this;
        }

        public Builder widthDimenRes(int dimenRes) {
            width = context.getResources().getDimensionPixelOffset(dimenRes);
            return this;
        }

        public Builder cancelTouchout(boolean val) {
            cancelTouchout = val;
            return this;
        }

        public Builder isFocusable(boolean val) {
            isFocusable = val;
            return this;
        }

        public Builder isTouchable(boolean val) {
            isTouchable = val;
            return this;
        }

        public Builder animStyle(int val){
            animStyle = val;
            return this;
        }

        public Builder addViewOnclick(int viewRes, View.OnClickListener listener) {
            view.findViewById(viewRes).setOnClickListener(listener);
            return this;
        }


        public CustomPopupWindow build() {

            return new CustomPopupWindow(this);
        }
    }
    
    @Override
    public int getWidth() {
        return getContentView().getMeasuredWidth();
    }
    
    public static int dip2px(Context context, float dipValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dipValue * scale + 0.5f);
    }
}

所以只要知道我們要設(shè)定哪些屬性,就很容易封裝。

然后使用就可以:

customPopupWindow = new CustomPopupWindow.Builder(this)
                .cancelTouchout(true)
                .view(popupWindowView)
                .isFocusable(true)
                .animStyle(R.style.AnimDown)
                .build();

這里我要額外提上面封裝類代碼中的二個(gè)知識(shí)點(diǎn)

知識(shí)點(diǎn)1. 提前知道popupwindow的寬高。

我們可以看到在我們的工具類中,有一段代碼:

builder.view.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);,

就是把我們傳進(jìn)去的contentView提前繪制,這樣我們就可以調(diào)用popupwindow.getContentView().getMeasuredWidth()方法來(lái)獲取這個(gè)contentView的寬高了(ps:我們一般設(shè)置的popupwindow的寬高肯定跟我們傳進(jìn)去的contentview一致)。

可能有些人就會(huì)問(wèn)了,我們?yōu)樯缎枰崆爸纏opupwindow的寬高呢,比如下面這個(gè)需求:


比如上面的啟動(dòng)PopupWindow的按鈕,比下面的選項(xiàng)寬,我們肯定希望咱們的PopupWindow是顯示在正中間,所以我們?cè)谡{(diào)用:

showAsDropDown(View anchor, int xoff, int yoff);

時(shí)候傳入的X值的偏移量就要為上面的按鈕寬度減去下面PopupWindow的寬度后的一半。但是平常情況下,我們單純通過(guò)PopupWindow.getWidth()或者contentView.getWidth()方法,在第一次點(diǎn)擊出現(xiàn)的時(shí)候,獲取到的值前者為-2,后者為0,然后再次點(diǎn)擊的時(shí)候就是正確值了。因?yàn)榈谝淮吸c(diǎn)擊前,PopupWindow還沒(méi)出現(xiàn)在屏幕過(guò),所以也沒(méi)有被繪制出來(lái)過(guò),寬度當(dāng)然也獲取不到準(zhǔn)確值了。出現(xiàn)過(guò)一次后,第二次點(diǎn)擊就能正確獲取了。所以第一次PopupWindow就出現(xiàn)在錯(cuò)誤位置,后面就對(duì)了。

所以我們重新重載了PopupWindowgetWidth方法:

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

知識(shí)點(diǎn)2. Touchable和Focusable的設(shè)置

我們一般對(duì)上面的按鈕設(shè)置成這樣:

btn.setOnclickListener(v -> {
    if (popupWindow != null) {
        popupWindow.showAsDropDown(v);
    }
})

這樣點(diǎn)擊按鈕后就可以出現(xiàn)我們的PopupWindow,但是你再次點(diǎn)擊這個(gè)按鈕,PopupWindow會(huì)先消失,然后再次出現(xiàn),就像下面這樣:

但是我們希望的是點(diǎn)擊按鈕后,如果PopupWindow在的話就消失。

當(dāng)然你可以在點(diǎn)擊事件里面用:PopupWindow.isShowing();判斷,然后讓PopupWindow.dismiss();,但是別人用了我們的工具類,總不能還要告訴它要在觸發(fā)按鈕點(diǎn)擊事件里面要額外判斷吧,所以我們只需要在我們工具類中默認(rèn)設(shè)置PopupWindow的touchablefocusabletrue,這樣,我們的點(diǎn)擊事件啥都不用改,就可以點(diǎn)擊一下出現(xiàn),再點(diǎn)擊消失。


3. PopupWindow源碼簡(jiǎn)單分析

很慚愧,很早以前就會(huì)用PopupWindow,但是源碼一直沒(méi)有去看過(guò)。

在講解PopupWindow源碼前我們先來(lái)看下其他的知識(shí)。

我們應(yīng)該都做過(guò)或者看見過(guò)添加懸浮窗等功能,或者在某些文章看見過(guò)Window和WindowManager的介紹,比如在《Android藝術(shù)開發(fā)之旅》里面,也有相關(guān)的一章專門講這個(gè),大家可以看下:

Android開發(fā)藝術(shù)探索——第八章:理解Window和WindowManager

假設(shè)我們現(xiàn)在要在應(yīng)用程序的某處加個(gè)按鈕,應(yīng)該怎么樣呢:

Button btn = new Button(this);
btn.setText("我是窗口");
WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
WindowManager.LayoutParams layout = new WindowManager.LayoutParams(WindowManager.LayoutParams.WRAP_CONTENT
    , WindowManager.LayoutParams.WRAP_CONTENT, 0,0, 
    PixelFormat.TRANSLUCENT);
layout.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
        | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
        | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
layout.gravity = Gravity.CENTER;
layout.type = WindowManager.LayoutParams.TYPE_APPLICATION;
layout.x = 300;
layout.y = 100;
wm.addView(btn, layout);

只需要通過(guò)WindowManager的addView方法,把這個(gè)按鈕加進(jìn)來(lái)即可,我估計(jì)有百分之八九十的安卓開發(fā)都大概見過(guò)或者知道這種通過(guò)WindowManager添加的方式。

我們可以看出有這么幾步:

  1. 創(chuàng)建了要顯示的ContentView(此處為Button)
  2. 創(chuàng)建WindowMananger.LayoutParams對(duì)象
  3. 對(duì)LayoutParams對(duì)象設(shè)置相應(yīng)的屬性值,比如x,y
  4. WindowMananger對(duì)象調(diào)用addView(ContentView,LayoutParams);

PS:這里額外提下layout.type = WindowManager.LayoutParams.TYPE_APPLICATION;這個(gè)屬性,比如我們當(dāng)前只是在我們的app里面加一個(gè)按鈕,所以也不需要做其他額外處理;如果我們是想全局添加按鈕,也就是我們的app最小化到了后臺(tái),在手機(jī)桌面還是能看到有個(gè)按鈕懸浮(類似一些手機(jī)清理助手等懸浮小球),需要切換這里的type屬性,同時(shí)還要聲明相應(yīng)的權(quán)限,不然app就會(huì)報(bào)錯(cuò),說(shuō)permission denied for this window type。相應(yīng)的type介紹大家可以參考:WindowManager.LayoutParams的type屬性

沒(méi)錯(cuò),咱們的PopupWindow也是類似的。

我們從構(gòu)造函數(shù)開始看起來(lái):

public PopupWindow(View contentView, int width, int height, boolean focusable) {
    if (contentView != null) {
        mContext = contentView.getContext();
        mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
    }

    setContentView(contentView);
    setWidth(width);
    setHeight(height);
    setFocusable(focusable);
}

我們可以看到,果然獲取了WindowManager對(duì)象,然后給PopupWindow的內(nèi)部的contentView、width、height、focusable賦值。

我們看最后顯示的方法源碼:

public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
    if (isShowing() || mContentView == null) {
        return;
    }

    TransitionManager.endTransitions(mDecorView);

    attachToAnchor(anchor, xoff, yoff, gravity);

    mIsShowing = true;
    mIsDropdown = true;
    
    //'我們可以看到這里果然生成了相應(yīng)的WindowManager.LayoutParams'
    final WindowManager.LayoutParams p = createPopupLayoutParams(anchor.getWindowToken());
    
    //'把這個(gè)LayoutParams傳過(guò)去,把PopupWindow真正的樣子,也就是view創(chuàng)建出來(lái)'
    preparePopup(p);

    //'findDropDownPosition方法確定好PopupWindow要顯示的位置'
    final boolean aboveAnchor = findDropDownPosition(anchor, p, xoff, yoff,
            p.width, p.height, gravity);
    updateAboveAnchor(aboveAnchor);
    p.accessibilityIdOfAnchor = (anchor != null) ? anchor.getAccessibilityViewId() : -1;
    
    //'最終調(diào)用windowmanager.addview方法呈現(xiàn)popupwindow'
    invokePopup(p);
}

第一步:創(chuàng)建WindowManager.LayoutParams

我們可以看到創(chuàng)建WindowManager.LayoutParams是通過(guò)代碼
final WindowManager.LayoutParams p = createPopupLayoutParams(anchor.getWindowToken());我們具體來(lái)看下這個(gè)方法

private WindowManager.LayoutParams createPopupLayoutParams(IBinder token) {
    final WindowManager.LayoutParams p = new WindowManager.LayoutParams();

    // These gravity settings put the view at the top left corner of the
    // screen. The view is then positioned to the appropriate location by
    // setting the x and y offsets to match the anchor bottom-left
    // corner.
    p.gravity = computeGravity();
    p.flags = computeFlags(p.flags);
    p.type = mWindowLayoutType;
    p.token = token;
    p.softInputMode = mSoftInputMode;
    p.windowAnimations = computeAnimationResource();

    if (mBackground != null) {
        p.format = mBackground.getOpacity();
    } else {
        p.format = PixelFormat.TRANSLUCENT;
    }

    if (mHeightMode < 0) {
        p.height = mLastHeight = mHeightMode;
    } else {
        p.height = mLastHeight = mHeight;
    }

    if (mWidthMode < 0) {
        p.width = mLastWidth = mWidthMode;
    } else {
        p.width = mLastWidth = mWidth;
    }

    p.privateFlags = PRIVATE_FLAG_WILL_NOT_REPLACE_ON_RELAUNCH
            | PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME;

    // Used for debugging.
    p.setTitle("PopupWindow:" + Integer.toHexString(hashCode()));

    return p;
}

第二步:創(chuàng)建View

我們?cè)倏?code>preparePopup(p);方法:

private void preparePopup(WindowManager.LayoutParams p) {
    if (mContentView == null || mContext == null || mWindowManager == null) {
        throw new IllegalStateException("You must specify a valid content view by calling setContentView() before attempting to show the popup.");
    }

    // The old decor view may be transitioning out. Make sure it finishes
    // and cleans up before we try to create another one.
    if (mDecorView != null) {
        mDecorView.cancelTransitions();
    }

    // When a background is available, we embed the content view within
    // another view that owns the background drawable.
    
    
    /**
    '準(zhǔn)備backgroundView,因?yàn)橐话鉳BackgroundView是null,
    所以把之前setContentView設(shè)置的contentView作為mBackgroundView,
    不然就生成一個(gè)PopupBackgroundView(繼承FrameLayout),
    把contentView加進(jìn)去,然后再對(duì)這個(gè)PopupBackgroundView設(shè)置背景'
    */
    if (mBackground != null) {
        mBackgroundView = createBackgroundView(mContentView);
        mBackgroundView.setBackground(mBackground);
    } else {
        mBackgroundView = mContentView;
    }

    /**
    '生成相應(yīng)的PopupWindow的根View。
    實(shí)際也就是實(shí)例一個(gè)PopupDecorView(繼承FrameLayout),然后把contentView add進(jìn)來(lái)
    (ps:是不是想起Activity的根view:DecorView,也是叫這個(gè)名字,也是把Activity的contentView加進(jìn)來(lái))'
    */
    mDecorView = createDecorView(mBackgroundView);

    // The background owner should be elevated so that it casts a shadow.
    mBackgroundView.setElevation(mElevation);

    // We may wrap that in another view, so we will need to manually specify
    // the surface insets.
    p.setSurfaceInsets(mBackgroundView, true /*manual*/, true /*preservePrevious*/);

    mPopupViewInitialLayoutDirectionInherited =
            (mContentView.getRawLayoutDirection() == View.LAYOUT_DIRECTION_INHERIT);
}

第三步:WindowManager.LayoutParams根據(jù)我們的參考View來(lái)確定具體屬性值

主要是通過(guò)源碼中的下面這個(gè)方法:

findDropDownPosition(anchor, p, xoff, yoff,p.width, p.height, gravity);

因?yàn)槲覀兛赡茏孭opupWindow出現(xiàn)在我們點(diǎn)擊按鈕的下面,所以我們會(huì)傳入按鈕的View,我們知道我們讓PopupWindow出現(xiàn)在按鈕下方,肯定需要設(shè)置WindowManager.LayoutParams的x,y值,才能讓它出現(xiàn)在指定位置,所以我們肯定要根據(jù)按鈕的View,獲取它的x,y值,然后額外加上我們后來(lái)傳進(jìn)來(lái)的x,y軸的偏移值,然后最后顯示。

我們具體查看源碼的內(nèi)容:

private boolean findDropDownPosition(View anchor, WindowManager.LayoutParams outParams,
        int xOffset, int yOffset, int width, int height, int gravity) {
    final int anchorHeight = anchor.getHeight();
    final int anchorWidth = anchor.getWidth();
    if (mOverlapAnchor) {
        yOffset -= anchorHeight;
    }

    // Initially, align to the bottom-left corner of the anchor plus offsets.
    final int[] drawingLocation = mTmpDrawingLocation;
    
    
    /**
    '我們可以看到調(diào)用了getLocationInWindow方法,
    來(lái)獲取我們參考的View的當(dāng)前窗口內(nèi)的絕對(duì)坐標(biāo),
    得到的值為數(shù)組:
    location[0] -----> x坐標(biāo)
    location[1] -----> y坐標(biāo)'
    */
    anchor.getLocationInWindow(drawingLocation);
    //'我們的PopupWindow的x為當(dāng)前的參考View的x值加上我們額外傳入的偏移值'
    outParams.x = drawingLocation[0] + xOffset;
    //'我們的PopupWindow的y為當(dāng)前的參考View的y值加上我們參考view的高度及額外傳入的偏移值'
    outParams.y = drawingLocation[1] + anchorHeight + yOffset;

    final Rect displayFrame = new Rect();
    anchor.getWindowVisibleDisplayFrame(displayFrame);
    if (width == MATCH_PARENT) {
        width = displayFrame.right - displayFrame.left;
    }
    if (height == MATCH_PARENT) {
        height = displayFrame.bottom - displayFrame.top;
    }

    // Let the window manager know to align the top to y.
    outParams.gravity = computeGravity();
    outParams.width = width;
    outParams.height = height;

    // If we need to adjust for gravity RIGHT, align to the bottom-right
    // corner of the anchor (still accounting for offsets).
    final int hgrav = Gravity.getAbsoluteGravity(gravity, anchor.getLayoutDirection())
            & Gravity.HORIZONTAL_GRAVITY_MASK;
            
    /**
    '如果是Gravity.RIGHT,我們的x值還需要再做偏移,
    相當(dāng)于減去(我們的PopupWindow寬度減去參考View的寬度)。'    
    */
    if (hgrav == Gravity.RIGHT) {
        outParams.x -= width - anchorWidth;
    }

    final int[] screenLocation = mTmpScreenLocation;
    anchor.getLocationOnScreen(screenLocation);

    // First, attempt to fit the popup vertically without resizing.
    final boolean fitsVertical = tryFitVertical(outParams, yOffset, height,
            anchorHeight, drawingLocation[1], screenLocation[1], displayFrame.top,
            displayFrame.bottom, false);

    // Next, attempt to fit the popup horizontally without resizing.
    final boolean fitsHorizontal = tryFitHorizontal(outParams, xOffset, width,
            anchorWidth, drawingLocation[0], screenLocation[0], displayFrame.left,
            displayFrame.right, false);

    // If the popup still doesn not fit, attempt to scroll the parent.
    if (!fitsVertical || !fitsHorizontal) {
        final int scrollX = anchor.getScrollX();
        final int scrollY = anchor.getScrollY();
        final Rect r = new Rect(scrollX, scrollY, scrollX + width + xOffset,
                scrollY + height + anchorHeight + yOffset);
        if (mAllowScrollingAnchorParent && anchor.requestRectangleOnScreen(r, true)) {
            // Reset for the new anchor position.
            anchor.getLocationInWindow(drawingLocation);
            outParams.x = drawingLocation[0] + xOffset;
            outParams.y = drawingLocation[1] + anchorHeight + yOffset;

            // Preserve the gravity adjustment.
            if (hgrav == Gravity.RIGHT) {
                outParams.x -= width - anchorWidth;
            }
        }

        // Try to fit the popup again and allowing resizing.
        tryFitVertical(outParams, yOffset, height, anchorHeight, drawingLocation[1],
                screenLocation[1], displayFrame.top, displayFrame.bottom, mClipToScreen);
        tryFitHorizontal(outParams, xOffset, width, anchorWidth, drawingLocation[0],
                screenLocation[0], displayFrame.left, displayFrame.right, mClipToScreen);
    }

    // Return whether the popup top edge is above the anchor top edge.
    return outParams.y < drawingLocation[1];
}

第三步:WindowManager添加相應(yīng)的View

通過(guò)最后的invokePopup(p);

private void invokePopup(WindowManager.LayoutParams p) {
    if (mContext != null) {
        p.packageName = mContext.getPackageName();
    }

    final PopupDecorView decorView = mDecorView;
    decorView.setFitsSystemWindows(mLayoutInsetDecor);

    setLayoutDirectionFromAnchor();
    
    //'最后通過(guò)windowmanager的addview方法把decorView加進(jìn)來(lái)'
    mWindowManager.addView(decorView, p);

    if (mEnterTransition != null) {
        decorView.requestEnterTransition(mEnterTransition);
    }
}

補(bǔ)充1:當(dāng)然我們平常也知道用WindowManager.removeView或者removeViewImmediate方法移除View,而我們的PopupWindow.dismiss()方法也是一樣,使用了mWindowManager.removeViewImmediate(decorView);移除,這步我就不多說(shuō)了。大家可以自己看下。

補(bǔ)充2:看懂了showAsDropDown的源碼,showAsLocation的就更簡(jiǎn)單了,直接讓LayoutParams的x和y值等于你傳入的x,y值,其他代碼都是類似的。

補(bǔ)充3:我們前面提過(guò)在onCreate方法里面直接顯示ShowAsDropDown等顯示方法會(huì)報(bào)錯(cuò):android.view.WindowManager$BadTokenException,因?yàn)檫@時(shí)候Activity的相關(guān)View都沒(méi)初始化好,也就拿到的view.token為null了。

結(jié)語(yǔ)

PopupWindow小結(jié)可能寫的不夠全,或者哪里寫的不對(duì),歡迎大家指出。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,237評(píng)論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,957評(píng)論 3 423
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,248評(píng)論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,356評(píng)論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,081評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,485評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,534評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,720評(píng)論 0 289
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,263評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,025評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,204評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,787評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,461評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,874評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,105評(píng)論 1 289
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,945評(píng)論 3 395
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,205評(píng)論 2 375