沉浸式

Android刘海屏适配

2019-03-12  本文已影响377人  Cylee1989

1. 前言

在我们进行APP开发时,屏幕适配是一件很头疼的事,而Android又相对iOS的较为混杂,各种各样厂商和屏幕类型,有全面屏、刘海屏、水滴屏、钻孔屏、折叠屏、挖孔屏等等,所以在适配的过程中需要考虑不少东西。以下记录一下我在适配过程中遇到的厂商和解决方案。(如有更好的意见可以一起探讨 ^ _ ^ )

在适配之前需要先了解 刘海屏的开关设置以下两个类的代码内容

import android.graphics.Point;

abstract class BTDeviceFather {

    /**
     * 刘海宽度
     * 
     * @return
     */
    public int getNotchWidth() {
        return 0;
    }

    /**
     * 刘海高度
     * 
     * @return
     */
    public int getNotchHeigth() {
        return 0;
    }

    /**
     * 屏幕底部危险高度
     * 
     * @return
     */
    public int getBottomDangerHeigth() {
        return 0;
    }

    /**
     * 是否隐藏刘海
     * 
     * @return
     */
    public boolean isHideNotch() {
        return false;
    }

    /**
     * 是否支持刘海
     * 
     * @return
     */
    public boolean isSupportNotch() {
        return false;
    }

    /**
     * 获取设备(Physical Size)真实分辨率
     * 
     * @return
     */
    public Point getResolution() {
        return DeviceTools.getScreenRealSize();
    }

}
import java.lang.reflect.Method;

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.os.Build;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.View;
import android.view.WindowManager;

public class DeviceTools {

    /**
     * 是否支持全面屏系统 Android N+
     * 
     * @return
     */
    public static boolean isSupportAndroidVersion() {
        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
    }

