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運行在子線程導致崩潰如何解決?

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 注意問題寬和高屬性

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

    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封裝庫介紹

項目地址: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.關於我的博客

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章