Android 耳朵屏适配最佳实践

·  阅读 296

为什么要做耳朵屏的适配?

  • 适应更长的屏幕
  • 防止内容被刘海遮挡

适配思路

  1. 刘海屏适配仅针对全屏模型下的适配
  2. 是否有刘海
  3. 设置绘制区域 延伸到耳朵区域
  4. 获取耳朵屏刘海高度
  5. 对相关控件位置调整
  6. 各手机厂商的适配

遇到的问题

  1. 低版本编译环境 (本人项目最高仅支持 targetSdkVersion 26 、compileSdkVersion 26),无法正常调用 Android P 相关Api

  2. Android P 适配方案 Android P 在不设置绘制区域 延伸到耳朵区域时 无法获取耳朵高度, 且在获取 notchInHeight 时 必须是当前Activity onWindowFocusChanged =true 时才可以拿到正确的高度

  3. Android P 通过status_bar_height 获取耳朵高度并不牢靠,因为没有刘海的手机 status_bar_height 同样不为0

  4. 各厂商在 Android P后 都改用 Google 官方Api

问题解决方案

问题一:

通过 lib 依赖的方式解决  低版本编译问题;新建一个notchmodel ,hostmodel 对其进行依赖

compile project(path: ':notch')
复制代码

原 hostmodel 编译环境 不变(targetSdkVersion 26 、compileSdkVersion 26) notchmodel 编译环境 改为 (targetSdkVersion 28 、compileSdkVersion 28) 这样notchmodel 就可以编写相关的适配代码 且不影响原 hostmodel 编译环境

问题二:

适配耳朵区域前,必须先全屏模式且允许内容绘制到耳朵区,由于是通过mDecorView.getRootWindowInsets 获取刘海高度,为了确保能正确获取 notchInHeight 通过view.post方式处理

view.post(new Runnable() {
            @Override
            public void run() {
                int notchInHeight = NotchUtils.getNotchInHeight(SplashActivity.this);
                FxLog.d("notchInHeight = " + notchInHeight );
            }
        });
复制代码
问题三:

Android P 在获取的刘海高度时, 必须是全屏模式且允许内容绘制到耳朵区 才会读取正确的Notch  否则都为0

问题四:

Android P 如果设备版本大于等于Android P 则用 Google 官方的Api 进行适配

适配方案

1.Google 官方 Android P 耳朵屏适配方案

public class DisplayCutoutActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //1.设置全屏
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        Window window = getWindow();
        window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);

            WindowManager.LayoutParams params = window.getAttributes();
            /**
             *  * @see #LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT 全屏模式,内容下移,非全屏不受影响
             *  * @see #LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES 允许内容去延伸进刘海区
             *  * @see #LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER 不允许内容延伸进刘海区
             */
            params.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
            window.setAttributes(params);

            //3.设置成沉浸式
            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);
        }

        setContentView(R.layout.activity_main);

//        Button button = findViewById(R.id.button);
//        RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) button.getLayoutParams();
//        layoutParams.topMargin = heightForDisplayCutout();
//        button.setLayoutParams(layoutParams);

        RelativeLayout layout = findViewById(R.id.container);
        layout.setPadding(layout.getPaddingLeft(), heightForDisplayCutout(), layout.getPaddingRight(), layout.getPaddingBottom());
    }

}


复制代码

各厂商的适配方案 华为、小米、vivo、oppo

/**
 * Author : CharlesChen
 * Time : 2019/7/1.
 * Email : 
 * Desc :
 * version : v1.0
 */
public class NotchUtils {

    private static String TAG = "Notch_TAG";

    @NotchType
    private static int NOTCH_TYPE = 0;


    @interface NotchType {
        int NO_NOTCH = -1;
        int P = 1;
        int HUAWEI = 2;
        int OPPP = 3;
        int VIVO = 4;
        int MIUI = 5;
    }

    public static @NotchType
    int getNotchType(Context context) {
        if (NOTCH_TYPE != 0) {
            return NOTCH_TYPE;
        }
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                NOTCH_TYPE = 1;
            } else if (hasNotchInHuawei(context)) {
                NOTCH_TYPE = 2;
            } else if (hasNotchInOppo(context)) {
                NOTCH_TYPE = 3;
            } else if (hasNotchInVivo(context)) {
                NOTCH_TYPE = 4;
            } else if (hasNotchInMIUI()) {
                NOTCH_TYPE = 5;
            } else {
                NOTCH_TYPE = -1;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return NOTCH_TYPE;
    }

