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)是不一样的

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


浙公网安备 33010602011771号