Android PopupWindow 7.0之后出現(xiàn)的問題

緣由

之前老項目一直有個問題阅仔,就是彈窗會頂在屏幕最上面,一直以為是特殊機(jī)型適配的問題台谊,一拖再拖沒有解決蓉媳,后來測試實在忍不了,上網(wǎng)上查一查锅铅,發(fā)現(xiàn)這個bug出現(xiàn)好久了酪呻,是安卓7.0之后源碼發(fā)生改變導(dǎo)致的

兼容性現(xiàn)象

popuwindow設(shè)置 showAsDropDown(view)并沒有在某個view的下面
popupWindow設(shè)置了居中或者底部對齊,但是在7.0機(jī)器是跑到頂部盐须。
很明顯這個bug是和我們設(shè)置了Gravity有關(guān)玩荠。
展示popupWindow的函數(shù)有兩個,showAtLocation 和 update贼邓。
重點看了那兩個函數(shù)的API 24 和 API 23 的區(qū)別阶冈。

源碼分析

通過源碼分析發(fā)現(xiàn),在update函數(shù)里有一個和gravity相關(guān)的地方塑径,很明顯是個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 函數(shù),我們再看看

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

噗统舀,我們發(fā)現(xiàn)匆骗,我們之前設(shè)置的gravity被這個函數(shù)執(zhí)行之后覆蓋了劳景。。
我忽然覺得估計是Google的大牛自測的時候?qū)懰雷兞亢谜{(diào)試碉就,最后發(fā)布的時候忘記改了盟广。。
問題定位到了铝噩,符合2個條件:

1:popupWindow 在 show 的時候定義了 Gravity衡蚂,不是 Gravity.START | Gravity.TOP。
2:PopupWindow 調(diào)用了update骏庸。

下面是比較實用的方法毛甲,通過獲取要展示的view的坐標(biāo),然后計算屏幕減去坐標(biāo)減去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如果把變量名改了一姿,那就直接無效了七咧。
但是要解決這種系統(tǒng)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();
        }
    }

    /**
     * 反射執(zhí)行方法
     * @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
     *            目標(biāo)類
     * @param methodName
     *            方法名
     * @param classes
     *            方法參數(shù)類型數(shù)組
     * @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;
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蛉顽,一起剝皮案震驚了整個濱河市蝗砾,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌携冤,老刑警劉巖悼粮,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異曾棕,居然都是意外死亡扣猫,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進(jìn)店門翘地,熙熙樓的掌柜王于貴愁眉苦臉地迎上來申尤,“玉大人,你說我怎么就攤上這事子眶∑倌” “怎么了?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵臭杰,是天一觀的道長粤咪。 經(jīng)常有香客問我,道長渴杆,這世上最難降的妖魔是什么寥枝? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任宪塔,我火速辦了婚禮,結(jié)果婚禮上囊拜,老公的妹妹穿的比我還像新娘某筐。我一直安慰自己,他們只是感情好冠跷,可當(dāng)我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布南誊。 她就那樣靜靜地躺著,像睡著了一般蜜托。 火紅的嫁衣襯著肌膚如雪抄囚。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天橄务,我揣著相機(jī)與錄音幔托,去河邊找鬼。 笑死蜂挪,一個胖子當(dāng)著我的面吹牛重挑,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播棠涮,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼谬哀,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了严肪?” 一聲冷哼從身側(cè)響起玻粪,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤莫绣,失蹤者是張志新(化名)和其女友劉穎所禀,沒想到半個月后牍戚,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡结窘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了充蓝。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片隧枫。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖谓苟,靈堂內(nèi)的尸體忽然破棺而出官脓,到底是詐尸還是另有隱情,我是刑警寧澤涝焙,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布卑笨,位于F島的核電站,受9級特大地震影響仑撞,放射性物質(zhì)發(fā)生泄漏赤兴。R本人自食惡果不足惜妖滔,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望桶良。 院中可真熱鬧座舍,春花似錦、人聲如沸陨帆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽疲牵。三九已至承二,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間瑰步,已是汗流浹背矢洲。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留缩焦,地道東北人读虏。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像袁滥,于是被迫代替她去往敵國和親盖桥。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,086評論 2 355

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,185評論 25 707
  • 老早QA就提了個bug题翻,說我們的popupWindow在android N (7.0)系統(tǒng)展示不對揩徊。然后我今天有空...
    Kinva閱讀 13,117評論 14 30
  • ¥開啟¥ 【iAPP實現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個線程,因...
    小菜c閱讀 6,426評論 0 17
  • 在Android中彈出式菜單(以下稱彈窗)是使用十分廣泛一種菜單呈現(xiàn)的方式嵌赠,彈窗為用戶交互提供了便利塑荒。關(guān)于彈窗的實...
    OzanShareing閱讀 3,118評論 2 10
  • 20170809 Day11 背,硬拉6組姜挺,哈利波特6組齿税,爽翻。 平時需要做的是多聯(lián)系背炊豪,腹肌凌箕,平板支撐。 201...
    阿倫影子閱讀 187評論 0 0