    /**
     * 获取耳朵屏 凸出高度
     *
     * @param context
     * @return
     */
    public static int getNotchInHeight(Activity context) {
        @NotchType int notchType = getNotchType(context);
        int height = 0;
        switch (notchType) {
            case NotchType.HUAWEI:
                height = getNotchSizeAtHuawei(context)[1];
                break;
            case NotchType.MIUI:
                height = getNotchHeightInMIUI(context);
                break;
            case NotchType.NO_NOTCH:
                height = 0;
                break;
            case NotchType.P:
                height = getNotchHeightInP(context.getWindow());
                break;
            case NotchType.OPPP:
            case NotchType.VIVO:
                height = getStatusBarHeight(context);
                break;
        }

        return height;
    }

    /**
     * 设置绘制区域 延伸到耳朵区域
     * Oppo、Vivo 默认 延伸到耳朵区域
     * 小米、华为 需要手动设置
     */
    public static void setDisplayInNotch(Activity context) {
        try {
            @NotchType int notchType = getNotchType(context);
            switch (notchType) {
                case NotchType.HUAWEI:
                    setDisplayInNotchAtHuawei(context.getWindow());
                    break;
                case NotchType.MIUI:
                    setDisplayInNotchAtMIUI(context);
                    break;
                case NotchType.NO_NOTCH:
                    break;

                case NotchType.OPPP:
                case NotchType.VIVO:
                    //Oppo、Vivo 默认 延伸到耳朵区域
                case NotchType.P:
                    setDisplayInNotchInP(context);
                    break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static boolean hasNotch(Context context){
        boolean hasNotch = false;
        try {
            @NotchType int notchType = getNotchType(context);
            switch (notchType) {
                case NotchType.HUAWEI:
                    hasNotch = hasNotchInHuawei(context);
                    break;
                case NotchType.MIUI:
                    hasNotch = hasNotchInMIUI();
                    break;
                case NotchType.NO_NOTCH:
                    break;

                case NotchType.OPPP:
                    hasNotch = hasNotchInOppo(context);
                    break;
                case NotchType.VIVO:
                    hasNotch = hasNotchInVivo(context);
                    break;
                case NotchType.P:
                    hasNotch = hasNotchInP((Activity) context);
                    break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return hasNotch;
    }

    /********************************************* Android P *********************************************/

    /**
     * 必须是全屏模式且允许内容绘制到耳朵区 才会读取正确的Notch  否则都为null
     */
    @TargetApi(Build.VERSION_CODES.P)
    private static boolean hasNotchInP(Activity activity) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
            return false;
        }
        Window window = activity.getWindow();
        DisplayCutout displayCutout;
        View rootView = window.getDecorView();
        WindowInsets insets = rootView.getRootWindowInsets();
        if (insets != null) {
            displayCutout = insets.getDisplayCutout();
            if (displayCutout != null) {
                if (displayCutout.getBoundingRects() != null && displayCutout.getBoundingRects().size() > 0 && displayCutout.getSafeInsetTop() > 0) {
                    return true;
                }
            }
        }
        return true;
    }
    /**
     * 必须是全屏模式且允许内容绘制到耳朵区 才会读取正确的Notch  否则都为0
     */
    @TargetApi(Build.VERSION_CODES.P)
    private static int getNotchHeightInP(Window window){
        View rootView = window.getDecorView();
        WindowInsets insets = rootView.getRootWindowInsets();
        if (insets != null) {
            DisplayCutout displayCutout = insets.getDisplayCutout();
            if (displayCutout != null) {
                if (displayCutout.getBoundingRects() != null && displayCutout.getBoundingRects().size() > 0 && displayCutout.getSafeInsetTop() > 0) {
                    return displayCutout.getSafeInsetTop();
                }
            }
        }
        return 0;
    }

    public static void setDisplayInNotchInP(Activity activity) {
        Window window = activity.getWindow();
        // 延伸显示区域到耳朵区
        WindowManager.LayoutParams lp = window.getAttributes();
        lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
        window.setAttributes(lp);
        // 允许内容绘制到耳朵区
        final View decorView = window.getDecorView();
        decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
    }


    /********************************************* 华为 *********************************************/

    /*刘海屏全屏显示FLAG*/
    private static final int FLAG_NOTCH_SUPPORT = 0x00010000;

    /**
     * 是否刘海
     */
    private static boolean hasNotchInHuawei(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(TAG, "hasNotchInScreen ClassNotFoundException");
        } catch (NoSuchMethodException e) {
            Log.e(TAG, "hasNotchInScreen NoSuchMethodException");
        } catch (Exception e) {
            Log.e(TAG, "hasNotchInScreen Exception");
        }
        return ret;
    }

    /**
     * 获取刘海尺寸:width、height,int[0]值为刘海宽度 int[1]值为刘海高度。
     */
    private static int[] getNotchSizeAtHuawei(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(TAG, "getNotchSize ClassNotFoundException");
        } catch (NoSuchMethodException e) {
            Log.e(TAG, "getNotchSize NoSuchMethodException");
        } catch (Exception e) {
            Log.e(TAG, "getNotchSize Exception");
        }
        return ret;
    }

    /**
     * 设置使用刘海区域
     */
    private static void setDisplayInNotchAtHuawei(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(TAG, "other Exception");
        }
    }

