1. 程式人生 > >Android 7.0以上(包含8.0), 或者有虛擬按鍵,popupWindow彈窗位置異常的終極解決方案

Android 7.0以上(包含8.0), 或者有虛擬按鍵,popupWindow彈窗位置異常的終極解決方案

問題描述

前段時間發現Popupwindow在8.0的手機上顯示成全屏了,搜了下發現7.0以上就有這個問題了,好久沒寫Popwindow了,才知道(尷尬)。於是總結了在以下情況可能出問題:

  1. 當設定PopupWindow 的高度為 MATCH_PARENT,呼叫 showAsDropDown(View anchor) 時,在 7.0 之前,會在 anchor 下邊緣到螢幕底部之間顯示 PopupWindow;而在 7.0系統上(包括7.1, 8.0)的 PopupWindow 會佔據整個螢幕(除狀態列之外)。
  2. 當設定PopupWindow 的高度為 WRAP_CONTENT,呼叫 showAsDropDown(View anchor) 時,沒有相容性問題。
  3. 當設定PopupWindow 的高度為自定義的值height,呼叫 showAsDropDown(View anchor)時, 如果 height > anchor 下邊緣與螢幕底部的距離, 則還是會出現7.0以上顯示異常的問題;否則,不會出現該問題。

問題解決

好了,現在我們知道問題出現的情況了,上網搜了很多解決方案,發現都不能完美解決問題,如以下程式碼:

if (Build.VERSION.SDK_INT >= 24) {
     int[] location = new int[2];
     anchor.getLocationOnScreen(
location); // 7.1 版本處理 if (Build.VERSION.SDK_INT == 25) { WindowManager windowManager = (WindowManager) pw.getContentView().getContext().getSystemService(Context.WINDOW_SERVICE); if (windowManager != null) { int screenHeight = windowManager.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); }

或者這種程式碼:

public static void showAsDropDown(final PopupWindow pw, final View anchor, final int xoff, final int yoff) {
    if (Build.VERSION.SDK_INT >= 24) {
        Rect visibleFrame = new Rect();
        anchor.getGlobalVisibleRect(visibleFrame);
        int height = anchor.getResources().getDisplayMetrics().heightPixels - visibleFrame.bottom;
        pw.setHeight(height);
        pw.showAsDropDown(anchor, xoff, yoff);
    } else {
        pw.showAsDropDown(anchor, xoff, yoff);
    }
}

這兩段程式碼的主要問題是,沒有考慮虛擬按鍵的情況,螢幕的高度算的不一定對,或者有虛擬按鍵,虛擬按鍵是否顯示都會影響螢幕的可用高度。所以問題的關鍵是算出螢幕真實的可用高度。

為了方便以後複用,我們重新定義一個PopupWindowCompat繼承自PopupWindow,具體程式碼如下,主要是綜合考慮以上三種情況,和算出螢幕的真實可用高度,註釋已經寫的很清楚了吧,我就省點字了(偷懶)。

獲取螢幕真實總高度:

public static int getScreenHeight(Activity activity) {
    if (activity == null) {
        return 0;
    }
    Display display = activity.getWindowManager().getDefaultDisplay();
    int realHeight = 0;
    if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
        final DisplayMetrics metrics = new DisplayMetrics();
        display.getRealMetrics(metrics);
        realHeight = metrics.heightPixels;
    } else {
        try {
            Method mGetRawH = Display.class.getMethod("getRawHeight");
            realHeight = (Integer) mGetRawH.invoke(display);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    return realHeight;
}

檢測是否具有底部導航欄

private static boolean checkDeviceHasNavigationBar(Activity activity) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
        WindowManager windowManager = activity.getWindowManager();
        Display display = windowManager.getDefaultDisplay();
        DisplayMetrics realDisplayMetrics = new DisplayMetrics();
        display.getRealMetrics(realDisplayMetrics);
        int realHeight = realDisplayMetrics.heightPixels;
        int realWidth = realDisplayMetrics.widthPixels;
        DisplayMetrics displayMetrics = new DisplayMetrics();
        display.getMetrics(displayMetrics);
        int displayHeight = displayMetrics.heightPixels;
        int displayWidth = displayMetrics.widthPixels;
        return (realWidth - displayWidth) > 0 || (realHeight - displayHeight) > 0;
    } else {
        boolean hasNavigationBar = false;
        Resources resources = activity.getResources();
        int id = resources.getIdentifier("config_showNavigationBar", "bool", "android");
        if (id > 0) {
            hasNavigationBar = resources.getBoolean(id);
        }
        try {
            Class systemPropertiesClass = Class.forName("android.os.SystemProperties");
            Method m = systemPropertiesClass.getMethod("get", String.class);
            String navBarOverride = (String) m.invoke(systemPropertiesClass, "qemu.hw.mainkeys");
            if ("1".equals(navBarOverride)) {
                hasNavigationBar = false;
            } else if ("0".equals(navBarOverride)) {
                hasNavigationBar = true;
            }
        } catch (Exception e) {
        }
        return hasNavigationBar;
    }
}

