天天看點

PopupWindow源碼分析

目錄介紹

  • 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萬字],轉載請注明出處,謝謝!
  • 連結位址: https://github.com/yangchong211/YCBlogs
  • 如果覺得好,可以star一下,謝謝!當然也歡迎提出建議,萬事起于忽微,量變引起質變!
  • PopupWindow封裝庫項目位址: https://github.com/yangchong211/YCDialog
  • 02.Toast源碼深度分析
    • 最簡單的建立,簡單改造避免重複建立,show()方法源碼分析,scheduleTimeoutLocked吐司如何自動銷毀的,TN類中的消息機制是如何執行的,普通應用的Toast顯示數量是有限制的,用代碼解釋為何Activity銷毀後Toast仍會顯示,Toast偶爾報錯Unable to add window是如何産生的,Toast運作在子線程問題,Toast如何添加系統視窗的權限等等
  • 03.DialogFragment源碼分析
    • 最簡單的使用方法,onCreate(@Nullable Bundle savedInstanceState)源碼分析,重點分析彈窗展示和銷毀源碼,使用中show()方法遇到的IllegalStateException分析
  • 05.PopupWindow源碼分析
    • 顯示PopupWindow,注意問題寬和高屬性,showAsDropDown()源碼,dismiss()源碼分析,PopupWindow和Dialog有什麼差別?為何彈窗點選一下就dismiss呢?
  • 06.Snackbar源碼分析
    • 最簡單的建立,Snackbar的make方法源碼分析,Snackbar的show顯示與點選消失源碼分析,顯示和隐藏中動畫源碼分析,Snackbar的設計思路,為什麼Snackbar總是顯示在最下面
  • 07.彈窗常見問題
    • DialogFragment使用中show()方法遇到的IllegalStateException,什麼常見産生的?Toast偶爾報錯Unable to add window,Toast運作在子線程導緻崩潰如何解決?

  • 如下所示
    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)           

  • showAsDropDown(View anchor):相對某個控件的位置(正左下方),無偏移
    showAsDropDown(View anchor, int xoff, int yoff):相對某個控件的位置,有偏移
    showAtLocation(View parent, int gravity, int x, int y):相對于父控件的位置(例如正中央Gravity.CENTER,下方Gravity.BOTTOM等),可以設定偏移或無偏移           

  • 具體如下所示
    //建立對象
    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
    // 但是設定為true時setOutsideTouchable,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);
    }           

  • 先看問題代碼,下面這個不會出現彈窗,思考:為什麼?
    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.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;
    }           

  • 先來看一下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。
  • 接着看看createDecorView(mBackgroundView)這個方法源碼
    • 把PopupWindow的根view建立出來,并把contentView通過addView方法添加進去。PopupDecorView繼承FrameLayout,其中沒有繪畫什麼,隻是複寫了dispatchKeyEvent和onTouchEvent之類的事件分發的函數,還有實作進場退場動畫的執行函數
  • 最後看看invokePopup(WindowManager.LayoutParams p)源碼
    • 執行invokePopup(p),這個函數主要将popupView添加到應用DecorView的相應位置,通過之前建立WindowManager完成這個步驟,現在PopupWIndow可以看得到。
    • 并且請求在下一次布局傳遞之後運作Enter轉換。

  • 通過對象調用該方法可以達到銷毀彈窗的目的。
    • 重點看一下這個兩個方法。移除view和清除錨視圖
  • 接着看看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;
    }           

  • 通過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);
            }
        }
    }           

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

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

  • PopupWindow通過為傳入的View添加一層包裹的布局,并重寫該布局的點選事件,實作點選PopupWindow之外的區域PopupWindow消失的效果

項目位址:

  • 鍊式程式設計,十分友善,更多内容可以直接參考我的開源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.關于我的部落格

繼續閱讀