Welcome to Code Couch

Detecting orientation changes in mobile Safari on iOS, and other supported browsers

Posted by at 10:58pm on November 20, 2011.

I’ve been writing a tool for the iPad recently, and found a need to detect when its orientation had changed (whether from landscape to portrait or vice versa).

If I were writing this post years ago, I’d probably advise that the best way of detecting this change would be to have a timer run a function every half a second or so, with that function detecting the width of the window. I’m happy to report that times have changed since 2007, when this blog post was written.

These days, things are a lot easier and less clunky: you can listen for the orientationchange event which is fired on iOS devices whenever they change orientation. Having said that, it appears that Safari running on any iPhone doesn’t fire the 180 degree event and does not rotate the screen when in that orientation (I tested on my 3g, 4, and 4S models). Safari on my iPad fires it for all four positions (0, 90, 180, 270 degrees).

Enough waffle, here is Apple’s sample code taken from the Handling Orientation Events section of their Safari Web Content Guide:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>Orientation test</title>
    <script type="text/javascript">
        function updateOrientation() {
            var displayStr = 'Orientation: ';
 
            switch(window.orientation) {
                case 0:
                    displayStr += 'Portrait';
                    break;
 
                case -90:
                    displayStr += 'Landscape (right, screen turned clockwise)';
                    break;
 
                case 90:
                    displayStr += 'Landscape (left, screen turned counterclockwise)';
                    break;
 
                case 180:
                    displayStr += 'Portrait (upside-down portrait)';
                    break;
            }
 
            document.getElementById('output').innerHTML = displayStr;
        }
    </script>
</head>
 
<body onorientationchange="updateOrientation();">
    <div id="output"></div>
</body>
</html>

There are two things I would change about this code (other than the XHTML 1.0 Strict DOCTYPE, which I’ve already changed for the much more memorable HTML5 DOCTYPE). The first is the attachment of the event handler as an attribute on the <body> element – I’d attach an event listener (it’s much cleaner IMHO). The second issue both irks and puzzles me: the orientation has to be read from the window object instead of being passed in with the event data. Why is this? I can’t think of another case where data associated with an event not made available through the event data in some form or other. So why not with this event type?

Reducing the amount of code needed

I’m not a fan of switch statements. Don’t get me wrong, I’ll use them when they are appropriate, but I don’t believe this is such an occasion. All I wanted to do was determine whether my iPad was in landscape or portrait orientation; I really didn’t care whether the home button was to the left or to the right. After puzzling it over for a few minutes, I realised I could apply the same bitwise AND trick used to determine odd / even numbers (n & 1), using the value 2 instead of 1.

For those of you unfamiliar with bitwise operators, the Bitwise Operators page at MDN is a good all-round read covering a bit of everything, and JavaScript Binary Operations – the easy way is definitely worth a read as well. Anyway, here is my take on how it could be done, followed by an explanation of how it works:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>Orientation test</title>
    <script type="text/javascript">
        window.addEventListener('orientationchange', function() {
            document.getElementById('output').innerHTML = 'Orientation: ' + ['Portrait',,'Landscape'][window.orientation & 2];
        }, false);
    </script>
</head>
 
<body>
    <div id="output"></div>
</body>
</html>

I’m not going to go into detail about addEventListener in this article, as event handling is already very well documented. All I need to say on the subject is that if you prefer to use the method shown in Apple’s sample code to attach the event handler from my code, everything should still work correctly.

So how does it work?

The window object has a read-only property named orientation that holds one of four numbers: 0, -90, 90, or 180. As you might have deduced, these values are the angle of rotation in degrees, from the default upright portrait position.

To better understand how my code works, let’s take a look at the binary representations of the 5 numbers (I’m limiting the number of bits to 16 for brevity. In reality, they are operated on as 32-bit numbers):

Decimal    Binary
 
   0       0000000000000000
  90       0000000001011010
 180       0000000010110100
 -90       1111111110100110
   2       0000000000000010

As with the topic of event handling, the topic of bitwise logic and bitwise operations deserves much more in-depth coverage than I want to go into, so I’ll keep this brief: the result of a bitwise AND operation between any two bits will be 1 if and only if both operands are 1. In all other cases (0/0, 0/1, 1/0), the result will be 0. Here are the results for all for orientation values after the logical AND:

                   Portrait           Landscape            Portrait           Landscape
 
