Android基礎(chǔ) (11) PopupWindow詳解

(1)PopupWindow的使用
(2)自定義一個(gè)PopupWindow
(3)PopupWindow的源碼分析
(4)AlertDialog,popupWindow,Activity區(qū)別
(5)Activity-Window-View三者的差別

一.使用方式

Android的對(duì)話框有兩種:PopupWindow和AlertDialog。它們的不同點(diǎn)在于:

  • AlertDialog的位置固定,而PopupWindow的位置可以隨意
  • AlertDialog是非阻塞線程的,而PopupWindow是阻塞線程的

PopupWindow的位置按照有無(wú)偏移分,可以分為偏移和無(wú)偏移兩種;按照參照物的不同,可以分為相對(duì)于某個(gè)控件(Anchor錨)和相對(duì)于父控件。具體如下:
showAsDropDown(View anchor):相對(duì)某個(gè)控件的位置(正左下方),無(wú)偏移
showAsDropDown(View anchor, int xoff, int yoff):相對(duì)某個(gè)控件的位置,有偏移
showAtLocation(View parent, int gravity, int x, int y):相對(duì)于父控件的位置(例如正中央Gravity.CENTER,下方Gravity.BOTTOM等),可以設(shè)置偏移或無(wú)偏移。

使用詳述:https://blog.csdn.net/xiaanming/article/details/9121383

二. 源碼分析

1.最簡(jiǎn)單的創(chuàng)建方法
1.1 PopupWindow構(gòu)造方法
public PopupWindow (Context context)
public PopupWindow(View contentView)
public PopupWindow(int width, int height)
public PopupWindow(View contentView, int width, int height)
public PopupWindow(View contentView, int width, int height, boolean focusable)
1.2 顯示PopupWindow
showAsDropDown(View anchor):相對(duì)某個(gè)控件的位置(正左下方),無(wú)偏移
showAsDropDown(View anchor, int xoff, int yoff):相對(duì)某個(gè)控件的位置,有偏移
showAtLocation(View parent, int gravity, int x, int y):相對(duì)于父控件的位置(例如正中央Gravity.CENTER,下方Gravity.BOTTOM等),可以設(shè)置偏移或無(wú)偏移
1.3 最簡(jiǎn)單的創(chuàng)建
  • 具體如下所示
//創(chuàng)建對(duì)象
PopupWindow popupWindow = new PopupWindow(this);
View inflate = LayoutInflater.from(this).inflate(R.layout.view_pop_custom, null);
//設(shè)置view布局
popupWindow.setContentView(inflate);
popupWindow.setWidth(LinearLayout.LayoutParams.WRAP_CONTENT);
popupWindow.setHeight(LinearLayout.LayoutParams.WRAP_CONTENT);
//設(shè)置動(dòng)畫的方法
popupWindow.setAnimationStyle(R.style.BottomDialog);
//設(shè)置PopUpWindow的焦點(diǎn),設(shè)置為true之后,PopupWindow內(nèi)容區(qū)域,才可以響應(yīng)點(diǎn)擊事件
popupWindow.setTouchable(true);
//設(shè)置背景透明
popupWindow.setBackgroundDrawable(new ColorDrawable(0x00000000));
//點(diǎn)擊空白處的時(shí)候讓PopupWindow消失
popupWindow.setOutsideTouchable(true);
// true時(shí),點(diǎn)擊返回鍵先消失 PopupWindow
// 但是設(shè)置為true時(shí)setOutsideTouchable,setTouchable方法就失效了(點(diǎn)擊外部不消失,內(nèi)容區(qū)域也不響應(yīng)事件)
// false時(shí)PopupWindow不處理返回鍵,默認(rèn)是false
popupWindow.setFocusable(false);
//設(shè)置dismiss事件
popupWindow.setOnDismissListener(new PopupWindow.OnDismissListener() {
    @Override
    public void onDismiss() {

    }
});
boolean showing = popupWindow.isShowing();
if (!showing){
    //show,并且可以設(shè)置位置
    popupWindow.showAsDropDown(mTv1);
}
1.4 注意問(wèn)題:寬和高屬性
  • 先看問(wèn)題代碼,下面這個(gè)不會(huì)出現(xiàn)彈窗,思考:為什么?
