起因與Bug詳情
前段時間有反饋過來說,在Android7.0中,PopupWindow的展示位置出了問題(最開始以為沒彈出來,后來發(fā)現(xiàn)是位置錯了),通過查谷看歌源搜碼索到了可能的情況和一些文章.
但找到的文章對于問題的表述不太完整,還有一些文章根本就是搞錯了原因(大量文章說位置錯誤是由于PopupWindow的寬高設(shè)置的太大了…醉了),后來結(jié)合源碼和實際的Bug情況看到了具體的詳細原因.
這個Bug的具體情況是:在使用PopupWindow調(diào)用showAtLocation
方法showAsDropDown
方法和update
方法時,如果傳入的Gravity
參數(shù)不為Gravity.START|Gravity.TOP
則Gravity
會被設(shè)置為Gravity.START|Gravity.TOP
,PopupWindow的位置即發(fā)生了改變,可以通過反射來改掉這個Bug,下面是這個Bug的詳細解法
Bug原因
具體問題發(fā)生在computeGravity
方法
private int computeGravity() {
int gravity = Gravity.START | Gravity.TOP;
if (mClipToScreen || mClippingEnabled) {
gravity |= Gravity.DISPLAY_CLIP_VERTICAL;
}
return gravity;
}
可以看到返回的gravity
值,在方法的一開始就被強制設(shè)置為了Gravity.START|Gravity.TOP
,所以我們傳入的參數(shù)并沒有起到任何作用,而這個Bug只有API版本24,Android 7.0的SDK是這樣,無責任猜想可能是Android系統(tǒng)開發(fā)的某位大哥,在寫分屏相關(guān)的UI代碼的時候,出于測試方便或者什么的,直接將這里寫死了╮(╯_╰)╭
不過我們可以通過反射來改成正確的代碼.
如何解決
首先查看一下都哪里用到了這個computeGravity
,然后通過搜索看到,分別是1418行的createPopupLayoutParams
方法里出現(xiàn)了
p.gravity = computeGravity();
以及在1096行和2081行的兩個update
方法內(nèi)部出現(xiàn)了同樣的一行代碼
final int newGravity = computeGravity();
由于懶在實際使用中并沒有通過update
來更新PopupWindow的位置,并且也只是用了showAtLocation
,所以暫沒有對update
和showAsDropDown
進行反射來重寫方法,只重寫了showAtLocation
,理論上講,createPopupLayoutParams
同時被showAtLocation
和showAsDropDown
方法用到了,且這是一個私有方法,所以需要分別重寫showAtLocation
和showAsDropDown
方法以及兩個update
方法
這里提供出一個修改方案,import
和package
已去掉,如果要使用請自行添加,這里特別說明一下TransitionManager.endTransitions(mDecorView);
,這行代碼在IDE中很可能會報錯標紅,原因是使用了高版本API (Android M SDK 23)而沒有進行版本判斷,但實際上可以不用理會,因為目前這個Bug只有7.0這一個版本出現(xiàn)了,所以我在方法的最開始進行了當前Android版本的判斷,如果不是版本號24的7.0版本,直接執(zhí)行super.showAtLocation
調(diào)用原PopupWindow的方法,之后return
掉了這個方法,畢竟反射也是會帶來額外一丟丟的性能和內(nèi)存占用.如果不想讓他報紅可以選擇改成if else
的形式.
public class NougatPopupWindow extends PopupWindow {
public NougatPopupWindow(View contentView, int width, int height, boolean focusable) {
super(contentView, width, height, focusable);
}
@Override
public void showAtLocation(View parent, int gravity, int x, int y) {
if (Build.VERSION.SDK_INT != 24) {
super.showAtLocation(parent, gravity, x, y);
return;
}
Object obj = getParam("mContentView");
View mContentView = (View) obj;
if (isShowing() || mContentView == null) {
return;
}
obj = getParam("mDecorView");
ViewGroup mDecorView = (ViewGroup) obj;
//RequireAPI M but if SDK_INT != N,super.showAtLocation and returned;
TransitionManager.endTransitions(mDecorView);
execMethod("detachFromAnchor", new Class[]{}, new Object[]{});
setParam("mIsShowing", true);
setParam("mIsDropdown", false);
obj = execMethod("createPopupLayoutParams", new Class[]{IBinder.class}, new Object[]{parent.getWindowToken()});
final WindowManager.LayoutParams p = (WindowManager.LayoutParams) obj;
p.gravity = computeGravity(gravity);
execMethod("preparePopup",new Class[]{WindowManager.LayoutParams.class},new Object[]{p});
if (gravity != Gravity.NO_GRAVITY) {
p.gravity = gravity;
}
p.x = x;
p.y = y;
execMethod("invokePopup",new Class[]{WindowManager.LayoutParams.class},new Object[]{p});
}
private Object getParam(String paramName) {
if (TextUtils.isEmpty(paramName)) {
return null;
}
try {
Field field = PopupWindow.class.getDeclaredField(paramName);
field.setAccessible(true);
return field.get(this);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private void setParam(String paramName, Object obj) {
if (TextUtils.isEmpty(paramName)) {
return;
}
try {
Field field = PopupWindow.class.getDeclaredField(paramName);
field.setAccessible(true);
field.set(this, obj);
} catch (Exception e) {
e.printStackTrace();
}
}
private Object execMethod(String methodName, Class[] cls, Object[] args) {
if (TextUtils.isEmpty(methodName)) {
return null;
}
try {
Method method = getMethod(PopupWindow.class, methodName, cls);
method.setAccessible(true);
return method.invoke(this, args);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private Method getMethod(Class clazz, String methodName,
final Class[] classes) throws NoSuchMethodException {
Method method = null;
try {
method = clazz.getDeclaredMethod(methodName, classes);
} catch (NoSuchMethodException e) {
try {
method = clazz.getMethod(methodName, classes);
} catch (NoSuchMethodException ex) {
if (clazz.getSuperclass() == null) {
return method;
} else {
method = getMethod(clazz.getSuperclass(), methodName,
classes);
}
}
}
return method;
}
private int computeGravity(int mGravity) {
setParam("mGravity", mGravity);
int gravity = mGravity == Gravity.NO_GRAVITY ? Gravity.START | Gravity.TOP : mGravity;
Object obj = getParam("mIsDropdown");
boolean mIsDropdown = (boolean) obj;
obj = getParam("mClipToScreen");
boolean mClipToScreen = (boolean) obj;
obj = getParam("mClippingEnabled");
boolean mClippingEnabled = (boolean) obj;
if (mIsDropdown && (mClipToScreen || mClippingEnabled)) {
gravity |= Gravity.DISPLAY_CLIP_VERTICAL;
}
return gravity;
}
}
文中部分內(nèi)容參考了作者Kinva的文章,鏈接:http://www.reibang.com/p/0df10893bf5b