1. 为什么你的UI总在异形屏上“翻车”
做移动端Unity开发的朋友,大概率都遇到过这种场景:测试机拿过来一跑,界面上的按钮被刘海挡住了一半,排行榜顶部的标题钻进了状态栏,横屏游戏里关键的技能按键直接陷进屏幕角落的“挖孔”区域。以前我们骂机型杂、怪厂商乱搞,后来发现根子其实在自己——没有做异形屏适配。
异形屏这个词,从iPhone X发布那天起就成了移动开发的必修课。刘海、水滴、挖孔、药丸,各种形态的屏幕切割方案层出不穷。Android这边更夸张,同样是挖孔屏,不同厂商的开孔位置、孔径、状态栏高度都不一致,甚至连系统导航栏是手势条还是三键,都会直接挤压你的UI布局。如果你还停留在“UI居中就完事”的思路,那我只能说,你还没被用户评论区的差评毒打过。
这篇内容要聊的,就是Unity生态里一个非常成熟的解决方案——SafeArea Helper插件。我会从异形屏的适配原理讲起,手把手拆解这个插件的实现思路,再给你一套不依赖插件、自己也能写出来的纯代码适配方案,最后把我在多个上线项目里踩过的坑一并倒出来。无论你是刚入行的Unity萌新,还是被线上问题搞到头秃的资深开发,这篇都能直接抄作业。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 异形屏适配的底层逻辑,其实就一句话
2.1 “安全区”到底是什么
苹果在iPhone X上首次引入了Safe Area(安全区)的概念。官方定义是:屏幕中不会被圆角、刘海、传感器区域和底部主屏幕指示器遮挡的矩形区域。换句话说,你的关键UI元素只要老老实实待在这个矩形里,就不会被任何物理结构挡住。
Android这边的对应概念叫DisplayCutout(显示切割区),从Android 9开始系统级支持。但问题在于,Android的碎片化导致各家厂商对cutout的实现并不完全统一。有的手机上报的cutout区域很准确,有的则需要你在manifest里手动声明才能拿到完整数据。
Unity引擎从2019.1版本开始,在Screen类里原生暴露了Screen.safeArea属性,返回的是一个Rect,对应当前屏幕的安全区矩形。这其实已经给了我们最基本的适配工具。但原生API只提供了数据,没有提供布局方案——拿到safeArea之后怎么用它调整UI,引擎不管,得你自己写。
2.2 为什么不能用“居中+留边距”糊弄过去
很多新手的第一反应是:我UI做小一点,四周多留点边距不就行了?这思路在特定机型上确实能跑通,但放到市场里就是灾难。
原因有三点。第一,不同设备的安全区差异巨大。iPhone 8的安全区上下几乎是满屏的,iPhone 14 Pro Max的顶部安全区和底部Home Indicator区域加起来能占到屏幕高度的15%以上。一张图用同一套边距去适配这两台机器,必然有一台被过度裁切或露出边角。第二,横竖屏切换会改变安全区的形状和位置,静态边距根本追不上。第三,系统导航栏的显示和隐藏会动态改变safeArea的值,如果你的UI布局不监听这个变化,切个输入法或者下拉个状态栏,界面直接错位。
适配的本质,是把布局计算从“写死”变成“动态”,以运行时拿到的safeArea数据为准,实时调整UI边界。
2.3 生产环境里三个无法绕开的现实问题
除了上述基础逻辑,实际项目里还有几个比较隐蔽的坑。一是Canvas的缩放模式会影响safeArea的计算基准。Unity的safeArea返回的是屏幕像素坐标,但你的UI是跑在Canvas下的,如果Canvas用了CanvasScaler的Scale With Screen Size模式,直接把safeArea的像素值赋给RectTransform是错的,必须做一次从屏幕坐标到Canvas坐标的换算,或者把safeArea换算成相对屏幕的归一化比例。
二是多分辨率适配和safeArea适配是两个维度。前者解决“屏幕大小不同”,后者解决“屏幕形状不同”,两者不能互相替代。只做多分辨率适配,遇上刘海屏照样穿帮;只做safeArea适配,不考虑不同分辨率下的UI缩放,元素大小又会失衡。
三是Android设备上safeArea数据的获取时机。部分国产机型在应用启动早期返回的safeArea值可能不准确,需要延迟几帧再读取,这个问题我在后面排查章节会细说。
3. SafeArea Helper插件到底做了什么
3.1 让你两条代码搞定适配的封装思路
SafeArea Helper(通常指Unity Asset Store上知名的开源实现,尤其是Jean Moreno那套)最大的价值,是解决了“拿到safeArea数据后怎么应用”的最后一公里问题。
它的核心思路非常清晰:把UI组件挂在Canvas下的一个专属节点上,插件在运行时自动读取Screen.safeArea,然后把这个安全区矩形转换成Canvas坐标系下的锚点,设置给UI节点的RectTransform。UI节点本身不需要固定大小,它的四个边会严格按照安全区的四条边来对齐。
用起来极简。Canvas下创建一个空节点,挂上SafeArea.cs脚本,把需要适配的UI元素丢到它的子节点下,运行时就自动避让了。如果你需要适配不同方向的屏幕,插件还提供了SafeArea.Edge枚举配置,可以单独控制是否适配顶部、底部、左侧、右侧,非常灵活。
3.2 内部实现拆解:一行行看它是怎么算的
我直接挑插件源码里的几个关键片段来拆,你自己写方案的时候也能参考。
csharp复制private RectTransform panel;
private Rect lastSafeArea = new Rect(0, 0, 0, 0);
private Vector2Int lastScreenSize = new Vector2Int(0, 0);
private ScreenOrientation lastOrientation = ScreenOrientation.AutoRotation;
private void Start()
{
panel = GetComponent<RectTransform>();
Refresh();
}
Start里拿到自身的RectTransform,然后调Refresh。注意这里的lastSafeArea和lastScreenSize、lastOrientation三个缓存变量,是用来做变化监听的——不是每帧都盲目刷新,只有safeArea变了、屏幕尺寸变了或者屏幕方向变了才重新计算,这是性能上的关键细节。
再看核心的计算逻辑:
csharp复制private void Refresh()
{
Rect safeArea = GetSafeArea();
if (safeArea == lastSafeArea
&& Screen.width == lastScreenSize.x
&& Screen.height == lastScreenSize.y
&& Screen.orientation == lastOrientation)
{
return;
}
Vector2 anchorMin = safeArea.position;
Vector2 anchorMax = safeArea.position + safeArea.size;
anchorMin.x /= Screen.width;
anchorMin.y /= Screen.height;
anchorMax.x /= Screen.width;
anchorMax.y /= Screen.height;
panel.anchorMin = anchorMin;
panel.anchorMax = anchorMax;
panel.offsetMin = Vector2.zero;
panel.offsetMax = Vector2.zero;
lastSafeArea = safeArea;
lastScreenSize = new Vector2Int(Screen.width, Screen.height);
lastOrientation = Screen.orientation;
}
这段代码最关键的是归一化处理:anchorMin取safeArea的起始坐标除以屏幕宽高,anchorMax取safeArea的终点坐标除以屏幕宽高,把像素坐标转成0到1的锚点比例。这样无论Canvas的缩放模式是Constant Pixel Size还是Scale With Screen Size,锚点比例都能天然适配,因为RectTransform的anchor本来就是按父节点比例来定位的。
然后把offsetMin和offsetMax都归零,意味着这个UI节点的边界完全贴住锚点边界,不多留一点空隙。之后你把按钮、面板放在这个SafeArea节点的子层级里,只要用锚点对齐到这个节点,它们就永远不会跑到安全区外面去。
3.3 横竖屏切换时插件是如何“自动”处理的
屏幕方向变化时,safeArea会变,Screen的宽高也会变,于是Refresh里的判断条件被触发,重新计算锚点。这里有个容易被忽略的细节:Unity在屏幕方向变化时,可能会先触发一次RectTransform的尺寸变化,但此时Screen.safeArea还没更新到最新值。所以插件在OnRectTransformDimensionsChange里也调用了Refresh,配合延迟更新逻辑(OnRectTransformDimensionsChange在布局变化时触发),确保即使拿到的是旧值,下一次Update里也会修正回来。
如果你自己在实现方案,我建议你至少同时监听两个回调:OnRectTransformDimensionsChange和OnOrientationChanged(可以用Screen.orientation在Update里轮询,或者用Application.onBeforeRender),双保险总不会错。
4. 从插件到自研:一套不依赖插件的完整适配代码
4.1 为什么我建议你自己写一套
SafeArea Helper插件本身很成熟,但有几个场景它解决不了。比如你要做的是一个不需要任何外部依赖的轻量工具库,或者你们项目里的UI框架对布局节点有自己的管理方式,又或者你需要的不是“避让出一个矩形”,而是“某个特定的悬浮按钮始终距离安全区边缘20像素”——插件那种整块面板式的适配,控制粒度就太粗了。
自研方案的思路也不复杂:封装一个安全的矩形获取方法,再做一个通用的UI对齐工具,甚至可以直接扩展你的基类UI脚本。这里我给出一套我实测过多个项目的实现,兼顾了竖屏、横屏、刘海屏和平板。
4.2 核心工具类:一次搞定Android和iOS的数据获取
csharp复制using UnityEngine;
public static class SafeAreaUtil
{
public static Rect GetSafeArea()
{
Rect safeArea = Screen.safeArea;
// Android部分机型在启动早期返回的safeArea有误,延迟校准
#if UNITY_ANDROID && !UNITY_EDITOR
if (safeArea.x == 0 && safeArea.y == 0
&& safeArea.width == Screen.width
&& safeArea.height == Screen.height)
{
// 可能是还没读取到cutout信息,先返回全屏,让调用方下一帧再取
return safeArea;
}
#endif
return safeArea;
}
public static Vector2 GetSafeAreaAnchorMin()
{
Rect safeArea = GetSafeArea();
return new Vector2(safeArea.xMin / Screen.width, safeArea.yMin / Screen.height);
}
public static Vector2 GetSafeAreaAnchorMax()
{
Rect safeArea = GetSafeArea();
return new Vector2(safeArea.xMax / Screen.width, safeArea.yMax / Screen.height);
}
}
这个工具类的核心价值是两个:一是屏蔽平台差异,不管你在iOS还是Android,直接调GetSafeAreaAnchorMin/Max拿到的就是归一化锚点;二是预留了Android安全机制,检测到返回全屏(说明数据不可信)时不强行适配,而是留给上层逻辑做延迟重试。
4.3 通用SafeArea组件:挂上就能用的版本
我实际项目里常用的组件写法如下:
csharp复制using UnityEngine;
[RequireComponent(typeof(RectTransform))]
public class UISafeAreaFitter : MonoBehaviour
{
public enum FitMode
{
All,
TopOnly,
BottomOnly,
LeftOnly,
RightOnly,
Custom
}
[SerializeField] private FitMode fitMode = FitMode.All;
[Tooltip("在安全区基础上再外扩或内缩的像素偏移,单位是Canvas坐标")]
[SerializeField] private Vector2 padding = Vector2.zero;
[Tooltip("是否在屏幕方向变化时自动刷新")]
[SerializeField] private bool autoRefresh = true;
private RectTransform rectTransform;
private Rect lastSafeArea;
private Vector2Int lastScreenSize;
private ScreenOrientation lastOrientation;
private void Awake()
{
rectTransform = GetComponent<RectTransform>();
Refresh();
}
private void Update()
{
if (!autoRefresh) return;
if (Screen.safeArea != lastSafeArea
|| Screen.width != lastScreenSize.x
|| Screen.height != lastScreenSize.y
|| Screen.orientation != lastOrientation)
{
Refresh();
}
}
private void Refresh()
{
Rect safeArea = SafeAreaUtil.GetSafeArea();
lastSafeArea = Screen.safeArea;
lastScreenSize = new Vector2Int(Screen.width, Screen.height);
lastOrientation = Screen.orientation;
Vector2 anchorMin = SafeAreaUtil.GetSafeAreaAnchorMin();
Vector2 anchorMax = SafeAreaUtil.GetSafeAreaAnchorMax();
// 根据模式裁剪不生效的边
if (fitMode == FitMode.TopOnly)
{
anchorMin.x = 0f;
anchorMin.y = SafeAreaUtil.GetSafeAreaAnchorMin().y;
anchorMax.x = 1f;
anchorMax.y = 1f;
}
else if (fitMode == FitMode.BottomOnly)
{
anchorMin.x = 0f;
anchorMin.y = 0f;
anchorMax.x = 1f;
anchorMax.y = SafeAreaUtil.GetSafeAreaAnchorMax().y;
}
// 其余模式类似,可自行扩展
rectTransform.anchorMin = anchorMin;
rectTransform.anchorMax = anchorMax;
rectTransform.offsetMin = new Vector2(padding.x, padding.y);
rectTransform.offsetMax = new Vector2(-padding.x, -padding.y);
}
// 运行时动态调整后手动调用
public void ManualRefresh()
{
Refresh();
}
#if UNITY_EDITOR
[ContextMenu("预览当前安全区")]
private void EditorPreview()
{
Refresh();
Debug.Log($"SafeArea: {Screen.safeArea}, AnchorMin: {rectTransform.anchorMin}, AnchorMax: {rectTransform.anchorMax}");
}
#endif
}
这里我额外加了几样插件原版没有的东西。一是FitMode,很多界面只需要避让顶部,不需要把底下也锁死。二是padding,安全区基础上再留一层呼吸空间,视觉上更协调。三是UnityEditor下加了个ContextMenu,开发时右键一键打印当前安全区参数,调试效率直接翻倍。
4.4 自研方案和插件在功能上的对比
| 能力项 | SafeArea Helper插件 | 自研方案 |
|---|---|---|
| 获取安全区数据 | 支持 | 支持 |
| 自动适配锚点 | 支持 | 支持 |
| 支持定向边适配 | 支持(枚举配置) | 支持(FitMode) |
| 支持内边距扩展 | 需额外写脚本 | 内置padding |
| 编辑器调试工具 | 有Preview场景 | ContextMenu预览 |
| 零外部依赖 | 需要导入插件 | 完全自包含 |
| Android早期误判处理 | 弱 | 强(延迟重试逻辑) |
5. 实战:不同布局场景下SafeArea怎么接
5.1 典型竖屏场景:登录页、首页、列表页
竖屏场景是最简单的,但也是出错率最高的。常见的错误是把整个页面背景也放进SafeArea节点里,导致背景四边出现黑边或色差。正确做法是:背景图铺满全屏,内容区放进SafeArea节点。
我的习惯是搭三层结构。第一层是background节点,锚点全屏,负责承载背景图和全屏特效。第二层是safeAreaContent节点,挂上UISafeAreaFitter,FitMode选All,所有会被刘海遮挡的交互元素(按钮、输入框、标题栏)都放这里。第三层是bottomBar节点,如果底部有虚拟按键,需要单独考虑。
这里注意一个细节:iOS底部横条(Home Indicator)区域虽然在安全区外,但Apple官方建议导航栏和工具栏的底线至少要和Home Indicator保持一定距离。Android三键导航在沉浸式模式下会悬浮在内容上方,safeArea会自动处理,但如果你用了非沉浸式模式,则要看具体机型的适配表现。
5.2 横屏游戏场景:操控区、HUD、技能栏
横屏比竖屏麻烦得多。iPhone横屏时,刘海在屏幕左侧,safeArea右侧会出现一个“耳朵区”,底部有Home Indicator区。Android挖孔屏横屏时,挖孔可能出现在左上角或右上角,不同厂商不一样。
我做过一个射击游戏,技能按钮在右下角,打了好几版都发现按钮被Home Indicator压住。后来把按钮单独挂了一个UISafeAreaFitter,FitMode设为BottomOnly,问题立刻解决。这里有个经验:横屏下不要把整个HUD面板统一用一个SafeArea节点,因为不同元素可能需要适配不同的边。左上角的队伍信息只需适配左边,右下角的技能按钮只需适配底部,各自挂独立的SafeArea组件,效果最好。
5.3 动态安全区:弹窗、键盘、影藏式导航
前面讲动态安全区,这里具体说明你什么时候会用到。最常见的是带输入框的聊天页:点输入框弹出系统键盘时,iOS会调整safeAreaInsets的底部值,Android的resize模式也会改变可用区域。如果你不监听safeArea变化并刷新布局,输入框就会被键盘挡死。
其次是游戏内弹窗。很多弹窗是全屏遮罩+居中卡片,卡片理论上在安全区内就不怕。但如果你做了“贴边抽屉”式的弹窗(从屏幕边缘滑出),必须实时对齐安全区。这时候我建议你在弹窗控制器里监听safeArea变化,驱动抽屉节点的position偏移,而不是依赖Update每帧刷。
还有一类是Android手势导航切换。用户在设置里从“三键导航”切到“手势导航”,系统导航栏高度会瞬间改变,safeArea的底部值也跟着变。如果你的UI没有监听,用户切完设置回来,界面就错位了。用我上面的UISafeAreaFitter组件,在Update里检测safeArea变化会自动刷新,稳得很。
6. SafeArea适配的自动化与测试体系
6.1 怎么在编辑器里模拟各种“刘海”
真机测试是最准确的,但全机型覆盖不现实。好在Unity提供了模拟方案。
- Game视图的Device Simulator:Unity 2021.2以上自带Device Simulator,可以在编辑器里模拟iPhone 14 Pro、Pixel 7等设备的刘海和挖孔,safeArea值也会同步。这个工具强烈推荐,基本能覆盖大部分游戏机的适配验证。
- SafeArea Helper插件的Simulator组件:插件自带一个
Simulator脚本(在Simulator文件夹下),允许你手动拖拽一个矩形区域模拟safeArea。不管用什么引擎版本都能用。 - 自定义模拟器:如果你用自研方案,可以在编辑器里加一个Overlay窗口,手动输入safeArea的xywh,强制赋值给
Screen.safeArea的模拟类,方便测试特定设备数据。
我用得最多的是Device Simulator,它不只模拟safeArea,还模拟分辨率、DPI、屏幕方向,甚至能模拟屏幕刷新率。团队里新同学上手,我会要求他们至少把Device Simulator榜单里的TOP20机型跑一遍主界面,再提测。
6.2 一套可落地的适配自检清单
我在项目验收时有个checklist,分享出来给大家参考:
| 检查项 | 方法 | 通过标准 |
|---|---|---|
| 顶部避让 | 模拟iPhone 14 Pro刘海屏 | 标题、状态图标距安全区上沿≥0 |
| 底部避让 | 模拟iPhone 14 Pro Home Indicator | 按钮、TabBar不被遮挡 |
| 系统键盘弹出 | 真机调出输入法 | 输入框不被键盘遮挡,即时上移 |
| 横屏切换 | 模拟Pixel 7挖孔横屏 | 技能按钮、HUD不被挖孔遮挡 |
| Android手势导航切换 | 真机切换导航模式 | 底部布局实时适配,无错位 |
| 全屏播放视频 | 模拟全屏横屏 | 播放器控件在安全区内 |
| 多分辨率 | 模拟iPad和低端安卓机 | 元素无分散、无重叠、无裁切 |
这套清单看着简单,但真能全过的项目不多。尤其是“多分辨率”那条,很多人只测UI有没有散架,忽略了safeArea变化导致的元素重叠。
6.3 线上监控safeArea导致的崩溃
这一块很少有人聊,但我是踩过坑的。某些低端Android机型在特定系统版本下,Screen.safeArea会在屏幕方向切换的瞬间返回一个超出屏幕范围的值(比如x为负数,宽高为0),如果你的UI代码拿到这个值去做RectTransform设置,会造成锚点异常甚至UI渲染崩溃。
我在工具类里加了一层安全校验:如果safeArea的xMin或yMin小于0、xMax或yMax大于屏幕宽高、宽高为0,就回退到上一次有效的safeArea值,并打一条警告日志。这招帮我挡掉过不少线上问题。
csharp复制public static Rect GetSafeAreaWithFallback(Rect fallback)
{
Rect safeArea = Screen.safeArea;
bool invalid = safeArea.xMin < 0 || safeArea.yMin < 0
|| safeArea.xMax > Screen.width || safeArea.yMax > Screen.height
|| safeArea.width <= 0 || safeArea.height <= 0;
return invalid ? fallback : safeArea;
}
7. 常见问题与排查技巧实录
7.1 适配了safeArea还是被遮挡?检查这三处
如果你确定代码里已经正确设置了safeArea锚点,但界面上元素还是被刘海遮住,按顺序排查三个地方。第一,Canvas的Render Mode是不是Screen Space Overlay,如果是,safeArea锚点能生效;如果是Screen Space Camera,要确认UI相机是否在渲染层的正确位置。第二,CanvasScaler的Scale Mode,如果用Scale With Screen Size且Match不为0或1,计算缩放时safeArea需要额外处理,我的方案里直接用归一化锚点绕开了这个问题,但如果你是直接换算Canvas坐标,这块最容易出错。第三,父节点层级——检查你的UISafeAreaFitter挂在哪个节点上,如果它挂在了一个本身设置了固定锚点的子节点上,那么它的RectTransform会被父节点的锚点影响,导致safeArea计算被“叠加”错误。
7.2 两个永不过时的调试技巧:画Gizmos和读日志
开发safeArea适配时,我强烈建议在编辑器里画出安全区矩形和UI节点的实际边界。Unity的OnDrawGizmos可以用在任意MonoBehaviour上,不需要运行Build就能看到。
csharp复制#if UNITY_EDITOR
private void OnDrawGizmos()
{
if (!Application.isPlaying) return;
// 画safeArea矩形
Rect safeArea = Screen.safeArea;
Vector3 center = new Vector3(safeArea.x + safeArea.width / 2f,
safeArea.y + safeArea.height / 2f, 0f);
Vector3 size = new Vector3(safeArea.width, safeArea.height, 0f);
Gizmos.color = Color.green;
Gizmos.DrawWireCube(center, size);
}
#endif
日志方面,在Refresh里加一行关键数据输出:
csharp复制Debug.Log($"[SafeArea] screen={Screen.width}x{Screen.height}, safeArea={Screen.safeArea}, orientation={Screen.orientation}");
线上版本可以加个宏控制日志开关,开发版默认打开,这样测试反馈问题时,你直接要日志就能定位是设备数据问题还是布局逻辑问题。
7.3 针对典型问题的排查速查表
| 现象 | 可能原因 | 排除方式 |
|---|---|---|
| 竖屏正常,横屏错乱 | 未监听方向变化,或safeArea获取太早 | 确认Update刷新逻辑;横屏在第一帧延迟2~3帧再取safeArea |
| Android部分机型顶部有空隙 | 状态栏透明/沉浸式配置不一致 | 检查Mainfest主题,确认使用全屏+透明状态栏 |
| iOS刘海屏底部和左右有空隙 | 未适配不同机型的safeArea差异 | 真机日志看Screen.safeArea具体值,和模拟器对比 |
| UI元素在safeArea内仍重叠 | 忽略了CanvasScaler缩放比例 | 检查Canvas的referenceResolution和Match值 |
| 切后台再回来UI错位 | 生命周期中safeArea刷新时机丢失 | 在OnApplicationFocus回调里强制刷新 |
| Android键盘弹出时bottomBar上移 | 未监听safeArea变化 | 在safeArea变化时驱动底栏位移,而非固定锚点 |
| 部分折叠屏展开后错乱 | 屏幕尺寸变化导致safeArea重算失败 | 在OnRectTransformDimensionsChange里强制Refresh |
7.4 Android厂商碎片化的真实现状
最后说点实际的。Android异形屏适配最大的敌人不是技术,是碎片化。同一款手机,不同系统版本返回的safeArea都不一样;同一个系统版本,不同厂商的cutout实现也可能有差异。华为、小米、OPPO、vivo、三星,每家都有自己的刘海策略。
我建议你在项目里维护一个设备白名单和黑名单机制。比如某些老机型返回的safeArea明显异常(比如顶部安全区高度高达400像素),你可以针对这些机型单独覆盖一个预期safeArea值。这个机制不复杂,就是在工具类里加一个Dictionary<string, Rect>,按机型覆盖。我先说清楚,用机型判断是下策,能用系统API就用系统API,但真遇到系统API不顶用的情况,白名单救急还是很有效的。
8. 我的个人体会:适配这事,宜早不宜迟
做移动端Unity开发这几年,SafeArea适配是我见过的“技术含量不高但坑极多”的领域之一。它的原理简单到一句话能说清,但覆盖的设备矩阵、系统版本、用户习惯排列组合起来,测试量能让人头皮发麻。
如果你正在一个新项目初期,我的建议是把safeArea适配作为基础框架的一部分去建设,而不是等UI做完了再补。等界面全做完了再适配,就会发现所有写死的坐标、所有自以为是的居中、所有偷懒留的白边,都要返工。反过来,如果你前期就定下“所有关键UI一律走SafeArea组件”的规矩,后期基本不会再被异形屏问题找上门。
另外一个小技巧分享给做独立游戏的朋友:测试机不用买太多,一台iOS刘海屏、一台Android挖孔屏、一台老款16:9设备、一台平板,基本能覆盖90%的适配问题。剩下的,用Device Simulator跑一跑,线上监控再兜个底,就够了。适配这事儿不追求完美,但求关键界面不翻车。
