Android7.0 PopupWindow的兼容問題
Android7.0 中對 PopupWindow 這個常用的控件又做了一些改動,修復了以前遺留的一些問題的同時貌似又引入了一些問題,本文通過在7.0設備上實測並且結合源碼分析,帶你了解關於 PopupWindow 的相關改動。
Android7.0 中下面兩個問題解決了,這里強調一下,不是說從 Android7.0 開始才解決這兩個問題的,因為具體版本細節沒去深究。可能在其他的某些版本下面的問題也是被解決了的。
PopupWindow不響應點擊外部消失和返回鍵消失的解決方法,博文地址:
http://www.cnblogs.com/popfisher/p/5608717.html- 不得不吐槽的
Android PopupWindow的幾個痛點(實現帶箭頭的上下文菜單遇到的坑),博文地址:
http://www.cnblogs.com/popfisher/p/5944054.html
Android7.0 中又引入了新的問題(這就非常的尷尬了)
- 調用
update方法,PopupWindow的Gravity會改變
從源碼看7.0怎么解決遺留問題的
解決 PopupWindow 不響應點擊外部消失和返回鍵消失的問題,我們是通過自己設置一個背景。Android7.0 中不設置背景也是可以的,那么它的代碼肯定做了處理。從 api24 的源碼中找到 PopupWindow.java 文件,我找到里面的 preparePopup 方法如下:
private void preparePopup(WindowManager.LayoutParams p) {
if (mContentView == null || mContext == null || mWindowManager == null) {
throw new IllegalStateException("You must specify a valid content view by "
+ "calling setContentView() before attempting to show the popup.");
}
// The old decor view may be transitioning out. Make sure it finishes
// and cleans up before we try to create another one.
if (mDecorView != null) {
mDecorView.cancelTransitions();
}
// When a background is available, we embed the content view within
// another view that owns the background drawable.
if (mBackground != null) {
mBackgroundView = createBackgroundView(mContentView);
mBackgroundView.setBackground(mBackground);
} else {
mBackgroundView = mContentView;
}
mDecorView = createDecorView(mBackgroundView);
// The background owner should be elevated so that it casts a shadow.
mBackgroundView.setElevation(mElevation);
// We may wrap that in another view, so we'll need to manually specify
// the surface insets.
p.setSurfaceInsets(mBackgroundView, true /*manual*/, true /*preservePrevious*/);
mPopupViewInitialLayoutDirectionInherited =
(mContentView.getRawLayoutDirection() == View.LAYOUT_DIRECTION_INHERIT);
}
重點只需要看 mDecorView = createDecorView(mBackgroundView); 可以看到不管 mBackground 變量是否為空,最終都執行了這句代碼,這句代碼會多加一層 ViewGroup 把 mBackgroundView 包進去了,里面應該包含了對返回鍵的處理邏輯,我們再看看 createDecorView 方法源碼:
private PopupDecorView createDecorView(View contentView) {
final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams();
final int height;
if (layoutParams != null && layoutParams.height == WRAP_CONTENT) {
height = WRAP_CONTENT;
} else {
height = MATCH_PARENT;
}
final PopupDecorView decorView = new PopupDecorView(mContext);
decorView.addView(contentView, MATCH_PARENT, height);
decorView.setClipChildren(false);
decorView.setClipToPadding(false);
return decorView;
}
createDecorView 里面還是沒有直接看出對事件的處理,但是里面有個 PopupDecorView 類,應該在里面了吧,繼續看:
private class PopupDecorView extends FrameLayout {
//......有代碼被省略
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
if (getKeyDispatcherState() == null) {
return super.dispatchKeyEvent(event);
}
if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
final KeyEvent.DispatcherState state = getKeyDispatcherState();
if (state != null) {
state.startTracking(event, this);
}
return true;
} else if (event.getAction() == KeyEvent.ACTION_UP) {
final KeyEvent.DispatcherState state = getKeyDispatcherState();
if (state != null && state.isTracking(event) && !event.isCanceled()) {
dismiss();
return true;
}
}
return super.dispatchKeyEvent(event);
} else {
return super.dispatchKeyEvent(event);
}
}
//......有代碼被省略
@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);
}
}
//......有代碼被省略
}
從上面的代碼中我們看到了KeyEvent.KEYCODE_BACK 和 MotionEvent.ACTION_OUTSIDE,沒錯這里有對返回鍵和其他事件的處理。
至於怎么解決 showAsDropDown 方法彈出位置不對的問題,也就是上文中描述的第二個問題,本文就不貼源碼了,感興趣的可以下載源碼去看看,本文只是提供一種解決問題的思路,希望大家能從源碼中找到解決問題的辦法,這才是作者希望達到的效果。 文章末尾會給出 Android7.0 PopupWindow.java 的 java 文件。
Android7.0引入的新問題
調用 update 方法時,PopupWindow 的 Gravity 會改變,導致位置發生了改變,具體看下圖:
showAtLocation傳入Gravity.Bottom:從屏幕底部對齊彈出

調用update方法更新第5點中彈出PopupWindow,發現PopupWindow的Gravity發生了改變

關於這個問題還有篇文章可以參考, http://www.jianshu.com/p/0df10893bf5b
Android7.0 PopupWindow其他改動點,與Android5.1的對比
主界面

1. PopupWindow高寬都設置為match_parent:7.0(左邊)從屏幕左上角彈出,5.1(右邊)從anchorView下方彈出

2. 寬度wrap_content-高度match_parent:7.0(左邊)從屏幕左上角彈出,5.1(右邊)從anchorView下方彈出

3. 寬度match_parent-高度wrap_content:都從anchorView下方彈出

4. 寬度wrap_content-高度大於anchorView到屏幕底部的距離:7.0與5.1都從anchorView上方彈出,與anchorView左對齊

源碼地址
Github工程地址,收錄了 PopupWindow 相關使用問題:
https://github.com/PopFisher/SmartPopupWindow
Android 7.0 PopupWindow.java 文件:
https://github.com/PopFisher/SmartPopupWindow/blob/master/sourcecode/PopupWindow(7.0).java
總結
Android PopupWindow 這個控件 Google 一直沒有優化好,使用時需要參考我之前的幾篇文章。本文是希望讀者善於從源碼的角度去分析和解決問題,加深自己對源碼的理解,對問題的理解,這樣印象要深刻一些。
本來2017年回來還沒有時間寫寫文章,這篇文章也是巧合,同事在 Android7.0 中發現 PopupWindow 使用上有 bug,所以我就借此機會研究一下,雖然知識點簡單,但是也花費了幾個小時的時間整理出這樣一篇文章。如果讀者覺得有用,別忘記點擊推薦哦,總之也算是開了一個好頭吧,以后還是會堅持每個月寫些文章出來分享。