    /**
     * 获取屏幕真实分辨率(除vivo系统)
     * 
     * @return
     */
    @SuppressWarnings("deprecation")
    public static Point getScreenRealSize() {
        Point screenSize = null;
        try {
            screenSize = new Point();
            WindowManager windowManager = (WindowManager) BTDeviceSDK.getInstance().getActivity().getSystemService(Context.WINDOW_SERVICE);
            Display defaultDisplay = windowManager.getDefaultDisplay();
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                defaultDisplay.getRealSize(screenSize);
            } else {
                try {
                    Method mGetRawW = Display.class.getMethod("getRawWidth");
                    Method mGetRawH = Display.class.getMethod("getRawHeight");
                    screenSize.set((Integer) mGetRawW.invoke(defaultDisplay), (Integer) mGetRawH.invoke(defaultDisplay));
                } catch (Exception e) {
                    screenSize.set(defaultDisplay.getWidth(), defaultDisplay.getHeight());
                    e.printStackTrace();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return screenSize;
    }

    /**
     * 获取显示分辨率
     * 
     * @param activity
     * @return
     */
    public static Point getScreenDisplaySize(Activity activity) {
        DisplayMetrics dm = activity.getResources().getDisplayMetrics();
        Point screenSize = new Point();
        screenSize.set(dm.widthPixels, dm.heightPixels);
        return screenSize;
    }

    /**
     * 获取截图分辨率
     */
    @SuppressWarnings("deprecation")
    public static Point getScreenShotSize() {
        View view = BTDeviceSDK.getInstance().getActivity().getWindow().getDecorView();
        view.setDrawingCacheEnabled(true);
        view.buildDrawingCache();
        Bitmap bmp = view.getDrawingCache();
        Point screenSize = new Point();
        if (bmp != null) {
            screenSize.set(bmp.getWidth(), bmp.getHeight());
        }
        return screenSize;
    }

    /**
     * 获取状态栏高度
     * 
     * @return
     */
    public static int getStatusBarHeight(Activity activity) {
        int result = 0;
        int resourceId = activity.getResources().getIdentifier("status_bar_height", "dimen", "android");
        if (resourceId > 0) {
            result = activity.getResources().getDimensionPixelSize(resourceId);
        }
        return result;
    }

    /**
     * 获取导航栏高度
     * 
     * @return
     */
    public static int getNavigationBarHeight(Activity activity) {
        int resourceId = 0;
        int rid = activity.getResources().getIdentifier("config_showNavigationBar", "bool", "android");
        if (rid != 0) {
            resourceId = activity.getResources().getIdentifier("navigation_bar_height", "dimen", "android");
            return activity.getResources().getDimensionPixelSize(resourceId);
        }
        return 0;
    }

}

2. 华为屏幕适配

官方文档: https://developer.huawei.com/consumer/cn/devservice/doc/50114

import java.lang.reflect.Method;

import android.graphics.Point;
import android.provider.Settings;
import android.util.Log;

/**
 * 
 * 需要再AndroidManifest.xml中配置
 * 
 * <meta-data android:name="android.notch_support" android:value="true"/>
 * 
 * @author Cylee
 *
 */
class BTHuawei extends BTDeviceFather {

    private String TAG = "BTHuawei";

    @Override
    public boolean isSupportNotch() {
        try {
            ClassLoader cl = BTDeviceSDK.getInstance().getActivity().getClassLoader();
            Class<?> HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil");
            Method get = HwNotchSizeUtil.getMethod("hasNotchInScreen");
            return (Boolean) get.invoke(HwNotchSizeUtil);
        } catch (ClassNotFoundException e) {
            Log.e(TAG, "isFeatureSupport ClassNotFoundException");
        } catch (NoSuchMethodException e) {
            Log.e(TAG, "isFeatureSupport NoSuchMethodException");
        } catch (Exception e) {
            Log.e(TAG, "isFeatureSupport Exception");
        }
        return false;
    }

    @Override
    public boolean isHideNotch() {
        boolean isHide = Settings.Secure.getInt(BTDeviceSDK.getInstance().getActivity().getContentResolver(), "display_notch_status", 0) == 1;
        if (!isHide) {
            Point pReal = BTDeviceSDK.getInstance().getResolution();
            Point pShot = DeviceTools.getScreenShotSize();
            int realSize = pReal.x > pReal.y ? pReal.x : pReal.y;
            int shotSize = pShot.x > pShot.y ? pShot.x : pShot.y;
            int statusBarHeigth = BTDeviceSDK.getInstance().getStatusBarHeight();
            // 设备分辨率-截图分辨率=状态栏,则代表隐藏刘海
            if (realSize - shotSize == statusBarHeigth) {
                return true;
            }
            return false;
        }
        return true;
    }

    @Override
    public int getNotchWidth() {
        // 如果不具备特性或者隐藏了刘海,则返回0
        if (!isSupportNotch() || isHideNotch()) {
            return 0;
        }
        return getNotchSize()[0];
    }

    @Override
    public int getNotchHeigth() {
        // 如果不具备特性或者隐藏了刘海,则返回0
        if (!isSupportNotch() || isHideNotch()) {
            return 0;
        }
        return getNotchSize()[1];
    }

    /**
     * 获取刘海屏凹槽尺寸
     * 
     * @param context
     * @return
     */
    private int[] getNotchSize() {
        int[] ret = new int[] { 0, 0 };
        try {
            ClassLoader cl = BTDeviceSDK.getInstance().getActivity().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, "getNotcSize ClassNotFoundException");
        } catch (NoSuchMethodException e) {
            Log.e(TAG, "getNotcSize NoSuchMethodException");
        } catch (Exception e) {
            Log.e(TAG, "getNotcSize Exception");
        }
        return ret;
    }
}

3. 小米屏幕适配

官方文档: https://dev.mi.com/console/doc/detail?pId=1293

import java.lang.reflect.Method;

import android.annotation.SuppressLint;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.provider.Settings;
import android.view.Window;

/**
 * 在AndroidManifest.xml中配置
 * 
 * <meta-data android:name="notch.config" android:value="portrait|landscape"/>
 * 横竖屏都绘制到耳朵区
 * 
 * <meta-data android:name="notch.config" android:value="none"/> 不绘制到耳朵区
 * 
 * @author Cylee
 *
 */
class BTXiaomi extends BTDeviceFather {

