Unity刘海适配

原理

根据刘海的信息,设置好安全区域,然后把ui都相对安全区域来显示。

比如:屏幕宽高为1334x750,刘海大小为:左60,右50,上0,下40,那安全区域就是(1334-60-50)x(750-0-40),即:1224x710

如果不做安全区域适配,就会像下面这样显示(边缘的灰色代表有刘海),ui显示到刘海下面被遮挡

 

根据刘海信息设置好安全区域后

 

2、如何获取刘海信息

  Unity提供的API来获取

Rect safeArea = Screen.safeArea;
float cutoutL = safeArea.xMin; //屏幕左边刘海大小
float cotoutB = safeArea.yMin; //屏幕下边刘海大小
float safeWidth = safeArea.width; //安全区域宽
float safeHeight = safeArea.height; //安全区域高

 

  Unity提供的API在有的平台上可能会没法正确获取到信息,此时就需要调用专用API去获取。

  比如:使用Android API来获取

Activity curActivity = UnityPlayer.currentActivity;
Window win = curActivity.getWindow();
View decoView = win.getDecorView();
int screenWidth = decoView.getWidth();
int screenHeight = decoView.getHeight();

WindowInsets winInsets = decoView.getRootWindowInsets();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
    DisplayCutout displayCutout = winInsets.getDisplayCutout();
    if (displayCutout != null) {
        int cutoutT = displayCutout.getSafeInsetTop(); //屏幕上方刘海大小
        int cutoutB = displayCutout.getSafeInsetBottom(); //屏幕下侧刘海大小
        int cutoutL = displayCutout.getSafeInsetLeft(); //屏幕左侧刘海大小
        int cutoutR = displayCutout.getSafeInsetRight(); //屏幕右侧刘海大小

        int safeWidth = screenWidth - (cutoutL + cutoutR); //安全区域宽
        int safeHeight = screenHeight - (cutoutT + cutoutB); //安全区域高
    }
}

 

2、如何设置安全区域

  设置安全区域包括设置RectTransform的位置和大小,可以使用下面的方式来计算:

int safeWidth = screenWidth - (cutoutL + cutoutR);
int safeHeight = screenHeight - (cutoutT + cutoutB);

//RectTransform的anchors相对中心时
int anPosX = cutoutL - (cutoutL + cutoutR) * 0.5f; 
int anPosY = cutoutB - (cutoutT + cutoutB) * 0.5f;

 

   但还要考虑一个问题,就是设计分辨率和屏幕分辨率不一致问题。比如:设计分辨率是1334*750,但是运行的设备可能有各种分辨率1920x1080,2436x1125,3200x1400等,

   假设在3200x1400屏幕上,左侧刘海大小为200,右侧刘海大小为400,按屏幕分辨率看只占1/7,但是按设计分辨率看占了1/2,这肯定不对。

   所以这边要用占屏幕分辨率的比例*设计分辨率大小来计算。即:1/7*1334=190.57,即左侧63.52,右侧127.05

int safeWidth = screenWidth - (cutoutL + cutoutR);
int safeHeight = screenHeight - (cutoutT + cutoutB);
float safeWidthPercent = safeWidth / screenWidth;
float safeHeightPercent = safeHeight / screenHeight;

//RectTransform的anchors相对中心时
int anPosX = cutoutL - (cutoutL + cutoutR) * 0.5f; 
int anPosY = cutoutB - (cutoutT + cutoutB) * 0.5f;
float anPosXPercent = anPosX / screenWidth;
float anPosYPercent = anPosY / screenHeight;

float safeWidthForDesign = safeWidthPercent * desighWidth;
float safeWidthForDesign = safeHeightPercent * desighHeight;

float anPosXForDesign = anPosXPercent * desighWidth;
float anPosYForDesign = anPosYPercent * desighHeight;

 

3、完整实现

 SafeAreaMgr.cs,加在UIRoot节点上

 

public class SafeAreaMgr : MonoBehaviour
{
    private static SafeAreaMgr m_Inst;
    public static SafeAreaMgr instance { get { return m_Inst; } }
    
    private bool m_UseCustomCutout;
    private bool m_CustomCutoutDirty;

