屏幕旋轉時調用PopupWindow update方法更新位置失效的問題及解決方案

   接到一個博友的反饋,在屏幕旋轉時調用 PopupWindow 的 update 方法失效。使用場景以下:在一個 Activity 中監聽屏幕旋轉事件,在Activity主佈局文件中有個按鈕點擊彈出一個 PopupWindow,另外在主佈局文件中有個
ListView。測試結果發現:若是 ListView 設置爲可見(visibile)的話,屏幕旋轉時調用的 update 方法無效,若是 ListView 設置爲不可見(gone)或者直接刪除的話,屏幕旋轉時調用的update方法就生效。下面先展現兩種狀況的效果圖對比。android

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 源碼所有下載下來吧,方便直接調試和查看)。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

萬能的Debug,找準位置打好斷點,開始調試

  先把彈窗彈出來,而後打上斷點,綁定調試的進程,轉屏以後斷點就過來了,以下所示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

  1. 能夠想辦法把它註冊的那個 OnScrollChangedLister 反註冊掉
  2. 轉屏的時候延遲一下,目的是等它的 OnScrollChangedLister 回調走完,咱們再走一次把正確的值覆蓋掉,可是延遲時間很差控制。還能夠本身也給那個 anchorView 的 ViewTreeObserver 添加一個 OnScrollChangedLister,準確的監聽到這個回調以後從新調用update方法設置正確的值,不過這個要和屏幕旋轉回調作好配合。
  3. 繞過這個坑,用其餘的方式實現

第二種方法比較經常使用,代碼以下

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/SmartPopupWindowdebug

個人博客即將搬運同步至騰訊雲+社區,邀請你們一同入駐:https://cloud.tencent.com/developer/support-plan

相關文章
相關標籤/搜索