(Decimal)                 0                  90                 180                 -90
Binary     0000000000000000    0000000001011010    0000000010110100    1111111110100110
& 2        0000000000000010    0000000000000010    0000000000000010    0000000000000010
Result     0000000000000000    0000000000000010    0000000000000000    0000000000000010
(Decimal)                 0                   2                   0                   2

Hopefully my very brief explanation makes sense. In a nutshell, the results of the bitwise AND operations are as follows:

  0 & 2 == 0
180 & 2 == 0
 90 & 2 == 2
-90 & 2 == 2

What to do with the result of the bitwise operation

If you need to know only whether the device is in landscape mode or not, or portrait mode or not, then you can simply return the result of the bitwise operation and use that in any tests. Remember, 0 equates to false and 2 equates to true, as does 1, or 42, or 109.6, etc. If you really need it as a boolean, the simplest way is to perform two logical NOT operations by using !!, e.g:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>Orientation test</title>
    <script type="text/javascript">
        window.addEventListener('orientationchange', function() {
 
            // You can test like this (on the values 0 and 2):
 
            if (window.orientation & 2) {
                // Do something if in landscape mode
            } else {
                // Do something if in portrait mode
            }
 
 
            // Or, if you really need to have boolean values, then logical NOT the result of the bitwise AND twice
            // The first NOT converts it to a boolean of the opposite state, the second restores its state
 
            if (!!(window.orientation & 2)) {
                // Do something if in landscape mode
            } else {
                // Do something if in portrait mode
            }
 
        }, false);
    </script>
</head>
 
<body>
</body>
</html>

What about the weird array syntax? What do the double commas mean?

There’s nothing special about the double-comma syntax used in the array; it simply means that I’ve chosen not to define one element of the array (or to put it another way, I’ve chosen to skip over that position). Therefore, the element at that position in the array will be undefined. The array will still have a length of 3, and it still holds 3 values – it just so happens that one of these is the value undefined. Element 0 will be the string ‘Portrait’, element 1 will be undefined, and element 2 will be the string ‘Landscape’:

var arr = ['Portrait',,'Landscape'];
alert(arr[0]);         // 'Portrait'
alert(arr[1]);         // undefined
alert(arr[2]);         // 'Landscape'
alert(arr.length);     // 3

So, the bitwise AND returns either 0 or 2, and I’ve got an array with data at elements 0 and 2. If it hasn’t done so yet, the penny should now be well and truly dropping: the result of the bitwise AND is being used to select one of the two array elements containing data – data that gives us the orientation.

Here’s an example of how this might be used in a real situation: The device orientation is added as an attribute to the <body> element, and this is used by the CSS to ensure that content (in this case, an image) is always displayed at the same position on the screen, no matter what the orientation, or where the status bar is. This avoids the problem of having to resize the content – which can become tricky, depending on the content. Note: this example was designed for the resolution and aspect ratio of the iPad screen. While the image does rotate on an iPhone, it doesn’t fit nicely. As the code I’m writing is not designed to be run on an iPhone (the small screen makes it impractical), I haven’t spent any time getting these examples to look perfect on an iPhone. However, it should be good enough to get the idea across :-).

There are two things I’d like to point out about this example: In addition to the orientationchange event, the attribute on the <body> is being updated once the page has loaded so the device doesn’t have to be rotated to ensure everything is sized correctly. The second point is the issue of the empty CSS rule body[orient="landscape"] {}. On my iPad running iOS 4.3.3, if I do not include this line, the code fails to size the image as expected – even though the rule contains no styling, and thus should make no difference. Unfortunately, I don’t have an iPad 2 to test it on. If anyone would like to donate one to me, I’d be only too happy to accept it :-)

If anyone can tell me why this might be happening, or how to fix it without having to frig my code in a manner I thought I’d seen the last of with IE6 (correction: that was until I found that Safari/iOS will not fire touchstart events on anchors containing semi-opaque PNGs… Apple must have got the IE6 devs going cheap. Read more on the PNG bug).

