查看原文
其他

提醒!7月起必须适配刘海屏,这份高效的iPhone X适配方案看一下?| 干货集锦

Jeff 七麦研究院 2019-07-05

七麦研究院 特邀作者 - Jeff 

Jeff

Jeff


5 月 7 日,苹果在开发者后台发布通知,2018 年 7 月起,所有 iOS 应用必须适配 iOS11 系统,并且必须支持 iPhone X 的超级 Retina 显示。


距离 7 月还有一周时间,七麦研究院再次提醒还没有完成适配的开发者需要尽快更新。


另外,今天给大家分享一篇《高效的 iPhone X 适配技术方案》,作者通过改锚点的方式,分别实现在 NGUI 和 UGUI 上 iPhone X 适配技术方案,并结合自身项目经验,阐述了主要的实现细节,希望对广大的游戏开发者有借鉴意义。


适配来源: 按照苹果官方人机界面指南 :
https://developer.apple.com/ios/human-interface-guidelines/overview/iphone-x/


在 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 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 和 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 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;
}


5.这里都是直接使用 Screen.width 和 Height,要改成安全区域 Safe Area.width 和 Safe Area.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 也就可以了。



添加一个 812x375 就只可以直接预览:



以上,因为我的两个上线项目恰好分别适配了 UGUI 和 NGUI,所以根据经验,总结了高效的 Unity3D 适配 iPhone X 技术方案,希望大家能有收获。


《干货集锦》第九期就到这里啦~ 欢迎留言说出最近困扰哦~


- end -


本文由七麦研究院特邀作者【 Jeff 】原创,首发于公众号侑虎科技(ID:uwatech)转载需联系 Jeff 获取授权,七麦研究院有权向非授权转载追究责任。


查看往期精彩文章


抖音带火的又一款魔性小游戏“黑洞大作战”强势上榜!


约谈、下架、停更、关停……整改消息扑面而来,这些坑不能再跳了!


世界杯竞猜持续升温,你的手机屏幕是否已被“赌球” App 占据?


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存