Apple의 공식 휴먼 인터페이스 가이드라인에 따르면:
https://developer.apple.com/design/human-interface-guidelines/guidelines/overview/
Apple은 iPhone X의 특수한 모양의 화면에 Safe Area라는 개념을 제안했는데, 이 Safe Area는 Safe Area의 UI가 디스플레이가 잘리지 않도록 보장할 수 있음을 의미합니다.

Apple의 디자인 사양에 따라 검은색 테두리를 남기지 않고 안전 영역에 UI 컨트롤을 SafeArea에 배치해야 합니다.
Unity에서는 더 적은 작업 부하로 모든 인터페이스 컨트롤을 안전 영역에 도킹하고 검은색 테두리를 장면 또는 배경 이미지로 채우는 방법을 해결해야 합니다.
iPhoneX를 수평으로 잡을 때:
iPhone X의 전체 픽셀 크기는
2436 x 1125 픽셀입니다.
전체 SafeArea 영역은
2172 x 1062 픽셀입니다.
왼쪽 및 오른쪽 슬롯(평평한 앞머리 및 둥근 모서리, 여백 포함)은 각각 132픽셀입니다.
하단 여백 (iPhoneX에는 홈 버튼이 없으므로 가상 홈 화면 표시 막대가 있습니다.) 홈 화면의 표시 막대는 63픽셀의 높이를 차지하고 테두리가 없는 상단은 0픽셀입니다.
1. 기술 솔루션
1. 카메라 뷰포트 변경
UI 카메라의 뷰포트를 Rect(132/2436, 0, 2172/2436, 1062/1125)로 직접 변경한 후 배경 이미지를 다른 카메라로 설정합니다. 이것의 장점은 원래 레이아웃을 전혀 변경할 필요가 없다는 것입니다. 단점은 다중 UI의 경우 배경 이미지와 메인 UI의 깊이 관계를 재설정해야 한다는 점이다.
2. 스케일 조정
메인 UI의 Scale을 0.9로, 배경 이미지의 Scale을 1.1로 설정하여 검은 테두리가 남지 않도록 합니다. 이 방법의 장점은 간단하다는 점이지만 Tween과 Active/InActive 전환 사이에 약간의 문제가 발생한다는 단점이 있습니다.
3. 기준점 변경
두 가지 경우에 NGUI와 UGUI는 약간 다릅니다. 두 프로젝트에 대한 완벽한 적응 경험이 있어서 이 공유를 작성했습니다.
2. 구현 세부 정보
우선 아이폰X의 안전영역을 확보하고 유니티는 이를 확보하기 위해 플러그인 개발을해야 한다. 프로젝트의 Plugins/iOS 디렉토리에 SafeArea.mm을 복사합니다.
//获取iPhoneX safeArea
//Jeff 2017-12-1
//文件名 SafeArea.mm
#include <CoreGraphics/CoreGraphics.h>
#include "UnityAppController.h"
#include "UI/UnityView.h"
CGRect CustomComputeSafeArea(UIView* view)
{
CGSize screenSize = view.bounds.size;
CGRect screenRect = CGRectMake(0, 0, screenSize.width, screenSize.height);
UIEdgeInsets insets = UIEdgeInsetsMake(0, 0, 0, 0);
if ([view respondsToSelector: @selector(safeAreaInsets)])
insets = [view safeAreaInsets];
screenRect.origin.x += insets.left;
screenRect.size.width -= insets.left + insets.right;
float scale = view.contentScaleFactor;
screenRect.origin.x *= scale;
screenRect.origin.y *= scale;
screenRect.size.width *= scale;
screenRect.size.height *= scale;
return screenRect;
}
//外部调用接口
extern "C" void GetSafeArea(float* x, float* y, float* w, float* h)
{
UIView* view = GetAppController().unityView;
CGRect area = CustomComputeSafeArea(view);
*x = area.origin.x;
*y = area.origin.y;
*w = area.size.width;
*h = area.size.height;
}
패널을 조정해야 하는 일반 조정 구성 요소를 디자인하고 이 스크립트를 직접 추가하기만 하면 됩니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 设计安全区域面板(适配iPhone X)
/// Jeff 2017-12-1
/// 文件名 SafeAreaPanel.cs
/// </summary>
public class SafeAreaPanel : MonoBehaviour
{
private RectTransform target;
#if UNITY_EDITOR
[SerializeField]
private bool Simulate_X = false;
#endif
void Awake()
{
target = GetComponent<RectTransform>();
ApplySafeArea();
}
void ApplySafeArea()
{
var area = SafeAreaUtils.Get();
#if UNITY_EDITOR
/*
iPhone X 横持手机方向:
iPhone X 分辨率
2436 x 1125 px
safe area
2172 x 1062 px
左右边距分别
132px
底边距 (有Home条)
63px
顶边距
0px
*/
float Xwidth = 2436f;
float Xheight = 1125f;
float Margin = 132f;
float InsetsBottom = 63f;
if ((Screen.width == (int)Xwidth && Screen.height == (int)Xheight)
|| (Screen.width == 812 && Screen.height == 375))
{
Simulate_X = true;
}
if (Simulate_X)
{
var insets = area.width * Margin / Xwidth;
var positionOffset = new Vector2(insets, 0);
var sizeOffset = new Vector2(insets * 2, 0);
area.position = area.position + positionOffset;
area.size = area.size - sizeOffset;
}
#endif
var anchorMin = area.position;
var anchorMax = area.position + area.size;
anchorMin.x /= Screen.width;
anchorMin.y /= Screen.height;
anchorMax.x /= Screen.width;
anchorMax.y /= Screen.height;
target.anchorMin = anchorMin;
target.anchorMax = anchorMax;
}
}
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using UnityEngine;
/// <summary>
/// iPhone X适配工具类
/// Jeff 2017-12-1
/// 文件名 SafeAreaUtils.cs
/// </summary>
public class SafeAreaUtils
{
#if UNITY_IOS
[DllImport("__Internal")]
private static extern void GetSafeArea(out float x, out float y, out float w, out float h);
#endif
/// <summary>
/// 获取iPhone X 等苹果未来的异性屏幕的安全区域Safe are
/// </summary>
/// <param name="showInsetsBottom"></param>
/// <returns></returns>
public static Rect Get()
{
float x, y, w, h;
#if UNITY_IOS && !UNITY_EDITOR
GetSafeArea(out x, out y, out w, out h);
#else
x = 0;
y = 0;
w = Screen.width;
h = Screen.height;
#endif
return new Rect(x, y, w, h);
}
}

예를 들어 SafeAreaPanel 구성 요소를 패널에 추가하고 Simulate_X를 선택하여 iPhone X의 작동을 시뮬레이션합니다.

런타임 이미지(빨간색 영역이 메인 UI 패널로, 평소에는 전체화면입니다. 여기서 Safe 영역에 따라 자동 적응 후 앵커 포인트로 표시되는 좌우 여백과 하단 여백을 조정하고, 하단 파란색 영역은 장면 또는 UI 배경 이미지 영역) 812×375를 추가하면 iPhoneX의 효과를 시뮬레이션할 수 있습니다.


예전 프로젝트를 NGUI로 개발하면 원리는 같고 위의 SafaArea.mm을 이용해서 안전영역을 얻어야 하는데 차이점은 ngui의 소스코드를 수정했다는 점과 NGUI 버전이 많다는 점이다. SafeArea.mm을 프로젝트의 Plugins/iOS 디렉토리에 복사하는 것을 잊지 마십시오 .
사용하는 NGUI와 함께 수정해야 할 아이디어와 핵심 코드를 제공하겠습니다.
NGUI의 UISprite, UILabel, UIPanel 등은 모두 추상 클래스 UIRect를 상속합니다.

UIRect UI 사각형에는 4개의 앵커 포인트(양쪽에 하나씩)가 포함되어 있으며 안전 영역에서 앵커 포인트의 표시를 제어하려고 합니다.
NGUITools.cs에 코드 추가
#if UNITY_IOS && !UNITY_EDITOR
[DllImport("__Internal")]
private static extern void GetSafeArea(out float x, out float y, out float w, out float h);
#endif
public static Rect SafeArea
{
get
{
return GetSafeArea();
}
}
/// <summary>
/// 获取iPhone X 等苹果未来的异型屏幕的安全区域SafeArea
/// </summary>
/// <returns>Rect</returns>
public static Rect GetSafeArea()
{
float x, y, w, h;
#if UNITY_IOS && !UNITY_EDITOR
GetSafeArea(out x, out y, out w, out h);
#else
x = 0;
y = 0;
w = Screen.width;
h = Screen.height;
#endif
return new Rect(x, y, w, h);
}
#if UNITY_EDITOR
static int mSizeFrame = -1;
static System.Reflection.MethodInfo s_GetSizeOfMainGameView;
static Vector2 mGameSize = Vector2.one;
/// <summary>
/// Size of the game view cannot be retrieved from Screen.width and Screen.height when the game view is hidden.
/// </summary>
static public Vector2 screenSize
{
get
{
int frame = Time.frameCount;
if (mSizeFrame != frame || !Application.isPlaying)
{
mSizeFrame = frame;
if (s_GetSizeOfMainGameView == null)
{
System.Type type = System.Type.GetType("UnityEditor.GameView,UnityEditor");
s_GetSizeOfMainGameView = type.GetMethod("GetSizeOfMainGameView",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
}
mGameSize = (Vector2)s_GetSizeOfMainGameView.Invoke(null, null);
}
return mGameSize;
}
}
#else
/// <summary>
/// Size of the game view cannot be retrieved from Screen.width and Screen.height when the game view is hidden.
/// </summary>
static public Vector2 screenSize { get { return new Vector2(Screen.width, Screen.height); } }
#endif
public static bool Simulate_X
{
get
{
#if UNITY_EDITOR
return (Screen.width == 812 && Screen.height == 375);
#else
return false;
#endif
}
}
/// <summary>
/// 模拟iPhone X比例
/// </summary>
public static float Simulate_iPhoneXScale
{
get
{
if (!Simulate_X) return 1f;
/*
iPhone X 横持手机方向分辨率:2436 x 1125 px
SafeArea:2172 x 1062 px
左右边距分别:132px
底边距(有Home条):63px
顶边距:0px
*/
float xwidth = 2436f;
float xheight = 1125f;
float margin = 132f;
return (xwidth - margin * 2) / xwidth;
}
}
앵커 포인트의 적응은 결국 NGUITools.GetSides 메서드를 호출하게 되며, 이는 실제로 NGUI for Camera에 의해 작성된 확장 메서드입니다.
NGUITools.cs의 정적 public Vector3[] GetSides(this Camera cam, float depth, Transform relativeTo)
를 찾고 bool showInSafeArea를 추가합니다. 기본값은 false입니다.
static public Vector3[] GetSides(this Camera cam, float depth, Transform relativeTo, bool showInSafeArea = false)
{
#if UNITY_4_3 || UNITY_4_5 || UNITY_4_6 || UNITY_4_7
if (cam.isOrthoGraphic)
#else
if (cam.orthographic)
#endif
{
float xOffset = 1f;
#if UNITY_IOS
if (showInSafeArea)
{
xOffset = SafeArea.width / Screen.width;
}
#elif UNITY_EDITOR
if (showInSafeArea)
{
xOffset = Simulate_iPhoneXScale;
}
#endif
float os = cam.orthographicSize;
float x0 = -os * xOffset;
float x1 = os * xOffset;
float y0 = -os;
float y1 = os;
Rect rect = cam.rect;
Vector2 size = screenSize;
float aspect = size.x / size.y;
aspect *= rect.width / rect.height;
x0 *= aspect;
x1 *= aspect;
// We want to ignore the scale, as scale doesn't affect the camera's view region in Unity
Transform t = cam.transform;
Quaternion rot = t.rotation;
Vector3 pos = t.position;
int w = Mathf.RoundToInt(size.x);
int h = Mathf.RoundToInt(size.y);
if ((w & 1) == 1) pos.x -= 1f / size.x;
if ((h & 1) == 1) pos.y += 1f / size.y;
mSides[0] = rot * (new Vector3(x0, 0f, depth)) + pos;
mSides[1] = rot * (new Vector3(0f, y1, depth)) + pos;
mSides[2] = rot * (new Vector3(x1, 0f, depth)) + pos;
mSides[3] = rot * (new Vector3(0f, y0, depth)) + pos;
}
else
{
mSides[0] = cam.ViewportToWorldPoint(new Vector3(0f, 0.5f, depth));
mSides[1] = cam.ViewportToWorldPoint(new Vector3(0.5f, 1f, depth));
mSides[2] = cam.ViewportToWorldPoint(new Vector3(1f, 0.5f, depth));
mSides[3] = cam.ViewportToWorldPoint(new Vector3(0.5f, 0f, depth));
}
if (relativeTo != null)
{
for (int i = 0; i < 4; ++i)
mSides[i] = relativeTo.InverseTransformPoint(mSides[i]);
}
return mSides;
}
또한 UIRect.cs에 추가하려면 UIRect 및 UIRectEditor 의 관련 메서드를 변경해야 합니다.
[HideInInspector][SerializeField]public bool mShowInSafeArea = false;
GetSides에 대한 호출 수정
/// <summary>
/// Convenience function that returns the sides the anchored point is anchored to.
/// </summary>
public Vector3[] GetSides (Transform relativeTo)
{
if (target != null)
{
if (rect != null) return rect.GetSides(relativeTo);
if (target.camera != null) return target.camera.GetSides(relativeTo, rect.mShowInSafeArea);//这里增加了是否在安全区域的参数
}
return null;
}
/// <summary>
/// Get the sides of the rectangle relative to the specified transform.
/// The order is left, top, right, bottom.
/// </summary>
public virtual Vector3[] GetSides (Transform relativeTo)
{
if (anchorCamera != null)
{
return anchorCamera.GetSides(relativeTo, mShowInSafeArea);//这里增加了是否在安全区域的参数
}
else
{
Vector3 pos = cachedTransform.position;
for (int i = 0; i < 4; ++i)
mSides[i] = pos;
if (relativeTo != null)
{
for (int i = 0; i < 4; ++i)
mSides[i] = relativeTo.InverseTransformPoint(mSides[i]);
}
return mSides;
}
}
물론 UIRectEditor.cs 확장에서
/// <summary>
/// Draw the "Anchors" property block.
/// </summary>
protected virtual void DrawFinalProperties ()
{
if (!((target as UIRect).canBeAnchored))
{
if (NGUIEditorTools.DrawHeader("iPhone X"))
{
NGUIEditorTools.BeginContents();
{
GUILayout.BeginHorizontal();
NGUIEditorTools.SetLabelWidth(100f);
NGUIEditorTools.DrawProperty("ShowInSafeArea", serializedObject, "mShowInSafeArea", GUILayout.Width(120f));
GUILayout.Label("控制子节点的锚点在安全区域内显示");
GUILayout.EndHorizontal();
}
NGUIEditorTools.EndContents();
}
}
//......原来的逻辑....
}
GetSides와 mShowInSafeArea 호출 내부
보충:
실제 프로젝트에서 일부 노드는 UIAnchor에 의해 설정되므로 이 스크립트도
UIAnchor의 업데이트를 찾을 수 있도록 조정해야 합니다.
if (pc.clipping == UIDrawCall.Clipping.None)
{
// Panel has no clipping -- just use the screen's dimensions
float ratio = (mRoot != null) ? (float)mRoot.activeHeight / Screen.height * 0.5f : 0.5f;
mRect.xMin = -Screen.width * ratio;
mRect.yMin = -Screen.height * ratio;
mRect.xMax = -mRect.xMin;
mRect.yMax = -mRect.yMin;
}
여기에서는 Screen.width 및 height를 직접 사용하고 안전 영역 SafeArea.width 및 SafeArea.height로 변경합니다.
if (pc.clipping == UIDrawCall.Clipping.None)
{
// Panel has no clipping -- just use the screen's dimensions
float ratio = (mRoot != null) ? (float)mRoot.activeHeight / NGUITools.SafeArea.height * 0.5f : 0.5f;
mRect.xMin = -NGUITools.SafeArea.width * ratio * NGUITools.Simulate_iPhoneXScale;
mRect.yMin = -NGUITools.SafeArea.height * ratio;
mRect.xMax = -mRect.xMin;
mRect.yMax = -mRect.yMin;
}
이런 식으로 NGUI는 괜찮습니다.

812×375를 추가하면 직접 미리보기만 가능합니다!

위에서 2개의 온라인 프로젝트를 통과했는데 하나는 UGUI이고 다른 하나는 NGUI입니다.
효율적인 Unity3D 적응 iPhone X 기술 솔루션을 요약하면 모두가 무언가를 얻을 수 있기를 바랍니다.