Orientationchange has issues

With the rise of responsive and adaptive websites, the javascript resize event has become more and more important. With all the plugins we create now, we have to consider how they should behave when the user resizes their screen, or changes the orientation on their phone or tablet.

You may (or may not) have noticed that the resize event sometimes fires a little more often than you expect it to in some mobile browsers, namely Chrome and Firefox. As you scroll down the page, mobile browsers tend to hide their nav bar to allow more space for the website’s content. In Safari this isn’t a problem as the nav bar seems to render outside of the window area, in Chrome and Firefox however (and potentially quite a few other mobile browsers), the nav bar is part of the window, so affects the height of the page. Thus, when you scroll the page and the nav bar appears or disappears in Chrome and Firefox, once you stop scrolling, the browser fires a resize event to recalculate the height of the window and all the elements. So all of your js resize events will be firing again too.

You will have especially noticed this if you ever use fixed 100% height elements – once Chrome’s nav bar disappears, your 100% height element suddenly ends up with an empty gap under it until you stop scrolling and the browser recalculates all the heights again!

If you’re trying to cater for a fluid layout in a plugin such as a slider, the page’s height probably isn’t of much importance to you, but its width is. So you probably don’t need to do any width checks on that particular resize event. And so enters orientationchange detection.

My approach for resize/orientationchange always used to be this:

var isMobile = /android|webos|iphone|ipad|ipod|blackberry|windows phone/i.test(navigator.userAgent.toLowerCase()),
    orientationSupport = isMobile ? window.hasOwnProperty('orientation') : false,
    resizeEvent = orientationSupport ? 'orientationchange' : 'resize';

$(window).on(resizeEvent, function(){ ... });

This way I could use a standard resize event for desktop users, and orientationchange for mobile devices that supported it. We can’t assume that all mobile devices support the orientationchange event, as Android 2.2 Froyo certainly didn’t support it, and older versions of iOS didn’t either. The isMobile check in the code above is used inside the orientationSupport variable purely because if you use the .hasOwnProperty() method on the window object in IE7, it gives you a javascript error.

This approach is flawed however. While debugging this approach, on certain Android devices, I noticed that the orientationchange event often fires before the window’s width has been reset. In other words, the event fires as soon as you start to change the device’s orientation. So if you are doing any window width checks inside the orientationchange event, you may find that they return what the window width was before the orientation was changed, rather than after.

This issue happened occasionally on my Nexus 5, but consistently when I tested it on a Galaxy S3 and my Xperia Tablet Z. This is an issue that I had never encountered with the resize event.

So I needed a custom orientationchange check, done inside the resize event, that would cater for both mobile and desktop users. This is what I came up with:

var orientationchanged = true,
    $window = $(window),
    newOrientation,
    oldOrientation = $window.width() > $window.height() ? 'landscape' : 'portrait',
    isMobile = /android|webos|iphone|ipad|ipod|blackberry|windows phone/i.test(navigator.userAgent.toLowerCase());

$window.resize(function(){
    if (isMobile){
        newOrientation = $window.width() > $window.height() ? 'landscape' : 'portrait';
        orientationchanged = newOrientation !== oldOrientation;
        oldOrientation = newOrientation;
    }
    if (orientationchanged){
        // call all your functions in here
    }
});

With this approach, I set the orientationchanged variable to true by default, and only reset it if the user is on a mobile device. That way, the resize event will work as normal on desktop devices with the orientationchanged conditional always returning true. On mobile however, we check the orientation of the device (in the newOrientation variable) against the oldOrientation which we checked for on page load – if the two don’t match, then we allow our resize functions to fire. If they do match, the orientationchanged variable will be false, and our resize functions won’t fire. We of course have to reset the oldOrientation variable each time we do the check on mobile devices, because we need to check against the orientation from the previous resize event, rather than against the orientation of the page on load.

We have to remember of course that the standard resize event on desktop fires on every pixel movement. If we have quite a lot going on inside our resize function it can get quite intensive, so I altered the above code to do the resize checks specifically when the user stops resizing the page:

var timer,
    orientationchanged = true,
    $window = $(window),
    newOrientation,
    oldOrientation = $window.width() > $window.height() ? 'landscape' : 'portrait',
    isMobile = /android|webos|iphone|ipad|ipod|blackberry|windows phone/i.test(navigator.userAgent.toLowerCase());

$window.resize(function(){
    if (timer){
        clearTimeout(timer);
    }
    timer = setTimeout(function(){
        if (isMobile){
            newOrientation = $window.width() > $window.height() ? 'landscape' : 'portrait';
            orientationchanged = newOrientation !== oldOrientation;
            oldOrientation = newOrientation;
        }
        if (orientationchanged){
            $window.trigger('customResize');
        }
    }, 100);
});

The timer variable is used here to store a setTimeout() function with a small delay – for every pixel movement, if the timer has a set timeout defined which hasn’t fired yet (due to the 100ms delay) it will clear it, and then redefine it. That way all of our custom code will only fire if the user stops resizing the page for at least 100ms. The $window.trigger('customResize'); code is something I use so that I only have to define this block of code once. Whenever I want to use a window resize event, I simply define a custom event listener on the window: $window.on('customResize', function(){ ... });, and I know that this custom event listener will only fire on resize end, and only when the orientation has changed if the user is on a mobile device.

It’s worth bearing in mind that Android also fires a resize event when the keyboard appears on screen, but it doesn’t count the keyboard within the window dimensions. So this customResize event listener will also fire when the keyboard appears (and disappears). So it doesn’t fully replace the orientationchange event, but it’s a start!

I hope this has been helpful.


Posted: July 24th, 2014
Categories: code, mobile