    /**
     * 设置应用窗口在华为刘海屏手机不使用刘海
     */
    private static void setNotDisplayInNotchAtHuawei(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(TAG, "hw clear notch screen flag api error");
        }
    }


    /********************************************* OPPO *********************************************/


    /**
     * 判断该 OPPO 手机是否为刘海屏手机
     */
    private static boolean hasNotchInOppo(Context context) {
        return context.getPackageManager().hasSystemFeature("com.oppo.feature.screen.heteromorphism");
    }

    /**
     * 刘海高度和状态栏的高度是一致的
     *
     * @param context
     * @return
     */
    private 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判断是否有刘海, Vivo的刘海高度小于等于状态栏高度
     */
    private static final int VIVO_NOTCH = 0x00000020;//是否有刘海
    private static final int VIVO_FILLET = 0x00000008;//是否有圆角

    private static boolean hasNotchInVivo(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", "hasNotchInVivo ClassNotFoundException");
        } catch (NoSuchMethodException e) {
            Log.e("Notch", "hasNotchInVivo NoSuchMethodException");
        } catch (Exception e) {
            Log.e("Notch", "hasNotchInVivo Exception");
        } finally {
            return ret;
        }
    }


    /********************************************* 小米 *********************************************/


    /**
     * 是否有刘海
     *
     * @return
     */
    private static boolean hasNotchInMIUI() {
        try {
            Method getInt = Class.forName("android.os.SystemProperties").getMethod("getInt", String.class, int.class);
            int notch = (int) getInt.invoke(null, "ro.miui.notch", 0);
            return notch == 1;
        } catch (Throwable ignore) {
        }
        return false;
    }


    /**
     * 设置显示到刘海区域
     * 0x00000100 | 0x00000200 竖屏绘制到耳朵区
     * 0x00000100 | 0x00000400 横屏绘制到耳朵区
     * 0x00000100 | 0x00000200 | 0x00000400 横竖屏都绘制到耳朵区
     */
    private static void setDisplayInNotchAtMIUI(Activity activity) {
        int flag = 0x00000100 | 0x00000200 | 0x00000400;
        try {
            Method method = Window.class.getMethod("addExtraFlags",
                    int.class);
            method.invoke(activity.getWindow(), flag);
        } catch (Exception ignore) {
        }
    }

    /**
     * 获取刘海宽高
     */
    private static int getNotchHeightInMIUI(Context context) {
        int resourceId = context.getResources().getIdentifier("notch_height", "dimen", "android");
        if (resourceId > 0) {
            return context.getResources().getDimensionPixelSize(resourceId);
        }
        return 0;
    }
}
复制代码

其他手机厂商(华为,小米,oppo,vivo)适配 华为:devcenter-test.huawei.com/consumer/cn… 小米:dev.mi.com/console/doc… Oppo:open.oppomobile.com/service/mes… Vivo:dev.vivo.com.cn/documentCen…

分类:
Android
标签:
分类:
Android
标签: