📗

【Unity】iPhoneやAndroidのノッチ対応

2024/08/29に公開

はじめに

初めての方も、そうでない方もこんにちは!
現役ゲームプログラマーのたむぼーです。
自己紹介を載せているので、気になる方は見ていただければ嬉しいです!

今回は
 iPhoneやAndroidのノッチ対応
について、僕が実践している方法紹介します

https://zenn.dev/tmb/articles/1072f8ea010299

ノッチについて

ノッチとは、スマートフォンの画面上部の切り欠きで、カメラやセンサーを収めるために使われ領域のこと

SafeAreaAdjusterについて

ノッチ対応の基底クラス
SafeAreaAdjusterBase.cs
using UnityEngine;
using Cysharp.Threading.Tasks;

namespace UI
{
    abstract public class SafeAreaAdjusterBase : MonoBehaviour
    {
        protected RectTransform _rectTransform;
        protected ScreenData _lastScreenData;

        private bool _isSyncScreen;

        /// <summary>
        /// 破壊時
        /// </summary>
        private void OnDestroy()
        {
            _isSyncScreen = false;
        }

        /// <summary>
        /// ScreenData
        /// </summary>
        protected struct ScreenData
        {
            public int width;
            public int height;
            public Rect safeArea;

            public override readonly bool Equals(object obj)
            {
                if (obj is not ScreenData)
                {
                    return false;
                }

                ScreenData other = (ScreenData)obj;
                return width == other.width && height == other.height && safeArea == other.safeArea;
            }

            /// <summary>
            /// GetHashCode
            /// </summary>
            public override readonly int GetHashCode()
            {
                return base.GetHashCode();
            }
        }

        /// <summary>
        /// 起動時
        /// </summary>
        private void Awake()
        {
            _rectTransform = GetComponent<RectTransform>();

            ScreenData screenData = GetScreenData();
            UpdateSafeArea(screenData);
            SetLastScreenData(screenData);

#if UNITY_EDITOR
            SnckScreenAsync().Forget();
#endif
        }

        /// <summary>
        /// スクリーンデータの取得
        /// </summary>
        protected ScreenData GetScreenData()
        {
            ScreenData screenData = new ScreenData
            {
                width = Screen.width,
                height = Screen.height,
                safeArea = Screen.safeArea
            };
            return screenData;
        }

        /// <summary>
        /// セーフエリアに入れる
        /// </summary>
        protected virtual void UpdateSafeArea(ScreenData screenData)
        {
            // 継承先で実装
        }

        /// <summary>
        /// ラストデータの更新
        /// </summary>
        private void SetLastScreenData(ScreenData screenData)
        {
            _lastScreenData = screenData;
        }

#if UNITY_EDITOR
        /// <summary>
        /// 非同期で画面同期
        /// </summary>
        private async UniTaskVoid SnckScreenAsync()
        {
            _isSyncScreen = true;
            while (_isSyncScreen)
            {
                await UniTask.Yield(PlayerLoopTiming.Update);

                ScreenData screenData = GetScreenData();
                if (!screenData.Equals(_lastScreenData))
                {
                    UpdateSafeArea(screenData);
                    SetLastScreenData(screenData);
                }
            }
        }
#endif
    }
}
基本的なノッチ対応クラス
SafeAreaAdjusterForPanel.cs
using UnityEngine;

namespace UI
{
    sealed public class SafeAreaAdjusterForPanel : SafeAreaAdjusterBase
    {
        public enum SafeAreaMode
        {
            None,
            Vertical,
            Horizontal,
            Both
        }

        [Header("セーフモード")]

        [SerializeField]
        private SafeAreaMode _safeAreaMode = SafeAreaMode.Both;

        [Space]
        [Header("背景")]

        [SerializeField]
        private SafeAreaMode _backSafeAreaMode = SafeAreaMode.None;

        [SerializeField]
        private bool _isBackExpansion = false;

        [SerializeField]
        private RectTransform _backImage;

