Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cordova iPhone X safe area after layout/orientation changes.

I'm using the CSS variable safe-area-inset-top in my Cordova app to handle iPhone X safe area:

body {
 padding-top: env(safe-area-inset-top);
}

Works as expected when the app boots up. However, when I enter and exit a full screen (forced landscape) AVPlayer via a custom plugin, and return to the portrait app, the padding is gone and my app is partially cut off.

I'm currently using this in my AVPlayerViewController class within the plugin on iOS.

LandscapeVideo.m

- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
    return UIInterfaceOrientationLandscapeRight; // or LandscapeLeft
}

- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
    return UIInterfaceOrientationMaskLandscape;
}

Thanks in advance for any help / ideas!

like image 724
ndmweb Avatar asked Nov 13 '18 22:11

ndmweb


3 Answers

Fix for iPhone X/XS screen rotation issue

On iPhone X/XS, a screen rotation will cause the header bar height to use an incorrect value, because the calculation of safe-area-inset-* was not reflecting the new values in time for UI refresh. This bug exists in UIWebView even in the latest iOS 12. A workaround is inserting a 1px top margin and then quickly reversing it, which will trigger safe-area-inset-* to be re-calculated immediately. A somewhat ugly fix but it works if you have to stay with UIWebView for one reason or another.

window.addEventListener("orientationchange", function() {
    var originalMarginTop = document.body.style.marginTop;
    document.body.style.marginTop = "1px";
    setTimeout(function () {
        document.body.style.marginTop = originalMarginTop;
    }, 100);
}, false);

The purpose of the code is to cause the document.body.style.marginTop to change slightly and then reverse it. It doesn't necessarily have to be "1px". You can pick a value that doesn't cause your UI to flicker but achieves its purpose.

like image 138
YYL Avatar answered Nov 15 '22 07:11

YYL


As seen in the notes about device orientation of this answer: https://stackoverflow.com/a/46232813/1085272

There is a safe-area-inset-xxx issue after some orientation changes when using uiWebView (which is default).

Switching to WKWebView (e.g. by using cordova-plugin-wkwebview-engine) fixed the issue for me.

like image 32
pascalzon Avatar answered Nov 15 '22 07:11

pascalzon


Thanks to both @pascalzon and @YYL for useful information. I tried switching to WKWebView for my Cordova app but it just broke it, so for the time being I am stuck with uiWebview and thus this issue.

I want my app to look nice on notched devices like the iPhone X so I added viewport-fit=cover to the app's viewport declaration and started playing with safe-area-insets. My layout needs are quite simple. The top margin must either be 1rem or safe-area-inset-top if this has a value greater than 1rem.

Unfortunately, the CSS max() function is not yet standard. If I could have used that things would have been simpler (assuming it converts everything to pixels).

:root {
    --app-margin-top-default: 1rem;
    --app-margin-top: max(env(safe-area-inset-top), var(--app-margin-top-default));
}

So I was forced to perform the max comparison in javascript and then set --app-margin-top from there each time the screen orientation changed.

Since there seems to be no way, as yet, of reading CSS env vars from javascript, I start in the CSS by recording safe area insets in CSS variables which I can access later from javascript. Default values are:

:root {
    --safe-area-inset-top:      0px;
    --safe-area-inset-right:    0px;
    --safe-area-inset-bottom:   0px;
    --safe-area-inset-left:     0px;
}

I then set these vars as follows:

/* iOS 11.0: supports constant() css function. (Assume all other inset vars are supported.) */
@supports (padding-top: constant(safe-area-inset-top))  {
    :root {
        --safe-area-inset-top:      constant(safe-area-inset-top, 0);
        --safe-area-inset-right:    constant(safe-area-inset-right, 0);
        --safe-area-inset-bottom:   constant(safe-area-inset-bottom, 0);
        --safe-area-inset-left:     constant(safe-area-inset-left, 0);
    }
}
/* iOS 11.2 and latest Chrome webviews support the env() css function. (Assume all other inset vars are supported.) */
@supports (padding-top: env(safe-area-inset-top))  {
    :root {
        --safe-area-inset-top:      env(safe-area-inset-top, 0);
        --safe-area-inset-right:    env(safe-area-inset-right, 0);
        --safe-area-inset-bottom:   env(safe-area-inset-bottom, 0);
        --safe-area-inset-left:     env(safe-area-inset-left, 0);
    }
}

