Search Unity

  1. Are you interested in providing feedback directly to Unity teams? Sign up to become a member of Unity Pulse, our new product feedback and research community.
    Dismiss Notice

CanvasHelper resizes a RectTransform to iPhone X's safe area

Discussion in 'iOS and tvOS' started by _Adriaan, Mar 9, 2018.

  1. drorriov

    drorriov

    Joined:
    Jun 7, 2014
    Posts:
    42
    Thanks.

    With Android you can set Player -> Resolution and Presentation -> Render outside the safe area (to false) and from virtual tests I did it seems it automatically place blank link beside the notch. Unless i missed something here..

    Thanks for the video, that would be great in the next unity release!
     
  2. Martin_Gonzalez

    Martin_Gonzalez

    Joined:
    Mar 25, 2012
    Posts:
    358
    What about doing it as a Unity package with a github url?

    Another question, is it necessary to have it in the Update, beside the orientation checks?
     
    Last edited: Dec 6, 2019
  3. _Adriaan

    _Adriaan

    Joined:
    Nov 12, 2009
    Posts:
    469
    If you want the safe area rect to update when you change your phone's orientation, yes. 'Back in the day', it was also possible that SafeArea didn't return a correct safe area on startup, so that required it to be in Update() as well.

    Regardless, removing this Update() is such a small optimisation that you really shouldn't bother.
     
  4. KevinBunn

    KevinBunn

    Joined:
    Oct 4, 2019
    Posts:
    1
    Just wanted to post and say thanks! Saved me a lot of time.
     
    _Adriaan likes this.
  5. OldKing_Wang

    OldKing_Wang

    Joined:
    Jan 25, 2015
    Posts:
    25
    thanks @_Adriaan code. :)

    here is my simplify version:

    just attach it to any RectTransform node.

    Code (CSharp):
    1.  
    2. public class FitRectTransformToSafeArea : MonoBehaviour
    3.     {
    4.         private void Awake() { DoRunLogic(); }
    5.  
    6.         public void ManualRun() { DoRunLogic(); }
    7.  
    8.         private void DoRunLogic()
    9.         {
    10.             var selfRectTransform = GetComponent<RectTransform>();
    11.             if (selfRectTransform == null)
    12.             {
    13.                 Debug.Log($"{name} no RectTransform attached");
    14.                 return;
    15.             }
    16.  
    17.             var rootCanvas = FindRootCanvas();
    18.             if (rootCanvas == null) return;
    19.  
    20.             var safeArea = Screen.safeArea;
    21.  
    22.             var anchorMin = safeArea.position;
    23.             var anchorMax = safeArea.position + safeArea.size;
    24.             anchorMin.x /= rootCanvas.pixelRect.width;
    25.             anchorMin.y /= rootCanvas.pixelRect.height;
    26.             anchorMax.x /= rootCanvas.pixelRect.width;
    27.             anchorMax.y /= rootCanvas.pixelRect.height;
    28.  
    29.             selfRectTransform.anchorMin = anchorMin;
    30.             selfRectTransform.anchorMax = anchorMax;
    31.         }
    32.  
    33.         private Canvas FindRootCanvas()
    34.         {
    35.             var nowTrans = transform.parent;
    36.             while (true)
    37.             {
    38.                 if (nowTrans == null)
    39.                 {
    40.                     Debug.LogError($"{name} no Canvas root found");
    41.                     return null;
    42.                 }
    43.  
    44.                 var canvas = nowTrans.GetComponent<Canvas>();
    45.                 if (canvas != null) return canvas;
    46.                 nowTrans = nowTrans.parent;
    47.             }
    48.         }
    49.     }
    50.  
     
  6. _Adriaan

    _Adriaan

    Joined:
    Nov 12, 2009
    Posts:
    469
    I updated the script in the original post today to include the check for ResolutionChanged() on mobile devices too. Your game will otherwise act weird on Chromebooks running Android apps and dual screen / foldable Android phones that can enable / disable parts of the screen.
     
    JustAnotherDude likes this.
  7. frank-ijsfontein

    frank-ijsfontein

    Joined:
    Sep 11, 2015
    Posts:
    13
    We had a nasty issue on iOS 13, where the canvas would not scale right on orientation changes. Finally fixed it by always calling
    SafeAreaChanged()
    in Update, and changing ApplySafeArea and SafeAreaChanged like below:

    Code (CSharp):
    1.  
    2.     private Rect lastPixelRect;
    3.  
    4.     void ApplySafeArea()
    5.     {
    6.         if (safeAreaTransform == null)
    7.             return;
    8.  
    9.         var safeArea = Screen.safeArea;
    10.  
    11.         Rect pixelRect = canvas.pixelRect;
    12.         if (pixelRect == lastPixelRect)
    13.             return;
    14.  
    15.         lastPixelRect = pixelRect;
    16.         Debug.Log($"{this}: Applying safe area with canvas size {pixelRect.size}", this);
    17.  
    18.         var anchorMin = safeArea.position;
    19.         var anchorMax = safeArea.position + safeArea.size;
    20.         anchorMin.x /= pixelRect.width;
    21.         anchorMin.y /= pixelRect.height;
    22.         anchorMax.x /= pixelRect.width;
    23.         anchorMax.y /= pixelRect.height;
    24.  
    25.         safeAreaTransform.anchorMin = anchorMin;
    26.         safeAreaTransform.anchorMax = anchorMax;
    27.         //...
    28.     }
    29.  
    30.     private static void SafeAreaChanged()
    31.     {
    32.          for (int i = 0; i < helpers.Count; i++)
    33.          {
    34.              helpers[i].ApplySafeArea();
    35.          }
    36.      }
    37.  
    It seems that the pixelrect of the canvas only changes a frame later than the safe area, and so it would update itself with wrong values. Checking the last pixelRect against the current one fixes it.
     
    Last edited: Apr 30, 2020
  8. _Adriaan

    _Adriaan

    Joined:
    Nov 12, 2009
    Posts:
    469
    This also sounds like a Unity issue! Please report this to them so we'll all see this fixed!
     
  9. User340

    User340

    Joined:
    Feb 28, 2007
    Posts:
    2,975
    I'm noticing that Screen.safeArea reports wrong values occasionally, when changing device orientation. Is anyone familiar with this iOS issue?
     
  10. _Adriaan

    _Adriaan

    Joined:
    Nov 12, 2009
    Posts:
    469
    Literally said this a few posts ago. If you want Unity to fix this, make a bug report!
     
  11. _Adriaan

    _Adriaan

    Joined:
    Nov 12, 2009
    Posts:
    469
    I updated the script again today, unifying
    onResolutionChanged
    and
    onOrientationChanged
    to
    OnResolutionOrOrientationChanged
    , as it conceptually makes more sense and fixed a small issue on some android devices that would report an orientation change but not a resolution change. I also removed some of the extra static functions such as
    GetCanvasSize()
    and
    GetSafeAreaSize()
    . If you still need those and a few more, here they are:

    Code (CSharp):
    1.     public static Vector2 GetCanvasSize()
    2.     {
    3.         return helpers[0].rectTransform.sizeDelta;
    4.     }
    5.  
    6.     public static Vector2 GetSafeAreaSize()
    7.     {
    8.         for (int i = 0; i < helpers.Count; i++)
    9.         {
    10.             if(helpers[i].safeAreaTransform != null)
    11.             {
    12.                 return helpers[i].safeAreaTransform.sizeDelta;
    13.             }
    14.         }
    15.      
    16.         return GetCanvasSize();
    17.     }
    18.  
    19.     public static Rect SafeAreaRect()
    20.     {
    21.         for (int i = 0; i < helpers.Count; i++)
    22.         {
    23.             if(helpers[i].safeAreaTransform != null)
    24.             {
    25.                 return helpers[i].safeAreaTransform.rect;
    26.             }
    27.         }
    28.      
    29.         return helpers[0].rectTransform.rect;
    30.     }
    31.  
    32.     public static float GetSafeAreaGap(Vector2 side)
    33.     {
    34.         Rect safeArea = Screen.safeArea;
    35.         Rect canvasRect = new Rect(0f, 0f, Screen.width, Screen.height);
    36.      
    37.         if(safeArea == canvasRect)
    38.             return 0f;
    39.          
    40.         float gap = 0f;
    41.          
    42.         if(side == Vector2.down)
    43.             gap = safeArea.y;
    44.         else if(side == Vector2.up)
    45.             gap = canvasRect.height - (safeArea.x + safeArea.height);
    46.         else if(side == Vector2.left)
    47.             gap = safeArea.x;
    48.         else if(side == Vector2.right)
    49.             gap = canvasRect.width - (safeArea.x + safeArea.width);
    50.          
    51.         return (gap / canvasRect.height) * helpers[0].rectTransform.sizeDelta.y;
    52.     }
     
  12. Arcalise

    Arcalise

    Joined:
    Sep 27, 2019
    Posts:
    18
    Probably super simple question but where does the code "ApplicationManager.isLandscape" come from?

    I'm using unity beta 2020 i cant find any info on ApplicationManager.
     
  13. _Adriaan

    _Adriaan

    Joined:
    Nov 12, 2009
    Posts:
    469
    Ahhh woops, that was leftover code from one of my own projects :) I removed those two lines from the script in the original post, and as should you!
     
  14. SlimeProphet

    SlimeProphet

    Joined:
    Sep 30, 2019
    Posts:
    50
    This does not seem to work in Device Simulator (preview, 2.2.2) for me. Should it?
     
  15. _Adriaan

    _Adriaan

    Joined:
    Nov 12, 2009
    Posts:
    469
    It works for me as expected, but only in play mode. It does seem that the Device Simulator only applies the safe area when in play mode?
     
    SlimeProphet likes this.
  16. SlimeProphet

    SlimeProphet

    Joined:
    Sep 30, 2019
    Posts:
    50
    Thanks for letting me know. I was trying to set the position of something at runtime, and it was going to the wrong place. Must be a problem in my code. I'm still learning UGUI. I'll take another look.
     
  17. HeshamAkmal

    HeshamAkmal

    Joined:
    Oct 1, 2016
    Posts:
    11
    This works like a charm! And previews correctly in the device simulator. Thank you a lot for this, I think you should sell it on the asset store haha

    To make it always working (not just while in play mode) simply add [ExecuteAlways] in the script, like here https://docs.unity3d.com/ScriptReference/ExecuteAlways.html
     
    Last edited: Jun 12, 2020
  18. _Adriaan

    _Adriaan

    Joined:
    Nov 12, 2009
    Posts:
    469
    I played around with this idea but it would require some more changes to the script that would make it more complicated... I don't know. One example: you don't want to call those resize functions in-editor.
     
  19. HeshamAkmal

    HeshamAkmal

    Joined:
    Oct 1, 2016
    Posts:
    11
    Honestly it's working perfectly here. No console exceptions or anything, is there something that i'm not seeing?

    Edit: Never mind, it sometimes gave lots of exceptions (only in the editor)



     
    Last edited: Jun 14, 2020
  20. _Adriaan

    _Adriaan

    Joined:
    Nov 12, 2009
    Posts:
    469
    Awake() is only called once after creating an object, and the domain gets reloaded after a script compilation, so you'd lose access to the CanvasHelper singleton and because of that Update() will no longer work and any other scripts with ExecuteAlways dependent on the CanvasHelper would go bust. We'd need to rewrite the script to accomodate OnEnable and OnDisable instead of Awake and OnDestroy, and all the static functions would need more checks. This is all trivial work but like I said: it complicates the script, and I prefer an understandable and minimal script over a large complicated one!
     
    Last edited: Jun 15, 2020
  21. HeshamAkmal

    HeshamAkmal

    Joined:
    Oct 1, 2016
    Posts:
    11
    Ah yes I got it now, thanks for explaining that was helpful. Using the script in playmode is just enough anyway.
     
  22. abhayr

    abhayr

    Joined:
    Jan 31, 2017
    Posts:
    1
    Thanks brother. this works like charm. I was looking for a solution for iPhoneX and you saved a lot of my time!

    However, we also have an implementation to change the orientation on some screens (one rest of the app we don't allow auto-rotation). so, if we go to landscape automatic on that screen but came to other without automatic change to portray rather by the code then the Unity doesn't change the safeArea correctly and canvas helper sill using landscape safe-area.
    To fix this, we have saved the safe-area on each orientation change for each orientation and instaed of using Screen.safeArea, we use save safeArea for the current orientation (if already saved). This is working fine for us.

    Let me know if you found any potential issue in it. Attached are some changed snippet:

    private void updateSafeAreaForOrientation()
    {
    if (!_safeAreas.ContainsKey(Screen.orientation))
    {
    _safeAreas.Add(Screen.orientation, Screen.safeArea);
    }
    }

    void Update()
    {
    if (disableSafeScreenDrawing)
    return;

    if (helpers[0] != this)
    return;

    if (Application.isMobilePlatform && Screen.orientation != lastOrientation)
    {
    updateSafeAreaForOrientation();
    OrientationChanged();
    didOrientationChangedOnce = true;
    if(_safeAreas[Screen.orientation] != lastSafeArea)
    {
    ForceUpdateSafeArea(_safeAreas[Screen.orientation]);
    }
    }

    if (!didOrientationChangedOnce && Screen.safeArea != lastSafeArea)
    {//updating safe area here till orientation changed happens.
    SafeAreaChanged();
    }
    if (Screen.width != lastResolution.x || Screen.height != lastResolution.y)
    ResolutionChanged();
    }

    private static void ForceUpdateSafeArea(Rect safeArea)
    {

    lastSafeArea = safeArea;

    for (int i = 0; i < helpers.Count; i++)
    {
    helpers.ApplySafeArea();
    }
    }
     
  23. Hoorza

    Hoorza

    Joined:
    May 8, 2016
    Posts:
    35
    You, Sir, are a gentleman and a scholar! Thank you very much!
     
  24. Hoorza

    Hoorza

    Joined:
    May 8, 2016
    Posts:
    35
    Make sure you actually name Child object for your Canvas "SafeArea" not "Safe Area" or you will spend 4 hours trying to make work with no luck...
     
    andrew_pearce_ and _Adriaan like this.
  25. andrew_pearce_

    andrew_pearce_

    Joined:
    Nov 5, 2018
    Posts:
    136
    Thanks for the tip _Adriaan ! I personally do not want to risk (less changes = less issues with git) and instead changed the viewport of the camera =) The iOS users are minority in my case + plus notched phones are minority from that minority. I believe they will be happy with black line on the top and the bottom. Someone asked for this solution too in the topic and pushed me to this solution:
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. [RequireComponent(typeof(Camera))]
    4. public class CameraNotchSupport : MonoBehaviour {
    5.  
    6. #if UNITY_IOS
    7.   private Rect lastSafeArea = Rect.zero;
    8.   private Rect safeArea = Rect.zero;
    9.   private Camera camera;
    10.  
    11.   private void Start() {
    12.     this.camera = this.GetComponent<Camera>();
    13.   }
    14.  
    15.   void Update() {
    16.     this.safeArea = Screen.safeArea;
    17. #if UNITY_EDITOR
    18.     // simulate iPhoneX inside editor https://assetstore.unity.com/packages/tools/gui/safe-area-helper-130488
    19.     this.safeArea = (Screen.height > Screen.width) ? new Rect(0f, 102f / 2436f * Screen.height, 1f * Screen.width, 2202f / 2436f * Screen.height) :  // Portrait
    20.       new Rect(132f / 2436f * Screen.width, 63f / 1125f * Screen.height, 2172f / 2436f * Screen.width, 1062f / 1125f * Screen.height); // Landscape
    21. #endif
    22.  
    23.     if (this.safeArea != this.lastSafeArea) {
    24.       this.lastSafeArea = this.safeArea;
    25.  
    26.       this.safeArea.x /= Screen.width;
    27.       this.safeArea.width /= Screen.width;
    28.       this.safeArea.y /= Screen.height;
    29.       this.safeArea.height /= Screen.height;
    30.       this.camera.rect = this.safeArea;
    31.     }
    32.   }
    33. #endif
    34. }
    I do not have notched iPhone so I will test it with emulator and update my post. If anyone can test it with real phone, that will help a lot. Thanks

    UPD: Works fine on emulator, I had to modify code a bit =)
     
    Last edited: Feb 9, 2021
  26. Nokola

    Nokola

    Joined:
    Jul 25, 2015
    Posts:
    5
    Thank you for this! Worked immediately by following the instructions in the first post and using Unity 2020.3.1f1 LTS.
     
  27. SiWoC

    SiWoC

    Joined:
    Feb 16, 2021
    Posts:
    6
    Great, works like a charm indeed.

    Question, when my build is mobile only, portrait only, I can remove all the xxxChanged stuff right?
    So I end up with only 1 call to ApplySafeArea in Awake.
     
  28. _Adriaan

    _Adriaan

    Joined:
    Nov 12, 2009
    Posts:
    469
    No. The Safe Area may be reported by the OS after the first Awake after opening your app. Like I said in an earlier comment to someone here: removing the update from this script is such a small optimisation that you shouldn't bother. There are edge cases where the safe area changes. Just leave it there.
     
unityunity