PopupWindow popupWindow = new PopupWindow(this);
View inflate = LayoutInflater.from(this).inflate(R.layout.view_pop_custom, null);
popupWindow.setContentView(inflate);
popupWindow.setAnimationStyle(R.style.BottomDialog);
popupWindow.showAsDropDown(mTv1);

這里的WRAP_CONTENT可以換成fill_parent 也可以是具體的數(shù)值,它是指PopupWindow的大小,也就是contentView的大小,注意popupWindow根據(jù)這個(gè)大小顯示你的View,如果你的View本身是從xml得到的,那么xml的第一層view的大小屬性將被忽略。相當(dāng)于popupWindow的width和height屬性直接和第一層View相對(duì)應(yīng)。

2.源碼分析
2.1 setContentView(View contentView)源碼分析
  • 首先先來(lái)看看源碼
    可以看出,先判斷是否show,如果沒(méi)有showing的話,則進(jìn)行contentView賦值,如果mWindowManager為null,則取獲取mWindowManager,這個(gè)很重要。最后便是根據(jù)SDK版本而不是在構(gòu)造函數(shù)中設(shè)置附加InDecor的默認(rèn)設(shè)置,因?yàn)闃?gòu)造函數(shù)中可能沒(méi)有上下文對(duì)象。我們只想在這里設(shè)置默認(rèn),如果應(yīng)用程序尚未設(shè)置附加InDecor。
public void setContentView(View contentView) {
    //判斷是否show,如果已經(jīng)show,則返回
    if (isShowing()) {
        return;
    }
    //賦值
    mContentView = contentView;

    if (mContext == null && mContentView != null) {
        mContext = mContentView.getContext();
    }

    if (mWindowManager == null && mContentView != null) {
        mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
    }

    //在這里根據(jù)SDK版本而不是在構(gòu)造函數(shù)中設(shè)置附加InDecor的默認(rèn)設(shè)置,因?yàn)闃?gòu)造函數(shù)中可能沒(méi)有上下文對(duì)象。
   // 我們只想在這里設(shè)置默認(rèn),如果應(yīng)用程序尚未設(shè)置附加InDecor。
    if (mContext != null && !mAttachedInDecorSet) {
        setAttachedInDecor(mContext.getApplicationInfo().targetSdkVersion
                >= Build.VERSION_CODES.LOLLIPOP_MR1);
    }
}
  • 接著來(lái)看一下setAttachedInDecor源碼部分
    執(zhí)行setAttachedInDecor給一個(gè)變量賦值為true,表示已經(jīng)在decor里注冊(cè)了(注意:現(xiàn)在還沒(méi)有使用WindowManager把PopupWindow添加到DecorView上)
public void setAttachedInDecor(boolean enabled) {
    mAttachedInDecor = enabled;
    mAttachedInDecorSet = true;
}
2.2 showAsDropDown()源碼
  • 先來(lái)看一下showAsDropDown(View anchor)部分代碼
    可以看出,調(diào)用這個(gè)方法,默認(rèn)偏移值都是0;關(guān)于這個(gè)attachToAnchor(anchor, xoff, yoff, gravity)方法作用,下面再說(shuō)。之后通過(guò)createPopupLayoutParams方法創(chuàng)建和初始化LayoutParams,然后把這個(gè)LayoutParams傳過(guò)去,把PopupWindow真正的樣子,也就是view創(chuàng)建出來(lái)。
public void showAsDropDown(View anchor) {
    showAsDropDown(anchor, 0, 0);
}

//主要看這個(gè)方法
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;

    //通過(guò)createPopupLayoutParams方法創(chuàng)建和初始化LayoutParams
    final WindowManager.LayoutParams p = createPopupLayoutParams(anchor.getWindowToken());
    preparePopup(p);

    final boolean aboveAnchor = findDropDownPosition(anchor, p, xoff, yoff,
            p.width, p.height, gravity);
    updateAboveAnchor(aboveAnchor);
    p.accessibilityIdOfAnchor = (anchor != null) ? anchor.getAccessibilityViewId() : -1;

    invokePopup(p);
}
  • 接著來(lái)看看attachToAnchor(anchor, xoff, yoff, gravity)源碼

執(zhí)行了一個(gè)attachToAnchor,意思是PopupWindow類似一個(gè)錨掛在目標(biāo)view的下面,這個(gè)函數(shù)主要講xoff、yoff(x軸、y軸偏移值)、gravity(比如Gravity.BOTTOM之類,指的是PopupWindow放在目標(biāo)view哪個(gè)方向邊緣的位置)這個(gè)attachToAnchor有點(diǎn)意思,通過(guò)弱引用保存目標(biāo)view和目標(biāo)view的rootView(我們都知道:通過(guò)弱引用和軟引用可以防止內(nèi)存泄漏)、這個(gè)rootview是否依附在window、還有保存偏差值、gravity

private void attachToAnchor(View anchor, int xoff, int yoff, int gravity) {
    detachFromAnchor();

    final ViewTreeObserver vto = anchor.getViewTreeObserver();
    if (vto != null) {
        vto.addOnScrollChangedListener(mOnScrollChangedListener);
    }

    final View anchorRoot = anchor.getRootView();
    anchorRoot.addOnAttachStateChangeListener(mOnAnchorRootDetachedListener);

    mAnchor = new WeakReference<>(anchor);
    mAnchorRoot = new WeakReference<>(anchorRoot);
    mIsAnchorRootAttached = anchorRoot.isAttachedToWindow();

    mAnchorXoff = xoff;
    mAnchorYoff = yoff;
    mAnchoredGravity = gravity;
}
  • 接著再來(lái)看看preparePopup(p)這個(gè)方法源碼
    把這個(gè)LayoutParams傳過(guò)去,把PopupWindow真正的樣子,也就是view創(chuàng)建出來(lái),在這個(gè)preparePopup函數(shù)里,一開始準(zhǔn)備backgroundView,因?yàn)橐话鉳BackgroundView是null,所以把之前setContentView設(shè)置的contentView作為mBackgroundView。


  • 接著看看createDecorView(mBackgroundView)這個(gè)方法源碼
    把PopupWindow的根view創(chuàng)建出來(lái),并把contentView通過(guò)addView方法添加進(jìn)去。PopupDecorView繼承FrameLayout,其中沒(méi)有繪畫什么,只是復(fù)寫了dispatchKeyEvent和onTouchEvent之類的事件分發(fā)的函數(shù),還有實(shí)現(xiàn)進(jìn)場(chǎng)退場(chǎng)動(dòng)畫的執(zhí)行函數(shù)



  • 最后看看invokePopup(WindowManager.LayoutParams p)源碼
    執(zhí)行invokePopup(p),這個(gè)函數(shù)主要將popupView添加到應(yīng)用DecorView的相應(yīng)位置,通過(guò)之前創(chuàng)建WindowManager完成這個(gè)步驟,現(xiàn)在PopupWIndow可以看得到。
    并且請(qǐng)求在下一次布局傳遞之后運(yùn)行Enter轉(zhuǎn)換。


2.3 dismiss()源碼分析
  • 通過(guò)對(duì)象調(diào)用該方法可以達(dá)到銷毀彈窗的目的。
    重點(diǎn)看一下這個(gè)兩個(gè)方法。移除view和清除錨視圖


  • 接著看看dismissImmediate(View decorView, ViewGroup contentHolder, View contentView)源碼
    第一步,通過(guò)WindowManager注銷PopupView
    第二步,PopupView移除contentView
    第三步,講mDecorView,mBackgroundView置為null
private void dismissImmediate(View decorView, ViewGroup contentHolder, View contentView) {
    // If this method gets called and the decor view doesn't have a parent,
    // then it was either never added or was already removed. That should
    // never happen, but it's worth checking to avoid potential crashes.
    if (decorView.getParent() != null) {
        mWindowManager.removeViewImmediate(decorView);
    }

    if (contentHolder != null) {
        contentHolder.removeView(contentView);
    }

    // This needs to stay until after all transitions have ended since we
    // need the reference to cancel transitions in preparePopup().
    mDecorView = null;
    mBackgroundView = null;
    mIsTransitioningToDismiss = false;
}
2.4 PopupDecorView源碼分析
  • 通過(guò)createDecorView(View contentView)方法可以知道,是PopupDecorView直接new出來(lái)的布局對(duì)象decorView,外面包裹了一層PopupDecorView,這里的PopupDecorView也是我們自定義的FrameLayout的子類,然后看一下里面的代碼:
    • 可以發(fā)現(xiàn)其重寫了onTouchEvent時(shí)間,這樣我們?cè)邳c(diǎn)擊popupWindow外面的時(shí)候就會(huì)執(zhí)行pupopWindow的dismiss方法,取消PopupWindow。
private class PopupDecorView extends FrameLayout {
   private TransitionListenerAdapter mPendingExitListener;

   public PopupDecorView(Context context) {
       super(context);
   }
   @Override
   public boolean onTouchEvent(MotionEvent event) {
       final int x = (int) event.getX();
       final int y = (int) event.getY();

       if ((event.getAction() == MotionEvent.ACTION_DOWN)
               && ((x < 0) || (x >= getWidth()) || (y < 0) || (y >= getHeight()))) {
           dismiss();
           return true;
       } else if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
           dismiss();
           return true;
       } else {
           return super.onTouchEvent(event);
       }
   }
}
3.經(jīng)典總結(jié)
3.1 PopupWindow和Dialog有什么區(qū)別?
  • 兩者最根本的區(qū)別在于有沒(méi)有新建一個(gè)window,PopupWindow沒(méi)有新建,而是將view加到DecorView;Dialog是新建了一個(gè)window,相當(dāng)于走了一遍Activity中創(chuàng)建window的流程
  • 從源碼中可以看出,PopupWindow最終是執(zhí)行了mWindowManager.addView方法,全程沒(méi)有新建window
3.2 創(chuàng)建和銷毀的大概流程
  • 源碼比較少,比較容易懂,即使不太懂,只要借助有道詞典翻譯一下英文注釋,還是可以搞明白的。

  • 總結(jié)一下PopupWindow的創(chuàng)建出現(xiàn)、消失有哪些重要操作

  • 創(chuàng)建PopupWindow的時(shí)候,先創(chuàng)建WindowManager,因?yàn)閃IndowManager擁有控制view的添加和刪除、修改的能力。

  • 然后是setContentView,保存contentView,這個(gè)步驟就做了這個(gè)
    顯示PopupWindow,這個(gè)步驟稍微復(fù)雜點(diǎn),創(chuàng)建并初始化LayoutParams,設(shè)置相關(guān)參數(shù),作為以后PopupWindow在應(yīng)用DecorView里哪里顯示的憑據(jù)。然后創(chuàng)建PopupView,并且將contentView插入其中。最后使用WindowManager將PopupView添加到應(yīng)用DecorView里。

  • 銷毀PopupView,WindowManager把PopupView移除,PopupView再把contentView移除,最后把對(duì)象置為null

3.3 為何彈窗點(diǎn)擊一下就dismiss呢?

PopupWindow通過(guò)為傳入的View添加一層包裹的布局,并重寫該布局的點(diǎn)擊事件,實(shí)現(xiàn)點(diǎn)擊PopupWindow之外的區(qū)域PopupWindow消失的效果

封裝庫(kù)可前往:#### https://github.com/yangchong211/YCDialog

三者關(guān)系
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容