PopupWindow原始碼分析

楊充發表於2018-10-11

目錄介紹

  • 1.最簡單的建立方法
    • 1.1 PopupWindow構造方法
    • 1.2 顯示PopupWindow
    • 1.3 最簡單的建立
    • 1.4 注意問題寬和高屬性
  • 2.原始碼分析
    • 2.1 setContentView(View contentView)
    • 2.2 showAsDropDown()原始碼
    • 2.3 dismiss()原始碼分析
    • 2.4 PopupDecorView原始碼分析
  • 3.經典總結
    • 3.1 PopupWindow和Dialog有什麼區別?
    • 3.2 建立和銷燬的大概流程
    • 3.3 為何彈窗點選一下就dismiss呢?
  • 4.PopupWindow封裝庫介紹

好訊息

  • 部落格筆記大彙總【16年3月到至今】,包括Java基礎及深入知識點,Android技術部落格,Python學習筆記等等,還包括平時開發中遇到的bug彙總,當然也在工作之餘收集了大量的面試題,長期更新維護並且修正,持續完善……開源的檔案是markdown格式的!同時也開源了生活部落格,從12年起,積累共計47篇[近20萬字],轉載請註明出處,謝謝!
  • 連結地址:github.com/yangchong21…
  • 如果覺得好,可以star一下,謝謝!當然也歡迎提出建議,萬事起於忽微,量變引起質變!
  • PopupWindow封裝庫專案地址:github.com/yangchong21…

1.最簡單的建立方法

1.1 PopupWindow構造方法

  • 如下所示
    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):相對某個控制元件的位置(正左下方),無偏移
    showAsDropDown(View anchor, int xoff, int yoff):相對某個控制元件的位置,有偏移
    showAtLocation(View parent, int gravity, int x, int y):相對於父控制元件的位置(例如正中央Gravity.CENTER,下方Gravity.BOTTOM等),可以設定偏移或無偏移
    複製程式碼

1.3 最簡單的建立

  • 具體如下所示
    //建立物件
    PopupWindow popupWindow = new PopupWindow(this);
    View inflate = LayoutInflater.from(this).inflate(R.layout.view_pop_custom, null);
    //設定view佈局
    popupWindow.setContentView(inflate);
    popupWindow.setWidth(LinearLayout.LayoutParams.WRAP_CONTENT);
    popupWindow.setHeight(LinearLayout.LayoutParams.WRAP_CONTENT);
    //設定動畫的方法
    popupWindow.setAnimationStyle(R.style.BottomDialog);
    //設定PopUpWindow的焦點,設定為true之後,PopupWindow內容區域,才可以響應點選事件
    popupWindow.setTouchable(true);
    //設定背景透明
    popupWindow.setBackgroundDrawable(new ColorDrawable(0x00000000));
    //點選空白處的時候讓PopupWindow消失
    popupWindow.setOutsideTouchable(true);
    // true時,點選返回鍵先消失 PopupWindow
    // 但是設定為truesetOutsideTouchable,setTouchable方法就失效了(點選外部不消失,內容區域也不響應事件)
    // false時PopupWindow不處理返回鍵,預設是false
    popupWindow.setFocusable(false);
    //設定dismiss事件
    popupWindow.setOnDismissListener(new PopupWindow.OnDismissListener() {
        @Override
        public void onDismiss() {
    
        }
    });
    boolean showing = popupWindow.isShowing();
    if (!showing){
        //show,並且可以設定位置
        popupWindow.showAsDropDown(mTv1);
    }
    複製程式碼

1.4 注意問題寬和高屬性

  • 先看問題程式碼,下面這個不會出現彈窗,思考:為什麼?

    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 也可以是具體的數值,它是指PopupWindow的大小,也就是contentview的大小,注意popupwindow根據這個大小顯示你的View,如果你的View本身是從xml得到的,那麼xml的第一層view的大小屬性將被忽略。相當於popupWindow的width和height屬性直接和第一層View相對應。

