美文网首页我的Andorid 收藏
Android PopupWindow 7.0之后出现的问题

Android PopupWindow 7.0之后出现的问题

作者: 庞哈哈哈12138 | 来源:发表于2017-12-29 17:32 被阅读31次

    缘由

    之前老项目一直有个问题,就是弹窗会顶在屏幕最上面,一直以为是特殊机型适配的问题,一拖再拖没有解决,后来测试实在忍不了,上网上查一查,发现这个bug出现好久了,是安卓7.0之后源码发生改变导致的

    兼容性现象

    popuwindow设置 showAsDropDown(view)并没有在某个view的下面
    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个条件:

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

    下面是比较实用的方法,通过获取要展示的view的坐标,然后计算屏幕减去坐标减去view高度,减去偏移量就得到 popuwindow展示的位置,有效可用

    /**
        * android 7.0之后的坑
        */
        public static void showAsDropDownFor_N(PopupWindow pw, View anchor, int xoff, int yoff) {
            if (Build.VERSION.SDK_INT >= 24) {
                int[] location = new int[2];
                anchor.getLocationOnScreen(location);
                // 7.1 版本处理
                if (Build.VERSION.SDK_INT == 25) {
                    //【note!】Gets the screen height without the virtual key
                    WindowManager wm = (WindowManager) pw.getContentView().getContext().getSystemService(Context.WINDOW_SERVICE);
                    int screenHeight = wm.getDefaultDisplay().getHeight();
                    /*
                    /*
                     * PopupWindow height for match_parent,
                     * will occupy the entire screen, it needs to do special treatment in Android 7.1
                    */
                    pw.setHeight(screenHeight - location[1] - anchor.getHeight() - yoff);
                }
                pw.showAtLocation(anchor, Gravity.NO_GRAVITY, xoff, location[1] + anchor.getHeight() + yoff);
            } else {
                pw.showAsDropDown(anchor, xoff, yoff);
            }
        }
    

    还有就是通过反射的方式改变源码
    但是我这个方法有缺陷的,反射的办法会遇到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 pang on 17/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;
        }
    }
    

    相关文章

      网友评论

        本文标题:Android PopupWindow 7.0之后出现的问题

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