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

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
    // 可是設置爲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);
    }

1.4 注意問題寬和高屬性

  • 先看問題代碼,下面這個不會出現彈窗,思考:爲何?php

    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);
  • 注意:必須設置寬和高,不然不顯示任何東西java

    • 這裏的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的子類,而後看一下里面的代碼:git

    • 能夠發現其重寫了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封裝庫介紹

項目地址:https://github.com/yangchong211/YCDialog

  • 鏈式編程,十分方便,更多內容能夠直接參考個人開源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.關於個人博客

相關文章
相關標籤/搜索