    private ScreenOrientation m_Orient; //上次记录的屏幕方向

    private RectTransform m_UiRootRtf; //ui的根节点
    private Rect m_UiRootRect;

    private Vector2 m_SafeSizePercent;
    private Vector2 m_AnPosPercent;

    private HashSet<SafeArea> m_SafeAreas = new HashSet<SafeArea>();
    private Vector2 m_SafeAreaAnPos;
    private Vector2 m_SafeAreaSize;

    void Awake()
    {
        if (null != m_Inst)
        {
            Debug.LogError("SafeAreaMgr: more than one instance");
            return;
        }

        m_Inst = this;

        m_UiRootRtf = (RectTransform)transform;
        m_UiRootRect = m_UiRootRtf.rect;
    }

    void OnDestroy()
    {
        m_Inst = null;
    }

    public Vector2 UiRootSize
    {
        get { return m_UiRootRect.size; }
    }

    public Vector2 SafeAreaSize
    {
        get { return m_SafeAreaSize; }
    }

    public Vector2 SafeAreaAnPos
    {
        get { return m_SafeAreaAnPos; }
    }

    public void AddSafeArea(SafeArea s)
    {
        m_SafeAreas.Add(s);
    }

    public void RemoveSafeArea(SafeArea s)
    {
        m_SafeAreas.Remove(s);
    }

    private bool CheckIsDirty()
    {
        var orient = Screen.orientation;
        if (m_Orient != orient)
        {
            m_Orient = orient;
            return true;
        }
        
        Rect uiRootRect = m_UiRootRtf.rect;
        if (m_UiRootRect != uiRootRect)
        {
            m_UiRootRect = uiRootRect;
            return true;
        }

        if (m_CustomCutoutDirty)
        {
            m_CustomCutoutDirty = false;
            return true;
        }

        return false;
    }

    public void SetCustomCutoutPercent(float l, float t, float r, float b)
    {
        m_UseCustomCutout = true;
        m_CustomCutoutDirty = true;
        
        float safeWidthPercent = 1 - l - r;
        float safeHeightPercent = 1 - t - b;
        m_SafeSizePercent.Set(safeWidthPercent, safeHeightPercent);

        float anPosXPercent = l - (1 - safeWidthPercent) * 0.5f;
        float anPosYPercent = b - (1 - safeHeightPercent) * 0.5f;
        m_AnPosPercent.Set(anPosXPercent, anPosYPercent);
    }

    public void ClearCustomPadding()
    {
        m_UseCustomCutout = false;
        m_CustomCutoutDirty = true;
    }

    void Update()
    {
        if (CheckIsDirty())
        {
            UpdateSafeRect();

            foreach (var item in m_SafeAreas)
            {
                item.OnSafeAreaChange();
            }
        }
    }

