劉海屏適配

一、簡述

Android官方9.0劉海屏的適配策略是:如果非全屏模式(有狀態欄),則app不受劉海屏的影響,劉海屏的高就是狀態欄的高;如果全屏模式,app未適配劉海屏,系統會對界面做特殊處理,豎屏向下移動,橫屏向右移動。

二、實現

必須在setContentView方法前調用

 requestWindowFeature(Window.FEATURE_NO_TITLE);
        Window window = getWindow();
        window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);

這樣一來,就成了一條黑邊。我們需要將內容區域眼神到劉海區域。LayoutInDisplayCutoutMode表示的就是Android9.0對劉海屏支持的屬性,對該屬性賦值:

            params.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
            window.setAttributes(params);

如此,黑邊就消失了,表示內容已經延伸進了劉海區域。但是此時還會有白邊,需要調整爲沉浸式才能解決:

            int flags = View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
            int visibility = window.getDecorView().getSystemUiVisibility();
            visibility |= flags; //追加沉浸式設置
            window.getDecorView().setSystemUiVisibility(visibility);

經過這樣處理,內容區域就完美延伸進了劉海區域。

當然,還需要通過DisplayCutout判斷是否是劉海屏:

    private boolean hasDisplayCutout(Window window) {

        DisplayCutout displayCutout;
        View rootView = window.getDecorView();
        WindowInsets insets = rootView.getRootWindowInsets();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && insets != null){
            displayCutout = insets.getDisplayCutout();
            if (displayCutout != null){
				//displayCutout.getBoundingRects()表示有幾個劉海
				//displayCutout.getSafeInsetTop()表示劉海的高度
                if (displayCutout.getBoundingRects() != null && displayCutout.getBoundingRects().size() > 0 && displayCutout.getSafeInsetTop() > 0){
                    return true;
                }
            }
        }
        return false; 
    }

提前聲明一點,如果僅僅是將內容區域延伸至劉海,會有bug。比如頂部居中顯示一個Button或TextView,這樣Button就會因爲劉海的原因而顯示不完全。因此在UI設計稿就得規避這個問題。如果UI設計時沒注意這個細節,就得通過代碼,將其向下移動一個“劉海屏的高度”。
手機劉海屏的高默認就是狀態欄的高,可以通過displayCutout.getSafeInsetTop()獲取,也可以直接獲取狀態欄的高度:

    public int heightForDisplayCutout(){
        int resID = getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resID > 0){
            return getResources().getDimensionPixelSize(resID);
        }
        return 96;
    }

然後在代碼中設置控件的Margin值,當然也可以設置父容器的Margin。除了考慮Android 9.0,還有各手機廠商本身的官方文檔,如:華爲、小米、OV

三、手機廠商的適配

主流手機廠商的官方文檔連接如下:
華爲:https://devcenter-test.huawei.com/consumer/cn/devservice/doc/50114
小米:https://dev.mi.com/console/doc/detail?pId=1293
Oppo:https://open.oppomobile.com/service/message/detail?id=61876
Vivo:https://dev.vivo.com.cn/documentCenter/doc/103

它們的處理方式大同小異:

  • 1.判斷手機廠商
  • 2.判斷手機是否劉海
  • 3.設置是否讓內容區域延伸進劉海
  • 4.設置控件是否避開劉海區域
  • 5.獲取劉海的高度

四、工具類

public class Utils {

    /**
     * 是否劉海
     * @param context
     * @return
     */
    public static boolean hasNotchInScreen(Context context) {
        boolean ret = false;
        try {
            ClassLoader cl = context.getClassLoader();
            Class HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil");
            Method get = HwNotchSizeUtil.getMethod("hasNotchInScreen");
            ret = (boolean) get.invoke(HwNotchSizeUtil);
        } catch (ClassNotFoundException e) {
            Log.e("test", "hasNotchInScreen ClassNotFoundException");
        } catch (NoSuchMethodException e) {
            Log.e("test", "hasNotchInScreen NoSuchMethodException");
        } catch (Exception e) {
            Log.e("test", "hasNotchInScreen Exception");
        }
        return ret;
    }

