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


   接到一個博友的反饋,在屏幕旋轉時調用 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會出現這種情況,理論上還有很多其他的寫法會導致轉屏的時候觸發到那個滾動事件,轉屏這個操作太重了,什么都可能發生。所以個人推測這是一個普遍存在的問題,只是這種使用場景比較少。所以個人有如下建議:

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM