高效的iPhone X適配技術方案(UGUI和NGUI)
來自專欄 UWA:簡單優化、優化簡單
本文作者旨在通過改錨點的方式,分別實現在NGUI和UGUI上的iPhone X適配技術方案,並結合自身項目經驗,闡述了主要的實現細節,希望能對廣大遊戲開發團隊有借鑒意義。
適配來源: 按照蘋果官方人機界面指南 :
Human Interface Guidelines在iPhone X 異形屏幕上,蘋果提出了Safe Area安全區的概念,這個安全區域的意思是,UI在Safe Area能夠保證顯示不會被裁切掉。
按照蘋果的設計規範,要求我們把UI控制項放在Safe Area內,而且不能留黑邊。在Unity中就需要解決,怎麼以更少的工作量把所有界面的控制項停靠在Safe Area內,黑邊的部分用場景或者背景圖填充。當我們橫持iPhoneX的時候:
iPhone X整體像素為2436 x 1125像素;整體SafeArea區域為2172 x 1062像素;左右插槽(齊劉海和圓角,再加一個邊距)各132像素;
底部邊距(由於iPhoneX沒有Home鍵,會有一個虛擬的主屏幕的指示條)主屏幕的指示條佔用63像素高度,頂部沒有邊界是0像素。
一、技術方案
1.改相機ViewPort
直接把UI相機的視口改為Rect(132/2436, 0, 2172/2436, 1062/1125),然後把背景圖設為另外一個相機。這樣做的好處是,完全不用改原來的Layout。壞處是,多個UI的情況下,背景圖和主UI之間的深度關係要重新設置。2.縮放
把主UI的Scale設為0.9,背景圖的Scale設為1.1,這樣就能不留黑邊。這個方法的好處是簡單,壞處是會引起一些Tween已及Active/InActive切換之間的問題。3.改錨點
分2種情況,NGUI和UGUI都有點不同。正好我都有2個項目的完整適配經驗,所以才寫了這個分享。
二、實現細節
首先我們拿到iPhone X 安全區域,Unity得開發插件OC代碼來獲取。SafeArea.mm拷貝到項目的Plugins/iOS目錄中。
//獲取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;}
設計通用的適配component,哪些面板要適配,就直接添加這個腳本:
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); }}
比如這樣,給Panel加了Safe Area Panel這個組件,勾選Simulate_X模擬iPhone X運行。
運行時圖(紅色區域是UI主面板正常是全屏的,這裡根據Safe Area,自動適配後調整錨點展示的左右邊距下邊距,最底層藍色區域是場景或者UI背景圖區域)。
添加一個812x375就可以模擬iPhoneX的效果:
如果是舊項目是使用NGUI來開發的,原理一樣,也得用到以上Safa Area.mm來獲取安全區域,不同處在於修改NGUI的源碼,而NGUI版本有好多。不要忘記把SafeArea.mm拷貝到項目的Plugins/iOS目錄中。我提供思路和核心代碼,需要你結合自己使用的NGUI來修改。
NGUI中UI Sprite、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為Camera寫的擴展方法。
找到NGUITools.cs的static 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 doesnt affect the cameras 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和UIRectEditor的相關方法:
1.在UIRect.cs中添加[HideInInspector][SerializeField]public bool mShowInSafeArea = false;
2.修改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; } }
3.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(); } } //......原來的邏輯.... }
4.GetSides的調用加上mShowInSafeArea。
補充:實際項目中,部分節點是UIAnchor來設置,所以這個腳本也要適配找到UIAnchor的UpDate。if (pc.clipping == UIDrawCall.Clipping.None){ // Panel has no clipping -- just use the screens 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;}
5.這裡都是直接使用Screen.width和Height,要改成安全區域Safe Area.width和Safe Area.height。
if (pc.clipping == UIDrawCall.Clipping.None){ // Panel has no clipping -- just use the screens 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也就可以了。
添加一個812x375就只可以直接預覽:
以上,因為我的兩個上線項目恰好分別適配了UGUI和NGUI,所以根據經驗,總結了高效的Unity3D適配iPhone X技術方案,希望大家能有收穫。
文末,再次感謝Jeff的分享,如果您有任何獨到的見解或者發現也歡迎聯繫我們,一起探討。(QQ群:465082844)。
也歡迎大家來積极參与U Sparkle開發者計劃,簡稱"US",代表你和我,代表UWA和開發者在一起!
推薦閱讀:
※Redlang 開箱筆記
※GUI 常用元素中英對照表 / 1. Progress Indicator
※當沒有GUI的時候用戶要怎麼操作
※GacUI 支持運行時切換語言