    /**
     * 獲取劉海尺寸:width、height,int[0]值爲劉海寬度 int[1]值爲劉海高度。
     * @param context
     * @return
     */
    public static int[] getNotchSize(Context context) {
        int[] ret = new int[]{0, 0};
        try {
            ClassLoader cl = context.getClassLoader();
            Class HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil");
            Method get = HwNotchSizeUtil.getMethod("getNotchSize");
            ret = (int[]) get.invoke(HwNotchSizeUtil);
        } catch (ClassNotFoundException e) {
            Log.e("test", "getNotchSize ClassNotFoundException");
        } catch (NoSuchMethodException e) {
            Log.e("test", "getNotchSize NoSuchMethodException");
        } catch (Exception e) {
            Log.e("test", "getNotchSize Exception");
        }
        return ret;
    }

    /**
     * 設置使用劉海區域
     * @param window
     */
    public static void setFullScreenWindowLayoutInDisplayCutout(Window window) {
        if (window == null) {
            return;
        }

        try {
            WindowManager.LayoutParams layoutParams = window.getAttributes();
            Class layoutParamsExCls = Class.forName("com.huawei.android.view.LayoutParamsEx");
            Constructor con=layoutParamsExCls.getConstructor(WindowManager.LayoutParams.class);
            Object layoutParamsExObj=con.newInstance(layoutParams);
            Method method=layoutParamsExCls.getMethod("addHwFlags", int.class);
            method.invoke(layoutParamsExObj, FLAG_NOTCH_SUPPORT);
        } catch (Exception e) {
            Log.e("test", "other Exception");
        }
    }

    /*劉海屏全屏顯示FLAG*/
    public static final int FLAG_NOTCH_SUPPORT = 0x00010000;

    /**
     * 設置應用窗口在華爲劉海屏手機不使用劉海
     *
     * @param window 應用頁面window對象
     */
    public static void setNotFullScreenWindowLayoutInDisplayCutout(Window window) {
        if (window == null) {
            return;
        }
        try {
            WindowManager.LayoutParams layoutParams = window.getAttributes();
            Class layoutParamsExCls = Class.forName("com.huawei.android.view.LayoutParamsEx");
            Constructor con = layoutParamsExCls.getConstructor(WindowManager.LayoutParams.class);
            Object layoutParamsExObj = con.newInstance(layoutParams);
            Method method = layoutParamsExCls.getMethod("clearHwFlags", int.class);
            method.invoke(layoutParamsExObj, FLAG_NOTCH_SUPPORT);
        } catch (Exception e) {
            Log.e("test", "hw clear notch screen flag api error");
        }
    }

    /*********
     * 1、聲明全屏顯示。
     *
     * 2、適配沉浸式狀態欄,避免狀態欄部分顯示應用具體內容。
     *
     * 3、如果應用可橫排顯示,避免應用兩側的重要內容被遮擋。
     */


    /********************
     * 判斷該 OPPO 手機是否爲劉海屏手機
     * @param context
     * @return
     */
    public static boolean hasNotchInOppo(Context context) {
        return context.getPackageManager().hasSystemFeature("com.oppo.feature.screen.heteromorphism");
    }

    /**
     * 劉海高度和狀態欄的高度是一致的
     * @param context
     * @return
     */
    public static int getStatusBarHeight(Context context) {
        int resId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resId > 0){
            return context.getResources().getDimensionPixelSize(resId);
        }
        return 0;
    }


    /**
     * Vivo判斷是否有劉海, Vivo的劉海高度小於等於狀態欄高度
     */
    public static final int VIVO_NOTCH = 0x00000020;//是否有劉海
    public static final int VIVO_FILLET = 0x00000008;//是否有圓角

    public static boolean hasNotchAtVivo(Context context) {
        boolean ret = false;
        try {
            ClassLoader classLoader = context.getClassLoader();
            Class FtFeature = classLoader.loadClass("android.util.FtFeature");
            Method method = FtFeature.getMethod("isFeatureSupport", int.class);
            ret = (boolean) method.invoke(FtFeature, VIVO_NOTCH);
        } catch (ClassNotFoundException e) {
            Log.e("Notch", "hasNotchAtVivo ClassNotFoundException");
        } catch (NoSuchMethodException e) {
            Log.e("Notch", "hasNotchAtVivo NoSuchMethodException");
        } catch (Exception e) {
            Log.e("Notch", "hasNotchAtVivo Exception");
        } finally {
            return ret;
        }
    }

}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章