接到一個博友的反饋,在屏幕旋轉時調用 PopupWindow 的 update 方法失效。使用場景以下:在一個 Activity 中監聽屏幕旋轉事件,在Activity主佈局文件中有個按鈕點擊彈出一個 PopupWindow,另外在主佈局文件中有個
ListView。測試結果發現:若是 ListView 設置爲可見(visibile)的話,屏幕旋轉時調用的 update 方法無效,若是 ListView 設置爲不可見(gone)或者直接刪除的話,屏幕旋轉時調用的update方法就生效。下面先展現兩種狀況的效果圖對比。android
<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>
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 源碼所有下載下來吧,方便直接調試和查看)。git
咱們以前發現的現象是 update 方法失效,準確的說是update的前兩個參數 x,y 座標失效,高度和寬度是能夠的。那咱們就看開 update 方法的前面兩個參數怎麼使用的。github
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 方法開頭打個斷點,看看代碼怎麼執行的。api
先把彈窗彈出來,而後打上斷點,綁定調試的進程,轉屏以後斷點就過來了,以下所示ide
而後單步調試(AS的F8)完看看各個地方是否是正常的流程。這裏會發現整個 update 方法都正常,那咱們走完它吧(AS的F9快捷鍵),奇怪的時候發現update又一次調用進來了,這一次參數有點不同,看調用堆棧是從一個 onScrollChanged 方法調用過來的,並且參數x,y已經變了,高度寬度仍是-1沒變(到這裏問題已經找到了,就是 update 被其餘地方調用把咱們設置的值覆蓋了,不過都到這裏了,確定想知道爲何吧,繼續看吧)。源碼分析
從上面的調用堆棧,找到了 onScrollChanged 方法,咱們查找一下看看,果真不出所料,這個方法改變了 x,y 參數,具體修改的地方是 findDropDownPosition 方法中,想知道怎麼改的細節,能夠繼續斷點調試。佈局
繼續尋找調用源頭,mOnScrollChangedListener 的 onScrollChanged 誰調用?測試
最後經過源碼看到,在調用 showAsDropDown 方法的時候,會調用 registerForScrollChanged 方法,此方法會拿到 anchorView 的 ViewTreeObserver 並添加一個全局的滾動監聽事件。至於爲何有 ListView 的時候會觸發到這個滾動事件,這個具體也不知道,不過從這裏能夠推測,可能不只是ListView會出現這種狀況,理論上還有不少其餘的寫法會致使轉屏的時候觸發到那個滾動事件,轉屏這個操做過重了,什麼均可能發生。因此我的推測這是一個廣泛存在的問題,只是這種使用場景比較少。因此我的有以下建議:spa
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); } } }
https://github.com/PopFisher/SmartPopupWindowdebug
個人博客即將搬運同步至騰訊雲+社區,邀請你們一同入駐:https://cloud.tencent.com/developer/support-plan