當前位置:
首頁 > 知識 > PopupWindow源碼分析

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

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

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。

PopupWindow源碼分析

  • 接著看看createDecorView(mBackgroundView)這個方法源碼
  • 把PopupWindow的根view創建出來,並把contentView通過addView方法添加進去。PopupDecorView繼承FrameLayout,其中沒有繪畫什麼,只是複寫了dispatchKeyEvent和onTouchEvent之類的事件分發的函數,還有實現進場退場動畫的執行函數

PopupWindow源碼分析

PopupWindow源碼分析

  • 最後看看invokePopup(WindowManager.LayoutParams p)源碼
  • 執行invokePopup(p),這個函數主要將popupView添加到應用DecorView的相應位置,通過之前創建WindowManager完成這個步驟,現在PopupWIndow可以看得到。
  • 並且請求在下一次布局傳遞之後運行Enter轉換。

PopupWindow源碼分析

2.3 dismiss()源碼分析

  • 通過對象調用該方法可以達到銷毀彈窗的目的。
  • 重點看一下這個兩個方法。移除view和清除錨視圖

PopupWindow源碼分析

  • 接著看看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);

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 程序員小新人學習 的精彩文章:

區塊鏈教程之基礎開發通過介面查詢幣種提幣情況bch
數據抽取清洗轉換載入工具ETL

TAG:程序員小新人學習 |