using UnityEngine; namespace Crystal { /// /// Safe area implementation for notched mobile devices. Usage: /// (1) Add this component to the top level of any GUI panel. /// (2) If the panel uses a full screen background image, then create an immediate child and put the component on that instead, with all other elements childed below it. /// This will allow the background image to stretch to the full extents of the screen behind the notch, which looks nicer. /// (3) For other cases that use a mixture of full horizontal and vertical background stripes, use the Conform X & Y controls on separate elements as needed. /// public class SafeArea : MonoBehaviour { #region Simulations /// /// Simulation device that uses safe area due to a physical notch or software home bar. For use in Editor only. /// public enum SimDevice { /// /// Don't use a simulated safe area - GUI will be full screen as normal. /// None, /// /// Simulate the iPhone X and Xs (identical safe areas). /// iPhoneX, /// /// Simulate the iPhone Xs Max and XR (identical safe areas). /// iPhoneXsMax, /// /// Simulate the Google Pixel 3 XL using landscape left. /// Pixel3XL_LSL, /// /// Simulate the Google Pixel 3 XL using landscape right. /// Pixel3XL_LSR } /// /// Simulation mode for use in editor only. This can be edited at runtime to toggle between different safe areas. /// public static SimDevice Sim = SimDevice.None; /// /// Normalised safe areas for iPhone X with Home indicator (ratios are identical to iPhone Xs). Absolute values: /// PortraitU x=0, y=102, w=1125, h=2202 on full extents w=1125, h=2436; /// PortraitD x=0, y=102, w=1125, h=2202 on full extents w=1125, h=2436 (not supported, remains in Portrait Up); /// LandscapeL x=132, y=63, w=2172, h=1062 on full extents w=2436, h=1125; /// LandscapeR x=132, y=63, w=2172, h=1062 on full extents w=2436, h=1125. /// Aspect Ratio: ~19.5:9. /// Rect[] NSA_iPhoneX = new Rect[] { new Rect (0f, 102f / 2436f, 1f, 2202f / 2436f), // Portrait new Rect (132f / 2436f, 63f / 1125f, 2172f / 2436f, 1062f / 1125f) // Landscape }; /// /// Normalised safe areas for iPhone Xs Max with Home indicator (ratios are identical to iPhone XR). Absolute values: /// PortraitU x=0, y=102, w=1242, h=2454 on full extents w=1242, h=2688; /// PortraitD x=0, y=102, w=1242, h=2454 on full extents w=1242, h=2688 (not supported, remains in Portrait Up); /// LandscapeL x=132, y=63, w=2424, h=1179 on full extents w=2688, h=1242; /// LandscapeR x=132, y=63, w=2424, h=1179 on full extents w=2688, h=1242. /// Aspect Ratio: ~19.5:9. /// Rect[] NSA_iPhoneXsMax = new Rect[] { new Rect (0f, 102f / 2688f, 1f, 2454f / 2688f), // Portrait new Rect (132f / 2688f, 63f / 1242f, 2424f / 2688f, 1179f / 1242f) // Landscape }; /// /// Normalised safe areas for Pixel 3 XL using landscape left. Absolute values: /// PortraitU x=0, y=0, w=1440, h=2789 on full extents w=1440, h=2960; /// PortraitD x=0, y=0, w=1440, h=2789 on full extents w=1440, h=2960; /// LandscapeL x=171, y=0, w=2789, h=1440 on full extents w=2960, h=1440; /// LandscapeR x=0, y=0, w=2789, h=1440 on full extents w=2960, h=1440. /// Aspect Ratio: 18.5:9. /// Rect[] NSA_Pixel3XL_LSL = new Rect[] { new Rect (0f, 0f, 1f, 2789f / 2960f), // Portrait new Rect (0f, 0f, 2789f / 2960f, 1f) // Landscape }; /// /// Normalised safe areas for Pixel 3 XL using landscape right. Absolute values and aspect ratio same as above. /// Rect[] NSA_Pixel3XL_LSR = new Rect[] { new Rect (0f, 0f, 1f, 2789f / 2960f), // Portrait new Rect (171f / 2960f, 0f, 2789f / 2960f, 1f) // Landscape }; #endregion RectTransform Panel; Rect LastSafeArea = new Rect (0, 0, 0, 0); [SerializeField] bool ConformX = true; // Conform to screen safe area on X-axis (default true, disable to ignore) [SerializeField] bool ConformY = true; // Conform to screen safe area on Y-axis (default true, disable to ignore) void Awake () { Panel = GetComponent (); if (Panel == null) { Debug.LogError ("Cannot apply safe area - no RectTransform found on " + name); Destroy (gameObject); } Refresh (); } void Update () { Refresh (); } void Refresh () { Rect safeArea = GetSafeArea (); if (safeArea != LastSafeArea) ApplySafeArea (safeArea); } Rect GetSafeArea () { Rect safeArea = Screen.safeArea; if (Application.isEditor && Sim != SimDevice.None) { Rect nsa = new Rect (0, 0, Screen.width, Screen.height); switch (Sim) { case SimDevice.iPhoneX: if (Screen.height > Screen.width) // Portrait nsa = NSA_iPhoneX[0]; else // Landscape nsa = NSA_iPhoneX[1]; break; case SimDevice.iPhoneXsMax: if (Screen.height > Screen.width) // Portrait nsa = NSA_iPhoneXsMax[0]; else // Landscape nsa = NSA_iPhoneXsMax[1]; break; case SimDevice.Pixel3XL_LSL: if (Screen.height > Screen.width) // Portrait nsa = NSA_Pixel3XL_LSL[0]; else // Landscape nsa = NSA_Pixel3XL_LSL[1]; break; case SimDevice.Pixel3XL_LSR: if (Screen.height > Screen.width) // Portrait nsa = NSA_Pixel3XL_LSR[0]; else // Landscape nsa = NSA_Pixel3XL_LSR[1]; break; default: break; } safeArea = new Rect (Screen.width * nsa.x, Screen.height * nsa.y, Screen.width * nsa.width, Screen.height * nsa.height); } return safeArea; } void ApplySafeArea (Rect r) { LastSafeArea = r; // Ignore x-axis? if (!ConformX) { r.x = 0; r.width = Screen.width; } // Ignore y-axis? if (!ConformY) { r.y = 0; r.height = Screen.height; } // Convert safe area rectangle from absolute pixels to normalised anchor coordinates Vector2 anchorMin = r.position; Vector2 anchorMax = r.position + r.size; anchorMin.x /= Screen.width; anchorMin.y /= Screen.height; anchorMax.x /= Screen.width; anchorMax.y /= Screen.height; Panel.anchorMin = anchorMin; Panel.anchorMax = anchorMax; Debug.LogFormat ("New safe area applied to {0}: x={1}, y={2}, w={3}, h={4} on full extents w={5}, h={6}", name, r.x, r.y, r.width, r.height, Screen.width, Screen.height); } } }