    @Override
    public boolean isSupportNotch() {
        try {
            Class<?> mClassType = Class.forName("android.os.SystemProperties");
            Method mGetIntMethod = mClassType.getDeclaredMethod("getInt", String.class, int.class);
            Integer v = (Integer) mGetIntMethod.invoke(mClassType, "ro.miui.notch", 0);
            return v.intValue() == 1;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    @Override
    public boolean isHideNotch() {
        return Settings.Global.getInt(BTDeviceSDK.getInstance().getActivity().getContentResolver(), "force_black", 0) == 1;
    }

    @Override
    public int getNotchWidth() {
        // 如果不具备特性或者隐藏了刘海,则返回0
        if (!isSupportNotch() || isHideNotch()) {
            return 0;
        }

        // 如果隐藏了刘海,则设置不使用耳朵区
        if (isHideNotch()) {
            Message msg = new Message();
            msg.obj = "clearExtraFlags";
            mHandler.sendMessage(msg);
            return 0;
        }

        // 如果显示刘海,则设置使用耳朵区
        Message msg = new Message();
        msg.obj = "addExtraFlags";
        mHandler.sendMessage(msg);

        String model = Build.MODEL;
        if (model.contains("MI8Lite")) {
            return 296;
        }
        if (model.contains("Redmi 6 Pro")) {
            return 352;
        }
        if (model.contains("MI 8 SE")) {
            return 540;
        }
        if (model.contains("MI 8") || model.contains("MI 8 Explorer Edition") || model.contains("MI 8 UD")) {
            return 560;
        }
        if (model.contains("POCO F1")) {
            return 588;
        }

        int result = 0;
        int resourceId = BTDeviceSDK.getInstance().getActivity().getResources().getIdentifier("notch_width", "dimen", "android");
        if (resourceId > 0) {
            result = BTDeviceSDK.getInstance().getActivity().getResources().getDimensionPixelSize(resourceId);
        }
        return result;
    }

    @Override
    public int getNotchHeigth() {
        // 如果不具备特性或者隐藏了刘海,则返回0
        if (!isSupportNotch()) {
            return 0;
        }

        // 如果隐藏了刘海,则设置不使用耳朵区
        if (isHideNotch()) {
            Message msg = new Message();
            msg.obj = "clearExtraFlags";
            mHandler.sendMessage(msg);
            return 0;
        }

        // 如果显示刘海,则设置使用耳朵区
        Message msg = new Message();
        msg.obj = "addExtraFlags";
        mHandler.sendMessage(msg);

        String model = Build.MODEL;
        if (model.contains("MI8Lite")) {
            return 82;
        }
        if (model.contains("MI 8 SE")) {
            return 85;
        }
        if (model.contains("POCO F1")) {
            return 86;
        }
        if (model.contains("MI 8") || model.contains("MI 8 Explorer Edition") || model.contains("MI 8 UD") || model.contains("Redmi 6 Pro")) {
            return 89;
        }

        int result = 0;
        int resourceId = BTDeviceSDK.getInstance().getActivity().getResources().getIdentifier("notch_height", "dimen", "android");
        if (resourceId > 0) {
            result = BTDeviceSDK.getInstance().getActivity().getResources().getDimensionPixelSize(resourceId);
        }
        return result;
    }

    @SuppressLint("HandlerLeak")
    private Handler mHandler = new Handler() {

        @Override
        public void dispatchMessage(Message msg) {
            String cmd = (String) msg.obj;
            try {
                // 此方法需要在主线程上调用,否则会崩溃。
                Method method = Window.class.getMethod(cmd, int.class);
                method.invoke(BTDeviceSDK.getInstance().getActivity().getWindow(), 0x00000100 | 0x00000200 | 0x00000400);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    };

}

4. VIVO屏幕适配

官方文档: https://dev.vivo.com.cn/documentCenter/doc/103

import java.lang.reflect.Method;

import android.graphics.Point;
import android.util.Log;

/**
 * 在AndroidManifest.xml中配置
 * 
 * <meta-data android:name="android.vendor.home_indicator"android:value="hide"/>
 * 隐藏导航栏
 * 
 * <meta-data android:name="android.vendor.full_screen" android:value="true" />
 * 全屏显示
 * 
 * 在对应的Activity中添加下面的meta用于隐藏导航条 <meta-data
 * android:name="android.vendor.home_indicator" android:value="hide" />
 * 
 * @author Cylee
 *
 */
class BTVivo extends BTDeviceFather {

    private String TAG = "BTVivo";

    @Override
    public Point getResolution() {
        int statusBarHeight = BTDeviceSDK.getInstance().getStatusBarHeight();
        int navigationBarHeight = BTDeviceSDK.getInstance().getNavigationBarHeight();
        Point pReal = DeviceTools.getScreenRealSize();
        Point pShot = DeviceTools.getScreenShotSize();
        int realSize = pReal.x > pReal.y ? pReal.x : pReal.y;
        int shotSize = pShot.x > pShot.y ? pShot.x : pShot.y;
        // 由于vivo系统的问题,如果获取的真实分辨率-截图分辨率=状态栏+导航条的高度,则使用显示分辨率
        if (realSize - shotSize == statusBarHeight + navigationBarHeight) {
            return DeviceTools.getScreenDisplaySize(BTDeviceSDK.getInstance().getActivity());
        }
        return pReal;
    }

    @Override
    public boolean isSupportNotch() {
        try {
            Class<?> mClass = Class.forName("android.util.FtFeature");
            Method[] methods = mClass.getDeclaredMethods();
            Method method = null;
            for (Method m : methods) {
                if (m.getName().equalsIgnoreCase("isFeatureSupport")) {
                    method = m;
                    break;
                }
            }
            // 0x00000020表示是否有凹槽
            // 0x00000008表示是否有圆角
            return (Boolean) method.invoke(null, 0x00000020);
        } catch (ClassNotFoundException e) {
            Log.e(TAG, "getFeature ClassNotFoundException");
        } catch (Exception e) {
            Log.e(TAG, "getFeature Exception");
        }
        return false;
    }

    @Override
    public boolean isHideNotch() {
        Point pReal = BTDeviceSDK.getInstance().getResolution();
        Point pShot = DeviceTools.getScreenShotSize();
        int realSize = pReal.x > pReal.y ? pReal.x : pReal.y;
        int shotSize = pShot.x > pShot.y ? pShot.x : pShot.y;
        int statusBarHeigth = BTDeviceSDK.getInstance().getStatusBarHeight();
        // 设备分辨率-截图分辨率=状态栏,则代表隐藏刘海
        if (realSize - shotSize == statusBarHeigth) {
            return true;
        }
        return false;
    }

    @Override
    public int getNotchWidth() {
        if (!isSupportNotch() || isHideNotch()) {
            return 0;
        }
        int statusBarHeight = BTDeviceSDK.getInstance().getStatusBarHeight();
        return statusBarHeight * 100 / 32;
    }

    @Override
    public int getNotchHeigth() {
        if (!isSupportNotch() || isHideNotch()) {
            return 0;
        }
        int statusBarHeight = BTDeviceSDK.getInstance().getStatusBarHeight();
        return statusBarHeight * 27 / 32;
    }

    @Override
    public int getBottomDangerHeigth() {
        if (!isSupportNotch() || isHideNotch()) {
            return 0;
        }
        int statusBarHeight = BTDeviceSDK.getInstance().getStatusBarHeight();
        return statusBarHeight * 24 / 32;
    };

}

5. OPPO屏幕适配

官方文档: https://open.oppomobile.com/wiki/doc#id=10159

import android.content.pm.PackageManager;
import android.graphics.Point;

class BTOppo extends BTDeviceFather {

    @Override
    public boolean isSupportNotch() {
        PackageManager pm = BTDeviceSDK.getInstance().getActivity().getPackageManager();
        return pm.hasSystemFeature("com.oppo.feature.screen.heteromorphism");
    }

    @Override
    public boolean isHideNotch() {
        Point pReal = BTDeviceSDK.getInstance().getResolution();
        Point pShot = DeviceTools.getScreenShotSize();
        int realSize = pReal.x > pReal.y ? pReal.x : pReal.y;
        int shotSize = pShot.x > pShot.y ? pShot.x : pShot.y;
        int statusBarHeigth = BTDeviceSDK.getInstance().getStatusBarHeight();
        // 设备分辨率-截图分辨率=状态栏,则代表隐藏刘海
        if (realSize - shotSize == statusBarHeigth) {
            return true;
        }
        return false;
    }

    @Override
    public int getNotchWidth() {
        if (!isSupportNotch() || isHideNotch()) {
            return 0;
        }
        return 324;
    }

    @Override
    public int getNotchHeigth() {
        if (!isSupportNotch() || isHideNotch()) {
            return 0;
        }
        return 80;
    }

}

6. OnePlus屏幕适配

官方文档: 貌似没有官方API文档

import android.graphics.Point;
import android.os.Build;

class BTOnePlus extends BTDeviceFather {

    @Override
    public boolean isSupportNotch() {
        String model = Build.MODEL;
        if (model.contains("ONEPLUS A6000")) {
            return true;
        }
        return false;
    }

    @Override
    public boolean isHideNotch() {
        Point pReal = BTDeviceSDK.getInstance().getResolution();
        Point pShot = DeviceTools.getScreenShotSize();
        int realSize = pReal.x > pReal.y ? pReal.x : pReal.y;
        int shotSize = pShot.x > pShot.y ? pShot.x : pShot.y;
        int statusBarHeigth = BTDeviceSDK.getInstance().getStatusBarHeight();
        // 设备分辨率-截图分辨率=状态栏,则代表隐藏刘海
        if (realSize - shotSize == statusBarHeigth) {
            return true;
        }
        return false;
    }

    @Override
    public int getNotchHeigth() {
        if (!isSupportNotch() || isHideNotch()) {
            return 0;
        }
        return 80;
    }

}

7. 联想屏幕适配

官方文档: http://open.lenovo.com/sdk/%E5%85%A8%E9%9D%A2%E5%B1%8F%E9%80%82%E9%85%8D%E6%8C%87%E5%8D%97/

import android.graphics.Point;

class BTLenovo extends BTDeviceFather {

    @Override
    public boolean isSupportNotch() {
        boolean result = false;
        int resourceId = BTDeviceSDK.getInstance().getActivity().getResources().getIdentifier("config_screen_has_notch", "bool", "android");
        if (resourceId > 0) {
            result = BTDeviceSDK.getInstance().getActivity().getResources().getBoolean(resourceId);
        }
        return result;
    }

    @Override
    public boolean isHideNotch() {
        Point pReal = BTDeviceSDK.getInstance().getResolution();
        Point pShot = DeviceTools.getScreenShotSize();
        int realSize = pReal.x > pReal.y ? pReal.x : pReal.y;
        int shotSize = pShot.x > pShot.y ? pShot.x : pShot.y;
        int statusBarHeigth = BTDeviceSDK.getInstance().getStatusBarHeight();
        // 设备分辨率-截图分辨率=状态栏,则代表隐藏刘海
        if (realSize - shotSize == statusBarHeigth) {
            return true;
        }
        return false;
    }

    @Override
    public int getNotchHeigth() {
        if (!isSupportNotch() || isHideNotch()) {
            return 0;
        }

        int result = 0;
        int resourceId = BTDeviceSDK.getInstance().getActivity().getResources().getIdentifier("notch_h", "integer", "android");
        if (resourceId > 0) {
            result = BTDeviceSDK.getInstance().getActivity().getResources().getInteger(resourceId);
        }
        return result;
    }

    @Override
    public int getNotchWidth() {
        if (!isSupportNotch() || isHideNotch()) {
            return 0;
        }

        int result = 0;
        int resourceId = BTDeviceSDK.getInstance().getActivity().getResources().getIdentifier("notch_w", "integer", "android");
        if (resourceId > 0) {
            result = BTDeviceSDK.getInstance().getActivity().getResources().getInteger(resourceId);
        }
        return result;
    }

}

8.魅族屏幕适配

import java.lang.reflect.Field;

import android.provider.Settings;
import android.util.Log;

import com.platform.device.feature.BTDeviceFather;

public class BTMeizu extends BTDeviceFather {

    private String TAG = "BTMeizu";

    @Override
    public boolean isSupportNotch() {
        boolean fringeDevice = false;
        try {
            Class<?> clazz = Class.forName("flyme.config.FlymeFeature");
            Field field = clazz.getDeclaredField("IS_FRINGE_DEVICE");
            fringeDevice = (Boolean) field.get(null);
        } catch (Exception e) {
            Log.e(TAG, "isSupportNotch:\n" + e.toString());
        }
        return fringeDevice;
    }

    @Override
    public boolean isHideNotch() {
        // 判断隐藏刘海开关(默认关)
        return Settings.Global.getInt(BTDeviceSDK.getInstance().getActivity().getContentResolver(), "mz_fringe_hide", 0) == 1;
    }

    @Override
    public int getNotchHeigth() {
        if (!isSupportNotch() || isHideNotch()) {
            return 0;
        }

        // 获取刘海高度(51px)
        int fringeHeight = 0;
        int fhid = BTDeviceSDK.getInstance().getActivity().getResources().getIdentifier("fringe_height", "dimen", "android");
        if (fhid > 0) {
            fringeHeight = BTDeviceSDK.getInstance().getActivity().getResources().getDimensionPixelSize(fhid);
        }
        return fringeHeight;
    }

}

9. 设置沉浸式布局

设置沉浸式布局,可在一定程度上避免获取的屏幕宽高错误的问题。
比如修改了刘海开关、导航条显示方式(三键模式、手势模式等),都会影响获取的屏幕宽高大小。

@SuppressLint("InlinedApi")
private void setSystemUiVisibility() {
    int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
            | View.SYSTEM_UI_FLAG_FULLSCREEN;
    if (Build.VERSION.SDK_INT >= 19) {
        uiOptions |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
    }
    mActivity.getWindow().getDecorView().setSystemUiVisibility(uiOptions);
}

10. 解决Android9.0+不全屏问题

在安卓9.0系统的小米刘海屏手机中,当用户打开刘海开关时,但耳朵区并不进行绘制。
解决此问题在生命周期onCreate中调用以下方法即可。

@SuppressLint("InlinedApi")
private void initDisplayCutoutMode() {
    if ((Build.VERSION.SDK_INT >= 28)) {
        WindowManager.LayoutParams lp = mActivity.getWindow().getAttributes();
        lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
        mActivity.getWindow().setAttributes(lp);
        mActivity.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
        mActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
    }
}

11. 动态屏幕适配

当用户在使用APP的过程中,动态修改了刘海屏的显示或隐藏,那么我们就要在runtime下进行及时调整避让区域等,以解决UI显示异常等现象。

private void initObserver() {
    ViewTreeObserver vto = this.mActivity.getWindow().getDecorView().getViewTreeObserver();
    vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {

    @Override
    public void onGlobalLayout() {
        // 这里处理新视图的逻辑
    });
}

以上方案仅供参考,希望能帮助到需要的童鞋们!
对于其他厂商以及折叠屏等适配的处理之后会继续更新...

上一篇 下一篇

猜你喜欢

热点阅读