2.原始碼分析

2.1 setContentView(View contentView)原始碼分析

  • 首先先來看看原始碼
    • 可以看出,先判斷是否show,如果沒有showing的話,則進行contentView賦值,如果mWindowManager為null,則取獲取mWindowManager,這個很重要。最後便是根據SDK版本而不是在建構函式中設定附加InDecor的預設設定,因為建構函式中可能沒有上下文物件。我們只想在這裡設定預設,如果應用程式尚未設定附加InDecor。
    public void setContentView(View contentView) {
        //判斷是否show,如果已經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);
        }
    
        //在這裡根據SDK版本而不是在建構函式中設定附加InDecor的預設設定,因為建構函式中可能沒有上下文物件。我們只想在這裡設定預設,如果應用程式尚未設定附加InDecor。
        if (mContext != null && !mAttachedInDecorSet) {
            setAttachedInDecor(mContext.getApplicationInfo().targetSdkVersion
                    >= Build.VERSION_CODES.LOLLIPOP_MR1);
        }
    
    }
    複製程式碼
  • 接著來看一下setAttachedInDecor原始碼部分
    • 執行setAttachedInDecor給一個變數賦值為true,表示已經在decor裡註冊了(注意:現在還沒有使用WindowManager把PopupWindow新增到DecorView上)
    public void setAttachedInDecor(boolean enabled) {
        mAttachedInDecor = enabled;
        mAttachedInDecorSet = true;
    }
    複製程式碼

2.2 showAsDropDown()原始碼

  • 先來看一下showAsDropDown(View anchor)部分程式碼
    • 可以看出,呼叫這個方法,預設偏移值都是0;關於這個attachToAnchor(anchor, xoff, yoff, gravity)方法作用,下面再說。之後通過createPopupLayoutParams方法建立和初始化LayoutParams,然後把這個LayoutParams傳過去,把PopupWindow真正的樣子,也就是view建立出來。
    public void showAsDropDown(View anchor) {
        showAsDropDown(anchor, 0, 0);
    }
    
    //主要看這個方法
    //注意啦:關於更多內容,可以參考我的部落格大彙總:https://github.com/yangchong211/YCBlogs
    public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
        if (isShowing() || mContentView == null) {
            return;
        }
    
        TransitionManager.endTransitions(mDecorView);
    
        //下面單獨講
        //https://github.com/yangchong211/YCBlogs
        attachToAnchor(anchor, xoff, yoff, gravity);
    
        mIsShowing = true;
        mIsDropdown = true;
    
        //通過createPopupLayoutParams方法建立和初始化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);
    }
    複製程式碼
  • 接著來看看attachToAnchor(anchor, xoff, yoff, gravity)原始碼
    • 執行了一個attachToAnchor,意思是PopupWindow類似一個錨掛在目標view的下面,這個函式主要講xoff、yoff(x軸、y軸偏移值)、gravity(比如Gravity.BOTTOM之類,指的是PopupWindow放在目標view哪個方向邊緣的位置)這個attachToAnchor有點意思,通過弱引用儲存目標view和目標view的rootView(我們都知道:通過弱引用和軟引用可以防止記憶體洩漏)、這個rootview是否依附在window、還有儲存偏差值、gravity
    • 關於四種引用的深入介紹可以參考我的這邊文章:01.四種引用比較與原始碼分析
    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;
    }
    複製程式碼
  • 接著再來看看preparePopup(p)這個方法原始碼
    • 把這個LayoutParams傳過去,把PopupWindow真正的樣子,也就是view建立出來,在這個preparePopup函式裡,一開始準備backgroundView,因為一般mBackgroundView是null,所以把之前setContentView設定的contentView作為mBackgroundView。
    • image
  • 接著看看createDecorView(mBackgroundView)這個方法原始碼
    • 把PopupWindow的根view建立出來,並把contentView通過addView方法新增進去。PopupDecorView繼承FrameLayout,其中沒有繪畫什麼,只是複寫了dispatchKeyEvent和onTouchEvent之類的事件分發的函式,還有實現進場退場動畫的執行函式
    • image
    • image
  • 最後看看invokePopup(WindowManager.LayoutParams p)原始碼
    • 執行invokePopup(p),這個函式主要將popupView新增到應用DecorView的相應位置,通過之前建立WindowManager完成這個步驟,現在PopupWIndow可以看得到。
    • 並且請求在下一次佈局傳遞之後執行Enter轉換。
    • image

2.3 dismiss()原始碼分析

  • 通過物件呼叫該方法可以達到銷燬彈窗的目的。
    • 重點看一下這個兩個方法。移除view和清除錨檢視
    • image
  • 接著看看dismissImmediate(View decorView, ViewGroup contentHolder, View contentView)原始碼
    • 第一步,通過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原始碼分析

  • 通過createDecorView(View contentView)方法可以知道,是PopupDecorView直接new出來的佈局物件decorView,外面包裹了一層PopupDecorView,這裡的PopupDecorView也是我們自定義的FrameLayout的子類,然後看一下里面的程式碼:

    • 可以發現其重寫了onTouchEvent時間,這樣我們在點選popupWindow外面的時候就會執行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.經典總結

3.1 PopupWindow和Dialog有什麼區別?

  • 兩者最根本的區別在於有沒有新建一個window,PopupWindow沒有新建,而是將view加到DecorView;Dialog是新建了一個window,相當於走了一遍Activity中建立window的流程
  • 從原始碼中可以看出,PopupWindow最終是執行了mWindowManager.addView方法,全程沒有新建window

3.2 建立和銷燬的大概流程

  • 原始碼比較少,比較容易懂,即使不太懂,只要藉助有道詞典翻譯一下英文註釋,還是可以搞明白的。
  • 總結一下PopupWindow的建立出現、消失有哪些重要操作
    • 建立PopupWindow的時候,先建立WindowManager,因為WIndowManager擁有控制view的新增和刪除、修改的能力。這一點關於任主席的藝術探索書上寫的很詳細……
    • 然後是setContentView,儲存contentView,這個步驟就做了這個
    • 顯示PopupWindow,這個步驟稍微複雜點,建立並初始化LayoutParams,設定相關引數,作為以後PopupWindow在應用DecorView裡哪裡顯示的憑據。然後建立PopupView,並且將contentView插入其中。最後使用WindowManager將PopupView新增到應用DecorView裡。
    • 銷燬PopupView,WindowManager把PopupView移除,PopupView再把contentView移除,最後把物件置為null

3.3 為何彈窗點選一下就dismiss呢?

  • PopupWindow通過為傳入的View新增一層包裹的佈局,並重寫該佈局的點選事件,實現點選PopupWindow之外的區域PopupWindow消失的效果

4.PopupWindow封裝庫介紹

專案地址:github.com/yangchong21…

  • 鏈式程式設計,十分方便,更多內容可以直接參考我的開源demo
new CustomPopupWindow.PopupWindowBuilder(this)
        //.setView(R.layout.pop_layout)
        .setView(contentView)
        .setFocusable(true)
        //彈出popWindow時,背景是否變暗
        .enableBackgroundDark(true)
        //控制亮度
        .setBgDarkAlpha(0.7f)
        .setOutsideTouchable(true)
        .setAnimationStyle(R.style.popWindowStyle)
        .setOnDissmissListener(new PopupWindow.OnDismissListener() {
            @Override
            public void onDismiss() {
                //對話方塊銷燬時
            }
        })
        .create()
        .showAsDropDown(tv6,0,10);
複製程式碼

關於其他內容介紹

01.關於部落格彙總連結

02.關於我的部落格

相關文章