    //计算安全区域
    private void UpdateSafeRect()
    {
        if (m_UseCustomCutout)
        {
            //do nothing
        }
        else
        {
#if UNITY_ANDROID
            var javaClazz = new AndroidJavaClass("com.unity3d.SafeAreaHelper");
            var javaClazzPtr = javaClazz.GetRawClass();
            var methodIDPtr = AndroidJNIHelper.GetMethodID(javaClazzPtr, "updateScreenAndInsetInfo", "()V", true);
            AndroidJNI.CallStaticVoidMethod(javaClazzPtr, methodIDPtr, null);
            
            methodIDPtr = AndroidJNIHelper.GetMethodID(javaClazzPtr, "getScreenWidth", "()I", true);
            float screenW = AndroidJNI.CallStaticIntMethod(javaClazzPtr, methodIDPtr, null);
            
            methodIDPtr = AndroidJNIHelper.GetMethodID(javaClazzPtr, "getScreenHeight", "()I", true);
            float screenH = AndroidJNI.CallStaticIntMethod(javaClazzPtr, methodIDPtr, null);
            
            methodIDPtr = AndroidJNIHelper.GetMethodID(javaClazzPtr, "getSafeWidth", "()I", true);
            int safeWidth = AndroidJNI.CallStaticIntMethod(javaClazzPtr, methodIDPtr, null);
            
            methodIDPtr = AndroidJNIHelper.GetMethodID(javaClazzPtr, "getSafeHeight", "()I", true);
            int safeHeight = AndroidJNI.CallStaticIntMethod(javaClazzPtr, methodIDPtr, null);
            
            float safeWidthPercent = safeWidth / screenW;
            float safeHeightPercent = safeHeight / screenH;
            
            methodIDPtr = AndroidJNIHelper.GetMethodID(javaClazzPtr, "getInsetLeft", "()I", true);
            int cutoutL = AndroidJNI.CallStaticIntMethod(javaClazzPtr, methodIDPtr, null);
            
            methodIDPtr = AndroidJNIHelper.GetMethodID(javaClazzPtr, "getInsetBottom", "()I", true);
            int cutoutB = AndroidJNI.CallStaticIntMethod(javaClazzPtr, methodIDPtr, null);

            float cutoutLPercent = cutoutL / screenW;
            float curoutBPercent = cutoutB / screenH;
            
            //RectTransform的anchors相对中心时
            m_SafeSizePercent.Set(safeWidthPercent, safeHeightPercent);

            float anPosXPercent = cutoutLPercent - (1 - safeWidthPercent) * 0.5f;
            float anPosYPercent = curoutBPercent - (1 - safeHeightPercent) * 0.5f;
            m_AnPosPercent.Set(anPosXPercent, anPosYPercent);
#elif UNITY_IOS
            //todo
#else //#elif UNITY_EDITOR
            m_SafeRectInPercent = new Rect(0, 0, 1, 1);
            int screenW = Screen.width;
            int screenH = Screen.height;
            var safeArea = Screen.safeArea;
            float safeWidthPercent = safeArea.width / screenW;
            float safeHeightPercent = safeArea.height / screenH;
            m_SafeSizePercent.Set(safeWidthPercent, safeHeightPercent);
            
            float anPosXPercent = (safeArea.xMin - (screenW - safeArea.width) * 0.5f) / screenW;
            float anPosYPercent = (safeArea.yMin - (screenH - safeArea.height) * 0.5f) / screenH;
            m_AnPosPercent.Set(anPosXPercent, anPosYPercent);
#endif
        }

        Vector2 designSize = m_UiRootRect.size;
        m_SafeAreaSize = designSize * m_SafeSizePercent;
        m_SafeAreaAnPos = designSize * m_AnPosPercent;
    }

}

 

SafeArea.cs,加在界面的安全区节点上

 

public class SafeArea : MonoBehaviour
{
    public RectTransform m_BgRtf;

    void OnEnable() {
        SafeAreaMgr.instance.AddSafeArea(this);
        OnSafeAreaChange();
    }

    void OnDisable() {
        SafeAreaMgr.instance.RemoveSafeArea(this);
    }

    public void OnSafeAreaChange() {
        //根据屏幕比例, 等比例缩放背景图
        if (null != m_BgRtf) {
            Vector2 uiRootSize = SafeAreaMgr.instance.UiRootSize;
            Rect bgRect = m_BgRtf.rect;
            float sx = uiRootSize.x / bgRect.width;
            float sy = uiRootSize.y / bgRect.height;
            float s = Mathf.Max(sx, sy);
            m_BgRtf.localScale = new Vector3(s, s, s);
        }

        var rtf = (RectTransform) transform;
        rtf.anchorMin = new Vector2(0.5f, 0.5f);
        rtf.anchorMax = new Vector2(0.5f, 0.5f);
        rtf.anchoredPosition = SafeAreaMgr.instance.SafeAreaAnPos;
        rtf.sizeDelta = SafeAreaMgr.instance.SafeAreaSize;
    }

}

 

安卓部分的代码,SafeAreaHelper.java

package com.unity3d;

public class SafeAreaHelper {

    private static int s_screenWidth;
    private static int s_screenHeight;

    private static int[] s_insets;

    public static void printInfo() {
        updateScreenAndInsetInfo();

        String msg = "screen:(" + s_screenWidth + "," + s_screenHeight + "), lb:(";
        msg += s_insets[0] + "," + s_insets[3] + "), size:(";

        int w = s_screenWidth - (s_insets[0] + s_insets[2]);
        int h = s_screenHeight - (s_insets[1] + s_insets[3]);
        msg += w + "," + h + ")";

        Log.i("SafeAreaHelper",  msg);
    }

    public static boolean isScreenAndInsetInfoInited() {
        boolean b = (null != s_insets);
        return b;
    }

    //获取前先调用该方法
    public static void updateScreenAndInsetInfo() {
        Activity curActivity = UnityPlayer.currentActivity;
        Window win = curActivity.getWindow();
        View decoView = win.getDecorView();
        s_screenWidth = decoView.getWidth();
        s_screenHeight = decoView.getHeight();

        if (null == s_insets)
            s_insets = new int[4];
        updateCutoutSize_Google(decoView, s_insets);
    }
    public static int getScreenWidth() {
        return s_screenWidth;
    }

    public static int getScreenHeight() {
        return s_screenHeight;
    }

    public static int getSafeWidth() {
        int w = s_screenWidth - (s_insets[0] + s_insets[2]);
        return w;
    }

    public static int getSafeHeight() {
        int h = s_screenHeight - (s_insets[1] + s_insets[3]);
        return h;
    }

    public static int getInsetLeft() {
        return s_insets[0];
    }

    public static int getInsetTop() {
        return s_insets[1];
    }

    public static int getInsetRight() {
        return s_insets[2];
    }

    public static int getInsetBottom() {
        return s_insets[3];
    }

    @TargetApi(Build.VERSION_CODES.M)
    private static void updateCutoutSize_Google(View decoView, int[] outInsets) {
        WindowInsets winInsets = decoView.getRootWindowInsets();do {
            Class[] emptyClazzArr = new Class[0];
            Object[] emptyObjArr = new Object[0];

            try {
                Method getDisplayCutoutMethod = winInsets.getClass().getMethod("getDisplayCutout", emptyClazzArr);
                Object displayCutoutObj = getDisplayCutoutMethod.invoke(winInsets, emptyObjArr);
                if (null == displayCutoutObj) break;

                Class displayCutoutClazz = displayCutoutObj.getClass();

                Method getSafeInsetTopMethod = displayCutoutClazz.getMethod("getSafeInsetTop", emptyClazzArr);
                Object safeInsetTopObject = getSafeInsetTopMethod.invoke(displayCutoutObj, emptyObjArr);
                if (null == safeInsetTopObject) break;

                Method getSafeInsetBottomMethod = displayCutoutClazz.getMethod("getSafeInsetBottom", emptyClazzArr);
                Object safeInsetBottomObject = getSafeInsetBottomMethod.invoke(displayCutoutObj, emptyObjArr);
                if (null == safeInsetBottomObject) break;

                Method getSafeInsetLeftMethod = displayCutoutClazz.getMethod("getSafeInsetLeft", emptyClazzArr);
                Object safeInsetLeftobject = getSafeInsetLeftMethod.invoke(displayCutoutObj, emptyObjArr);
                if (null == safeInsetLeftobject) break;

                Method getSafeInsetRightMethod = displayCutoutClazz.getMethod("getSafeInsetRight", emptyClazzArr);
                Object safeInsetRightobject = getSafeInsetRightMethod.invoke(displayCutoutObj, emptyObjArr);
                if (null == safeInsetRightobject) break;

                int l = (Integer) safeInsetLeftobject;
                int t = (Integer) safeInsetTopObject;
                int r = (Integer) safeInsetRightobject;
                int b = (Integer) safeInsetBottomObject;

                outInsets[0] = l;
                outInsets[1] = t;
                outInsets[2] = r;
                outInsets[3] = b;
                return;
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        } while (false);

        outInsets[0] = 0;
        outInsets[1] = 0;
        outInsets[2] = 0;
        outInsets[3] = 0;
    }

}

 

4、测试运行

是在Android Studio下的安卓模拟器上运行的,这个机器的分辨率时1920*1080的,和我们的设计分辨率(1334*750)是不一样的

 运行效果,可以看到:刘海切换到右侧时,右侧按钮能够自动调整位置到安全区内。

 

posted @ 2025-05-20 00:17  yanghui01  阅读(177)  评论(0)    收藏  举报