有底部導航欄時,底部導航欄是否可見:

private static boolean isNavigationBarVisible(Activity activity) {
    boolean show = false;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
        Display display = activity.getWindow().getWindowManager().getDefaultDisplay();
        Point point = new Point();
        display.getRealSize(point);
        View decorView = activity.getWindow().getDecorView();
        Configuration conf = activity.getResources().getConfiguration();
        if (Configuration.ORIENTATION_LANDSCAPE == conf.orientation) {
            View contentView = decorView.findViewById(android.R.id.content);
            show = (point.x != contentView.getWidth());
        } else {
            Rect rect = new Rect();
            decorView.getWindowVisibleDisplayFrame(rect);
            show = (rect.bottom != point.y);
        }
    }
    return show;
}

獲取底部導航欄真實高度:

/**
  * 獲取當前底部導航欄高度(隱藏後高度為0)
  *
  * @param activity
  * @return
  */
public static int getCurrentNavigationBarHeight(Activity activity) {
    int navigationBarHeight = 0;
    Resources resources = activity.getResources();
    int resourceId = resources.getIdentifier(isPortrait(activity) ? "navigation_bar_height" : "navigation_bar_height_landscape", "dimen", "android");
    if (resourceId > 0 && checkDeviceHasNavigationBar(activity) && isNavigationBarVisible(activity)) {
        navigationBarHeight = resources.getDimensionPixelSize(resourceId);
    }
    return navigationBarHeight;
}

獲取螢幕可用高度:

/**
  * 獲取可用螢幕高度,排除虛擬鍵
  *
  * @param context 上下文
  * @return 返回高度
  */
public static int getContentHeight(Activity context) {
    int contentHeight = getScreenHeight(context) - getCurrentNavigationBarHeight(context);
    return contentHeight;
}

自定義的PopupWindowCompat的主要程式碼如下:

@Override
public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
    // 7.0 以下或者高度為WRAP_CONTENT, 預設顯示
    if (Build.VERSION.SDK_INT < 24 || getHeight() == ViewGroup.LayoutParams.WRAP_CONTENT) {
        super.showAsDropDown(anchor, xoff, yoff, gravity);
    } else {
        if (getContentView().getContext() instanceof Activity) {
            Activity activity = (Activity) getContentView().getContext();
            int screenHeight;
            // 獲取螢幕真實高度, 減掉虛擬按鍵的高度
            screenHeight = OsUtils.getContentHeight(activity);
            int[] location = new int[2];
            // 獲取控制元件在螢幕的位置
            anchor.getLocationOnScreen(location);
            // 算出popwindow最大高度
            int maxHeight = screenHeight - location[1] - anchor.getHeight();
            // popupwindow  有具體的高度值,但是小於anchor下邊緣與螢幕底部的距離, 正常顯示
            if(getHeight() > 0 && getHeight() < maxHeight){
                super.showAsDropDown(anchor, xoff, yoff, gravity);
            }else {
                // match_parent 或者 popwinddow的具體高度值大於anchor下邊緣與螢幕底部的距離, 都設定為最大可用高度
                setHeight(maxHeight);
                super.showAsDropDown(anchor, xoff, yoff, gravity);
            }

        }
    }
}

好了,完美解決,以後可以愉快的使用Popwindow了,如果想看完整程式碼和例子可以移步https://github.com/wang0826jj/PopupWindowCompat