Each time the screen orientation changed I would then compare --safe-area-inset-top with --app-margin-top-default and set --app-margin-top to the maximum of these values. It was then that I ran into this problem. There seemed to be a lag between the screen orientation changing and my CSS safe area inset variables being updated. So my margin setting code did not work.

I tried YYL's trick to force recalculation of the safe area insets but it didn't seem to work. The insets were eventually updated but after my screen orientation change event listener had finished executing :(

My Solution

This solution relies on the cordova screen orientation plugin:

https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-screen-orientation/

The only time you can rely on the safe area inset values being right is at application start up, so I record them in a set of javascript variables inside my app object:

safeAreaInsetNorth: undefined,
safeAreaInsetEast: undefined,
safeAreaInsetSouth: undefined,
safeAreaInsetWest: undefined,

The method that sets these variables at startup takes screen orientation into account.

console.log("Screen orientation at startup is: " +screen.orientation.type);
let $root = $(":root");
// Notch/North at the top.
if (screen.orientation.type.match("portrait-primary")) {
    app.safeAreaInsetNorth = $root.css("--safe-area-inset-top");
    app.safeAreaInsetEast  = $root.css("--safe-area-inset-right");
    app.safeAreaInsetSouth = $root.css("--safe-area-inset-bottom");
    app.safeAreaInsetWest  = $root.css("--safe-area-inset-left");
}
// Upside down... Notch/North at the bottom.
else if (screen.orientation.type.match("portrait-secondary")) {
    app.safeAreaInsetNorth = $root.css("--safe-area-inset-bottom");
    app.safeAreaInsetEast  = $root.css("--safe-area-inset-left");
    app.safeAreaInsetSouth = $root.css("--safe-area-inset-top");
    app.safeAreaInsetWest  = $root.css("--safe-area-inset-right");
}
// Notch/North to the left.
else if (screen.orientation.type.match("landscape-primary")) {
    app.safeAreaInsetNorth = $root.css("--safe-area-inset-left");
    app.safeAreaInsetEast  = $root.css("--safe-area-inset-top");
    app.safeAreaInsetSouth = $root.css("--safe-area-inset-right");
    app.safeAreaInsetWest  = $root.css("--safe-area-inset-bottom");
}
// Notch/North to the right.
else if (screen.orientation.type.match("landscape-secondary")) {
    app.safeAreaInsetNorth = $root.css("--safe-area-inset-right");
    app.safeAreaInsetEast  = $root.css("--safe-area-inset-bottom");
    app.safeAreaInsetSouth = $root.css("--safe-area-inset-left");
    app.safeAreaInsetWest  = $root.css("--safe-area-inset-top");
} else {
    throw "I have no idea which way up I am!";
}

At startup and then again each time the screen orientation changes I check to see if I need to update --app-margin-top like this:

let marginTopDefault = app.getGlobalCssVariableInPx("--app-margin-top-default");
let newMarginTop = undefined;
switch(screen.orientation.type) {
    case "portrait-primary": // Notch/North at the top.
        newMarginTop = app.safeAreaInsetNorth;
        break;
    case "portrait-secondary": // Upside down... Notch/North at the bottom.
        newMarginTop = app.safeAreaInsetSouth;
        break;
    case "landscape-primary": // Notch/North to the left.
        newMarginTop = app.safeAreaInsetEast;
        break;
    case "landscape-secondary": // Notch/North to the right.
        newMarginTop = app.safeAreaInsetWest;
        break;
    default:
        throw "No idea which way up I am!";
}

app.getGlobalCssVariableInPx is a helper function that converts rems into pixels by multiplying them by the base font size.

I then convert marginTopDefault and newMarginTop to integers, find the max and set --app-margin-top to this.

    $(":root").css("--app-margin-top", Math.max(marginTopDefault, newMarginTop) + "px");

Conclusion

I think I have described this well enough. It was a lot of messing about to get something working that should have just worked out of the box, but hey ho. That's how it goes sometimes. And I am no HTML5 guru, so there are probably ways in which this could made simpler.

like image 25
daffinm Avatar answered Nov 15 '22 08:11

daffinm