<!DOCTYPE html>
<html lang="en">
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=0" />
    <meta charset="utf-8" />
    <title>Orientation test</title>
    <script type="text/javascript">
        function updateOrientation() {
            document.body.setAttribute('orient', ['portrait',,'landscape'][window.orientation & 2]);
            window.scrollTo(0, 0);
        };
        window.addEventListener('orientationchange', updateOrientation, false);
        window.addEventListener('load', updateOrientation, false);
    </script>
 
    <style type="text/css">
        html, body {
            margin: 0px;
            padding: 0px;
        }
        body {
            text-align: center;
        }
        body[orient="landscape"] {}
        body[orient="landscape"] img {
            width: 890px;
            height: 690px;
        }
        body[orient="portrait"] img {
            -webkit-transform: rotate3d(0, 0, 1, -90deg) translate3d(-90px, -22px, 0px);
            width: 890px;
            height: 690px;
        }
    </style>
</head>
 
<body>
    <img src="http://oi39.tinypic.com/2l97b41.jpg" />
</body>
</html>

A quick note on decimal / binary conversion

You can easily convert from decimal to binary in JavaScript by passing 2 as the radix (base) into the toString() method available on every Number instance, and from binary back to decimal by passing 2 as the radix to parseInt. There are some caveats, but for positive integer numbers, you should find these two methods are reliable:

alert(6..toString(2));            // '110'
alert(42..toString(2));           // '101010'
alert(parseInt('110', 2));        // 6
alert(parseInt('101010', 2));     // 42

I mentioned a few caveats. These are that you will not be able to use the same methods to convert to and from floating point numbers, and negative numbers will need another step or two. As you may have already discovered, the toString representation of -90 in binary is nothing like the number I used:

The -90 I used (16-bit)    1111111110100110
The -90 I used (8-bit)             10100110
-90..toString(2)                   -1011010

The representation I used is the two’s complement of the positive number (90, in this case). Here’s the quick-and-dirty method I mentioned to give me the correct negative binary representation, which does require specifying a number of bits. For -90/90, an 8-bit number is fine, but I’ll show the 16-bit version as well:

var bitCount = 8;                                                        // 8 bit version
var negativeNum = -90;                                                   // Negative number to use
var combosForBitCount = Math.pow(2, bitCount);                           // 2 ** 8 == 256
// Here's the conversion to binary and back to decimal
var negativeBinary = (combosForBitCount + negativeNum).toString(2);      // 10100110
var negativeDecimal = parseInt(negativeBinary, 2) - combosForBitCount;   // -90
 
var bitCount = 16;                                                       // 16 bit version
var negativeNum = -90;                                                   // Negative number to use
var combosForBitCount = Math.pow(2, bitCount);                           // 2 ** 16 == 65536
// Here's the conversion to binary and back to decimal
var negativeBinary = (combosForBitCount + negativeNum).toString(2);      // 1111111110100110
var negativeDecimal = parseInt(negativeBinary, 2) - combosForBitCount;   // -90

It’s probably worth mentioning that there are multiple ways of converting negative binary numbers, and the method I’ve given above shouldn’t be used without ensuring that it is providing valid results. I really only included it so you could see how I obtained the binary value for -90. If you haven’t lost the will to live just yet, I’d read through the Binary Numbers page at Celtic Kane Online and the Wikipedia article on Signed number representations.

The floating point issue is easy to understand: parseInt can’t be used to convert a floating point number (the clue is in its name), and parseFloat doesn’t take a radix. Converting to and from floating point binary numbers, while not overly tricky, isn’t something I will go into here. I can recommend Essential Computer Mathematics by Seymour Lipschutz (Schaum’s Outline Series) as a fantastic all-round read (I still refer to my 1982 copy today, and have even taught the occasional class with the book as my only aid). 40+ million students can’t be wrong!

Post to Twitter

Comments

There are 2 responses to this post.

  1. As for esoteric methods of getting the orientation as "landscape" or "portrait", here's another you could use... var orientation = ["landscape", "portrait"][+!(angle % 180)]
  2. Thanks for this, works beautifully! :)

Leave a reply

You must either log in or enter your name and email address to post a comment.

Your email address will not be published.

  • You do not need to log in to comment, but you can if you wish.
  • Log in