        /// <summary>
        /// セーフエリア内に収めえる
        /// </summary>
        protected override void UpdateSafeArea(ScreenData screenData)
        {
            if (_rectTransform == null)
            {
                return;
            }

            Vector2 anchorMin = screenData.safeArea.position;
            Vector2 anchorMax = screenData.safeArea.position + screenData.safeArea.size;

            anchorMin.x /= screenData.width;
            anchorMin.y /= screenData.height;
            anchorMax.x /= screenData.width;
            anchorMax.y /= screenData.height;

            Vector2 newAnchorMin = _rectTransform.anchorMin;
            Vector2 newAnchorMax = _rectTransform.anchorMax;

            switch (_safeAreaMode)
            {
                case SafeAreaMode.Vertical:
                    newAnchorMin.y = anchorMin.y;
                    newAnchorMax.y = anchorMax.y;
                    break;
                case SafeAreaMode.Horizontal:
                    newAnchorMin.x = anchorMin.x;
                    newAnchorMax.x = anchorMax.x;
                    break;
                case SafeAreaMode.Both:
                    newAnchorMin = anchorMin;
                    newAnchorMax = anchorMax;
                    break;
            }

            _rectTransform.anchorMin = newAnchorMin;
            _rectTransform.anchorMax = newAnchorMax;

            // 背景は含めたくない場合があるので別処理
            if (_backImage != null)
            {
                Vector2 backAnchorMin = _backImage.anchorMin;
                Vector2 backAnchorMax = _backImage.anchorMax;

                if (_isBackExpansion)
                {
                    switch (_backSafeAreaMode)
                    {
                        case SafeAreaMode.Vertical:
                            backAnchorMin.y = 0;
                            backAnchorMax.y = 1;
                            backAnchorMin.x = -0.1f;
                            backAnchorMax.x = 1.1f;
                            break;
                        case SafeAreaMode.Horizontal:
                            backAnchorMin.x = 0;
                            backAnchorMax.x = 1;
                            backAnchorMin.y = -0.1f;
                            backAnchorMax.y = 1.1f;
                            break;
                        case SafeAreaMode.Both:
                            backAnchorMin = new Vector2(-0.1f, -0.1f);
                            backAnchorMax = new Vector2(1.1f, 1.1f);
                            break;
                    }
                }
                else
                {
                    switch (_backSafeAreaMode)
                    {
                        case SafeAreaMode.Vertical:
                            backAnchorMin.y = 0;
                            backAnchorMax.y = 1;
                            backAnchorMin.x = anchorMin.x;
                            backAnchorMax.x = anchorMax.x;
                            break;
                        case SafeAreaMode.Horizontal:
                            backAnchorMin.x = 0;
                            backAnchorMax.x = 1;
                            backAnchorMin.y = anchorMin.y;
                            backAnchorMax.y = anchorMax.y;
                            break;
                        case SafeAreaMode.Both:
                            backAnchorMin = Vector2.zero;
                            backAnchorMax = Vector2.one;
                            break;
                    }
                }

                _backImage.anchorMin = backAnchorMin;
                _backImage.anchorMax = backAnchorMax;
            }
        }
    }
}
変数 説明
SafeAreaMode _safeAreaMode セーフモード
SafeAreaMode _backSafeAreaMode 背景のセーフモード
bool _isBackExpansion セーフモードの外に出すか
RectTransform _backImage 背景のRectTransform
左右のどちらかに付くノッチ対応クラス
SafeAreaAdjusterForOneSide.cs
using UnityEngine;

namespace UI
{
    sealed public class SafeAreaAdjusterForOneSide : SafeAreaAdjusterBase
    {
        public enum SafeAreaAlignment
        {
            Left,
            Right,
        }

        [Header("セーフモード")]

        [SerializeField]
        private SafeAreaAlignment _safeAreaAlignment = SafeAreaAlignment.Left;

        [Header("背景")]

        [SerializeField]
        private RectTransform _backImage;

        /// <summary>
        /// 左右のみセーフエリアに入れる
        /// </summary>
        protected override void UpdateSafeArea(ScreenData screenData)
        {
            if (_rectTransform == null)
            {
                return;
            }

            Vector2 anchorMin = _rectTransform.anchorMin;
            Vector2 anchorMax = _rectTransform.anchorMax;

            float safeAreaLeft = screenData.safeArea.xMin / screenData.width;
            float safeAreaRight = screenData.safeArea.xMax / screenData.width;

            switch (_safeAreaAlignment)
            {
                case SafeAreaAlignment.Left:
                    anchorMin.x = safeAreaLeft;
                    anchorMax.x = 1.0f;
                    break;

                case SafeAreaAlignment.Right:
                    anchorMin.x = 0.0f;
                    anchorMax.x = safeAreaRight;
                    break;
            }

            _rectTransform.anchorMin = anchorMin;
            _rectTransform.anchorMax = anchorMax;

            // 背景は含めたくない場合があるので別処理
            if (_backImage != null)
            {
                Vector2 backgroundAnchorMin = _backImage.anchorMin;
                Vector2 backgroundAnchorMax = _backImage.anchorMax;

                switch (_safeAreaAlignment)
                {
                    case SafeAreaAlignment.Left:
                        backgroundAnchorMin.x = -safeAreaLeft;
                        backgroundAnchorMax.x = safeAreaLeft;
                        break;

                    case SafeAreaAlignment.Right:
                        backgroundAnchorMin.x = safeAreaRight;
                        backgroundAnchorMax.x = 1.0f + (1.0f - safeAreaRight);
                        break;
                }

                _backImage.anchorMin = backgroundAnchorMin;
                _backImage.anchorMax = backgroundAnchorMax;
            }
        }
    }
}
変数 説明
SafeAreaAlignment _safeAreaAlignment セーフモード
RectTransform _backImage 背景のRectTransform

使い方

対象のUIにパネルを作り、そのパネルにアタッチする。
セーフエリア対応したいUIを、そのパネルの子にすることで対応する

基本的には、SafeAreaAdjusterForPanelで良いが特別な処理をしたい場合は、SafeAreaAdjusterForOneSideのようにSafeAreaAdjusterBaseを継承して作成する

Discussion