接到一個博友的反饋,在屏幕旋轉時調用 PopupWindow 的 update 方法失效。使用場景如下:在一個 Activity 中監聽屏幕旋轉事件,在Activity主布局文件中有個按鈕點擊彈出一個 PopupWindow,另外在主布局文件中有個
ListView。測試結果發現:如果 ListView 設置為可見(visibile)的話,屏幕旋轉時調用的 update 方法無效,如果 ListView 設置為不可見(gone)或者直接刪除的話,屏幕旋轉時調用的update方法就生效。下面先展示兩種情況的效果圖對比。
ListView不可見的情況(update生效,效果符合預期)
橫屏效果圖如下
豎屏效果圖如下
ListView可見的情況(update不生效,效果不符合預期)
橫屏效果圖如下
豎屏效果圖如下
看了上面的效果圖,再來看看簡單的布局實現和Activity代碼實現
Activity主布局文件如下
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="popup.popfisher.com.smartpopupwindow.PopupWindowMainActivity">
<!-- 這個ListView的顯示隱藏直接影響到PopupWindow在屏幕旋轉的時候update方法是否生效 -->
<ListView
android:id="@+id/listview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:cacheColorHint="@android:color/transparent"
android:visibility="visible" />
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="監聽屏幕旋轉並調用PopupWindow的update方法,發現如果ListView可見的時候,update方法不生效,ListView不可見的時候update生效" />
<Button
android:id="@+id/anchor_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/textView1"
android:layout_below="@+id/textView1"
android:layout_marginLeft="44dp"
android:layout_marginTop="40dp"
android:text="點擊彈出PopupWindow" />
<LinearLayout
android:id="@+id/btnListLayout"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="@android:color/transparent"
android:orientation="horizontal"></LinearLayout>
</RelativeLayout>
Activity代碼如下(onConfigurationChanged中根據屏幕方向調用update方法)
public class ScreenChangeUpdatePopupActivity extends Activity {
private Button mAnchorBtn;
private PopupWindow mPopupWindow = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_screen_change_update_popup);
mAnchorBtn = (Button) findViewById(R.id.anchor_button);
mAnchorBtn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View arg0) {
View contentView = LayoutInflater.from(getApplicationContext()).
inflate(R.layout.popup_content_layout, null);
mPopupWindow = new PopupWindow(contentView,
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
mPopupWindow.setFocusable(true);
mPopupWindow.setOutsideTouchable(true);
mPopupWindow.setBackgroundDrawable(new ColorDrawable());
mPopupWindow.showAsDropDown(mAnchorBtn, 0, 0);
}
});
}
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// 轉屏時調用update方法更新位置,現象如下
// 1. 如果R.layout.activity_screen_change_update_popup中的ListView可見,則update無效
// 2. 如果R.layout.activity_screen_change_update_popup中的ListView不可見,則update有效
final int typeScreen = newConfig.orientation;
if (typeScreen == ActivityInfo.SCREEN_ORIENTATION_USER
|| typeScreen == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
mPopupWindow.update(0, 0, -1, -1);
} else if (typeScreen == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
mPopupWindow.update(0, 800, -1, -1);
}
}
}
效果圖也看了,代碼也看了,感覺代碼本身沒什么毛病,引起這個問題的導火索卻是一個ListView,怎么辦?當然一開始肯定要不停的嘗試新的寫法,看看是不是布局文件本身有什么問題。如果怎么嘗試都解決不了的時候,這個時候可能已經踩到系統的坑了,可是怎么確定?去看看源碼,然后調試一下看看。首先源碼要確定是哪個版本的,發現這個問題的 Android 版本是6.0(其實這個是個普遍的問題,應該不是特有的,看后面的源碼分析),那就找個api = 23的(平時空閑的時候再 Android studio 上把各種版本的 api 源碼全部下載下來吧,方便直接調試和查看)。
准備好源碼和調試環境之后,准備先看下源碼(從哪兒開始看?)
我們之前發現的現象是 update 方法失效,准確的說是update的前兩個參數 x,y 坐標失效,高度和寬度是可以的。那我們就看開 update 方法的前面兩個參數怎么使用的。
public void update(int x, int y, int width, int height, boolean force) {
if (width >= 0) {
mLastWidth = width;
setWidth(width);
}
if (height >= 0) {
mLastHeight = height;
setHeight(height);
}
if (!isShowing() || mContentView == null) {
return;
}
// 這里拿到了 mDecorView 的布局參數 WindowManager.LayoutParams p
final WindowManager.LayoutParams p =
(WindowManager.LayoutParams) mDecorView.getLayoutParams();
boolean update = force;
final int finalWidth = mWidthMode < 0 ? mWidthMode : mLastWidth;
if (width != -1 && p.width != finalWidth) {
p.width = mLastWidth = finalWidth;
update = true;
}
final int finalHeight = mHeightMode < 0 ? mHeightMode : mLastHeight;
if (height != -1 && p.height != finalHeight) {
p.height = mLastHeight = finalHeight;
update = true;
}
// 這里把x,y分別賦值給 WindowManager.LayoutParams p
if (p.x != x) {
p.x = x;
update = true;
}
if (p.y != y) {
p.y = y;
update = true;
}
final int newAnim = computeAnimationResource();
if (newAnim != p.windowAnimations) {
p.windowAnimations = newAnim;
update = true;
}
final int newFlags = computeFlags(p.flags);
if (newFlags != p.flags) {
p.flags = newFlags;
update = true;
}
if (update) {
setLayoutDirectionFromAnchor();
// 這里把 WindowManager.LayoutParams p 設置給了 mDecorView
mWindowManager.updateViewLayout(mDecorView, p);
}
}
里面的幾個注釋是本人加的,仔細看這個方法好像沒什么毛病。但是這個時候還是要堅信代碼里面存在真理,它不會騙人。這里其實可以靠猜,是不是可能存在調用了多次update,本來設置好的又被其他地方調用update給覆蓋了。但是猜是靠經驗的,一般不好猜,還是笨方法吧,在 update 方法開頭打個斷點,看看代碼怎么執行的。
萬能的Debug,找准位置打好斷點,開始調試
先把彈窗彈出來,然后打上斷點,綁定調試的進程,轉屏之后斷點就過來了,如下所示
然后單步調試(AS的F8)完看看各個地方是不是正常的流程。這里會發現整個 update 方法都正常,那我們走完它吧(AS的F9快捷鍵),奇怪的時候發現update又一次調用進來了,這一次參數有點不一樣,看調用堆棧是從一個 onScrollChanged 方法調用過來的,而且參數x,y已經變了,高度寬度還是-1沒變(到這里問題已經找到了,就是 update 被其他地方調用把我們設置的值覆蓋了,不過都到這里了,肯定想知道為什么吧,繼續看吧)。
從上面的調用堆棧,找到了 onScrollChanged 方法,我們查找一下看看,果然不出所料,這個方法改變了 x,y 參數,具體修改的地方是 findDropDownPosition 方法中,想知道怎么改的細節,可以繼續斷點調試。
繼續尋找調用源頭,mOnScrollChangedListener 的 onScrollChanged 誰調用?
源碼分析找到原因了,有什么解決方案呢?
最后通過源碼看到,在調用 showAsDropDown 方法的時候,會調用 registerForScrollChanged 方法,此方法會拿到 anchorView 的 ViewTreeObserver 並添加一個全局的滾動監聽事件。至於為什么有 ListView 的時候會觸發到這個滾動事件,這個具體也不知道,不過從這里可以推測,可能不僅是ListView會出現這種情況,理論上還有很多其他的寫法會導致轉屏的時候觸發到那個滾動事件,轉屏這個操作太重了,什么都可能發生。所以個人推測這是一個普遍存在的問題,只是這種使用場景比較少。所以個人有如下建議:
- 可以想辦法把它注冊的那個 OnScrollChangedLister 反注冊掉
- 轉屏的時候延遲一下,目的是等它的 OnScrollChangedLister 回調走完,我們再走一次把正確的值覆蓋掉,但是延遲時間不好控制。還可以自己也給那個 anchorView 的 ViewTreeObserver 添加一個 OnScrollChangedLister,准確的監聽到這個回調之后重新調用update方法設置正確的值,不過這個要和屏幕旋轉回調做好配合。
- 繞過這個坑,用其他的方式實現
第二種方法比較常用,代碼如下
public class ScreenChangeUpdatePopupActivity extends Activity {
private Button mAnchorBtn;
private PopupWindow mPopupWindow = null;
private int mCurOrientation = -1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_screen_change_update_popup);
mAnchorBtn = (Button) findViewById(R.id.anchor_button);
mAnchorBtn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View arg0) {
View contentView = LayoutInflater.from(getApplicationContext()).
inflate(R.layout.popup_content_layout, null);
mPopupWindow = new PopupWindow(contentView,
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
mPopupWindow.setFocusable(true);
mPopupWindow.setOutsideTouchable(true);
mPopupWindow.setBackgroundDrawable(new ColorDrawable());
mPopupWindow.showAsDropDown(mAnchorBtn, 0, 0);
// showAsDropDown里面注冊了一個OnScrollChangedListener,我們自己也注冊一個OnScrollChangedListener
// 但是要在它的后面,這樣系統回調的時候會先做完它的再做我們自己的,就可以用我們自己正確的值覆蓋掉它的
initViewListener();
}
});
}
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mCurOrientation = newConfig.orientation;
}
private void initViewListener() {
mAnchorBtn.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
@Override
public void onScrollChanged() {
if (mPopupWindow == null || !mPopupWindow.isShowing()) {
return;
}
updatePopupPos();
}
});
}
private void updatePopupPos() {
if (mCurOrientation == ActivityInfo.SCREEN_ORIENTATION_USER
|| mCurOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
mPopupWindow.update(0, 0, -1, -1);
} else if (mCurOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
mPopupWindow.update(0, 800, -1, -1);
}
}
}
Github項目地址
https://github.com/PopFisher/SmartPopupWindow
我的博客即將搬運同步至騰訊雲+社區,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan