美文网首页Android TechAndroid安卓
PopupWindow 在 Android N(7.0) 的兼容

PopupWindow 在 Android N(7.0) 的兼容

作者: Kinva | 来源:发表于2016-10-14 13:03 被阅读8474次

    老早QA就提了个bug,说我们的popupWindow在android N (7.0)系统展示不对。
    然后我今天有空就把这个bug修了,没明白google为啥这次这样改PopupWindow,可能是他们的bug,下面详细看看这个是什么bug。

    兼容性现象

    popupWindow设置了居中或者底部对齐,但是在7.0机器是跑到顶部。
    很明显这个bug是和我们设置了Gravity有关。
    展示popupWindow的函数有两个,showAtLocation 和 update。
    重点看了那两个函数的API 24 和 API 23 的区别。

    源码分析

    通过源码分析发现,在update函数里有一个和gravity相关的地方,很明显是个bug。

    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;
        }
    
        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;
        }
    
        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;
        }
    
        final int newGravity = computeGravity();
        if (newGravity != p.gravity) {
            p.gravity = newGravity;
            update = true;
        }
    
        int newAccessibilityIdOfAnchor =
                (mAnchor != null) ? mAnchor.get().getAccessibilityViewId() : -1;
        if (newAccessibilityIdOfAnchor != p.accessibilityIdOfAnchor) {
            p.accessibilityIdOfAnchor = newAccessibilityIdOfAnchor;
            update = true;
        }
    
        if (update) {
            setLayoutDirectionFromAnchor();
            mWindowManager.updateViewLayout(mDecorView, p);
        }
    }
    

    有个 computeGravity 函数,我们再看看

    private int computeGravity() {
        int gravity = Gravity.START | Gravity.TOP;
        if (mClipToScreen || mClippingEnabled) {
            gravity |= Gravity.DISPLAY_CLIP_VERTICAL;
        }
        return gravity;
    }
    

    噗,我们发现,我们之前设置的gravity被这个函数执行之后覆盖了。。
    我忽然觉得估计是Google的大牛自测的时候写死变量好调试,最后发布的时候忘记改了。。
    问题定位到了,符合2个条件:

    • popupWindow 在 show 的时候定义了 Gravity,不是 Gravity.START | Gravity.TOP。
    • PopupWindow 调用了update。

    解决方案

    两种方案

    • 不调用 update 方法即可
    • 重写 update 方法
    • 最简单是 dismiss,再调show
    • 反射方法 把gravity那一段去掉

    我提供反射方法的办法
    但是我这个方法有缺陷的,反射的办法会遇到Google Api如果把变量名改了,那就直接无效了。
    但是要解决这种系统bug也只能做API适配了。

    package cc.kinva.widget;
    
    import android.content.Context;
    import android.os.Build;
    import android.text.TextUtils;
    import android.util.AttributeSet;
    import android.view.View;
    import android.view.WindowManager;
    import android.widget.PopupWindow;
    
    import java.lang.reflect.Field;
    import java.lang.reflect.Method;
    
    /**
     * Created by Kinva on 16/9/23.
     */
    public class FixedPopupWindow extends PopupWindow {
        public FixedPopupWindow(Context context) {
            super(context);
        }
    
        public FixedPopupWindow(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        public FixedPopupWindow(Context context, AttributeSet attrs, int defStyle) {
            super(context, attrs, defStyle);
        }
    
        public FixedPopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
            super(context, attrs, defStyleAttr, defStyleRes);
        }
    
        public FixedPopupWindow(View contentView) {
            super(contentView);
        }
    
        public FixedPopupWindow() {
            super();
        }
    
        public FixedPopupWindow(int width, int height) {
            super(width, height);
        }
    
        public FixedPopupWindow(View contentView, int width, int height, boolean focusable) {
            super(contentView, width, height, focusable);
        }
    
        public FixedPopupWindow(View contentView, int width, int height) {
            super(contentView, width, height);
        }
    
        @Override
        public void update(int x, int y, int width, int height, boolean force) {
            if (Build.VERSION.SDK_INT < 24) {
                super.update(x, y, width, height, force);
                return;
            }
            if (width >= 0) {
                setParam("mLastWidth", width);
                setWidth(width);
            }
    
            if (height >= 0) {
                setParam("mLastHeight", height);
                setHeight(height);
            }
    
            Object obj = getParam("mContentView");
            View mContentView = null;
            if (obj instanceof View) {
                mContentView = (View) obj;
            }
            if (!isShowing() || mContentView == null) {
                return;
            }
    
            obj = getParam("mDecorView");
            View mDecorView = null;
            if (obj instanceof View) {
                mDecorView = (View) obj;
            }
            final WindowManager.LayoutParams p =
                    (WindowManager.LayoutParams) mDecorView.getLayoutParams();
    
            boolean update = force;
    
            obj = getParam("mWidthMode");
            int mWidthMode = obj != null ? (Integer) obj : 0;
            obj = getParam("mLastWidth");
            int mLastWidth = obj != null ? (Integer) obj : 0;
    
            final int finalWidth = mWidthMode < 0 ? mWidthMode : mLastWidth;
            if (width != -1 && p.width != finalWidth) {
                p.width = finalWidth;
                setParam("mLastWidth", finalWidth);
                update = true;
            }
    
            obj = getParam("mHeightMode");
            int mHeightMode = obj != null ? (Integer) obj : 0;
            obj = getParam("mLastHeight");
            int mLastHeight = obj != null ? (Integer) obj : 0;
            final int finalHeight = mHeightMode < 0 ? mHeightMode : mLastHeight;
            if (height != -1 && p.height != finalHeight) {
                p.height = finalHeight;
                setParam("mLastHeight", finalHeight);
                update = true;
            }
    
            if (p.x != x) {
                p.x = x;
                update = true;
            }
    
            if (p.y != y) {
                p.y = y;
                update = true;
            }
    
            obj = execMethod("computeAnimationResource");
            final int newAnim = obj == null ? 0 : (Integer) obj;
            if (newAnim != p.windowAnimations) {
                p.windowAnimations = newAnim;
                update = true;
            }
    
            obj = execMethod("computeFlags", new Class[]{int.class}, new Object[]{p.flags});
            final int newFlags = obj == null ? 0 : (Integer) obj;
            if (newFlags != p.flags) {
                p.flags = newFlags;
                update = true;
            }
    
            if (update) {
                execMethod("setLayoutDirectionFromAnchor");
                obj = getParam("mWindowManager");
                WindowManager mWindowManager = obj instanceof WindowManager ? (WindowManager) obj : null;
                if (mWindowManager != null) {
                    mWindowManager.updateViewLayout(mDecorView, p);
                }
            }
        }
    
        /**
         * 反射获取对象
         * @param paramName
         * @return
         */
        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;
        }
    
        /**
         * 反射赋值对象
         * @param paramName
         * @param obj
         */
        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();
            }
        }
    
        /**
         * 反射执行方法
         * @param methodName
         * @param args
         * @return
         */
        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;
        }
    
        /**
         * 利用递归找一个类的指定方法,如果找不到,去父亲里面找直到最上层Object对象为止。
         *
         * @param clazz
         *            目标类
         * @param methodName
         *            方法名
         * @param classes
         *            方法参数类型数组
         * @return 方法对象
         * @throws Exception
         */
        private Method getMethod(Class clazz, String methodName,
                                       final Class[] classes) throws Exception {
            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;
        }
    }
    
    

    欢迎围观 我的博客

    相关文章

      网友评论

      • 94cbc921fec7:太赞了,成功解决!
      • 古城一:楼主你好,试了下注释掉update这个方法确实能在底部弹出,不过我有点担心会适用所有android版本吗,因为是个新手,不是很明白,所以这点想请问下楼主
        古城一:@Kinva 我尝试过判断版本24,但是没有成功,也许哪里写错了。但着急版本更新,就直接去掉了update的代码,目前没发现什么问题。还有待观察
        Kinva:实际这个bug只存在24,所以判断好版本,其他版本完全适配的。
      • vincent210:使用反射有点麻烦,我找到一个解决方法:重写showAsDropDown(){ if (Build.VERSION.SDK_INT >= 24) {
        Rect rect = new Rect();
        anchor.getGlobalVisibleRect(rect);
        int h = anchor.getResources().getDisplayMetrics().heightPixels - rect.bottom;
        setHeight(h);
        }
        super.showAsDropDown(anchor);
        }
        亲测试,解决问题
        vincent210:@朴文 原理也不太清楚,但是好像就是把popupwindow的高设为定值了,通过获得屏幕的高度减去View的低所在屏幕的位置固定popupwindow的高!
        朴文:这个原理是什么?
      • ramblejoy:好坑 刚发了版本 发现7.0有这问题~~~
      • JokAr_:我的buildSdk是25,由于某些原因无法改为24,找不到execMethod("setLayoutDirectionFromAnchor");该怎么解决;非常感谢
        Ecge:我的 buildsdk为22,execMethod("setLayoutDirectionFromAnchor") 这个方法找不到,有什么解决方案吗?多谢。
        Kinva:加上判断,如果你用API25编译的话,那你就直接使用@TargetApi(24) 就行。你查查相关资料就可以了。
      • smile三七:execMethod("setLayoutDirectionFromAnchor"); 这个方法怎么没找到啊?
        smile三七:@鲨鱼的泪 我这样改就正常了。
        if (Build.VERSION.SDK_INT == Build.VERSION_CODES.N)
        {
        mPopupWindow.showAtLocation(view, Gravity.NO_GRAVITY, 0, 0);
        }
        e1623f5445d8:你好,请问下解决了吗?我也是这样
        Kinva:如果你看源码,确定API版本是24哈
      • jizeguo:7.1刚出的时候试了下虚拟机,发现修复了,本来想不管了,结果发现华为之类的国内rom并没有更新到7.1,昨天看到百度网盘的那个下拉框修复了。。就出来找找。。果然还是得靠会去阅读源码的大神,灰常感谢你的分享~~
        16028318fd4d:@翻滚吧咸鱼 虽然说7.1.1修复了7.0出现的这个问题,但是7.1.1还是和7.0之前的不一样,必须设置popupWindow的高为wrap_content。
        另外推荐一个轮子
        https://github.com/razerdp/BasePopup
        283de343f6aa:我7.1.1还是有这个bug啊,难道是因为我刷机的原因。
      • 风的微笑:7.1的SDK已经修复了,版本判断那里可以改为!=24了
        Kinva:谢谢提醒~
      • YoungTr:今天刚好遇到这个问题

      本文标题:PopupWindow 在 Android N(7.0) 的兼容

      本文链接:https://www.haomeiwen.com/subject/qnigyttx.html