接到一个博友的反馈,在屏幕旋转时调用 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