Archive for August, 2010

Dealing with device orientation (Mobile web part 6)

Tuesday, August 24th, 2010

Introduction

If you read part 4 of this series, you’ll know that because of the variety of screen sizes, the best way to develop for mobile is to develop fluid layouts that take 100% of the available space on the screen.

What you probably didn’t think of is that there’s different screen widths even on the same device! This is due to screen orientation. And when the user changes the screen orientation, stuff may break (hey, it’s not a perfect world). In my experience this has meant needing to tweak percentage widths on elements, but I imagine there’s even more needs. Imagine an image carousel that can only fit three images across in portrait mode but can possibly fit four images in landscape mode. It might be nice to re-initialize the carousel to accommodate a forth image when the phone is in landscape mode (then again, that might create a lot of unnecessary overhead…).

In any case, I hope you can agree that it would sometimes be useful to know the screen orientation.

window.orientation and the orientationchange event

Luckily on the latest smartphones you have some goodies available to you that you don’t have on the desktop (since desktop users aren’t in the habit of constantly turning their screens sideways!).

window.orientation: this property gives the current screen orientation: 0 in portrait mode, 90 when rotated left, -90 when rotated right (no special value when the screen is upside-down)

orientationchange event: this window event fires after every 90 degrees of rotation and, like other events, can be applied in various ways:

// DOM Level 0 (avoid)
window.onorientationchange = function(){};

// DOM Level 2
window.addEventListener('orientationchange', function(){}, false);

Some websites recommend using orientationchange to dynamically add an orient attribute on the body element and target the orientation with CSS selectors (body[orient=landscape]), but this is in error. As it turns out, orientationchange is only fired AFTER the screen has been rotated (which also triggers a CSS reflow), which means this attribute is updated later (after the reflow). And unfortunately editing this orient attribute doesn’t trigger another CSS reflow. The result? When you rotate the device, these new CSS styles don’t get applied!

The fix is to add the orientation as a CSS class, which does trigger a CSS reflow. So our code at this point will look something like this:

(function(){
var init = function() {
  var updateOrientation = function() {
    var orientation = window.orientation;
    
    switch(orientation) {
      case 90: case -90:
        orientation = 'landscape';
      break;
      default:
        orientation = 'portrait';
    }
    
    // set the class on the HTML element (i.e. )
    document.body.parentNode.setAttribute('class', orientation);
  };
  
  // event triggered every 90 degrees of rotation
  window.addEventListener('orientationchange', updateOrientation, false);
  
  // initialize the orientation
  updateOrientation();
}

window.addEventListener('DOMContentLoaded', init, false);

})();

Now we can target elements like this in CSS:

.portrait body div { width: 10%; }
.landscape body div { width: 15%; }

With a little help from media queries

You may have heard of media queries being used to target mobile devices (based on screen pixel width) or to target the iPhone 4′s Retina display, but you may not have known that you can also target screen orientation!

@media all and (orientation: portrait) {
  body div { width: 10%; }
}

@media all and (orientation: landscape) {
  body div { width: 15%; }
}

The orientation media query is available on iOS 3.2+, Android 2.0+, and some other browsers.

This is a lot cleaner than the above JavaScript example in the sense that it’s pure CSS, and it’s part of the CSS that gets reflowed when the screen is rotated.

(Minor note: iOS 4 on the iPhone Simulator running 4.0.0 looks like it’s stuck in landscape orientation, but the media queries work correctly on my 3GS with 4.0.1)

Fallback: when window.orientation and media queries aren’t available…

If window.orientation isn’t available on a device, chances are the orientationchange event and media queries (for orientation) will also not be available. Oh no, what do we do now?

Even though this isn’t an entirely foolproof method, we can dynamically measure the window width and height and guess orientation based on that:

(function(){
var HTMLNode = document.body.parentNode;
var updateOrientation = function() {  
  // landscape when width is biggest, otherwise portrait
  var orientation = (window.innerWidth > window.innerHeight) ? 'landscape': 'portrait';
  
  // set the class on the HTML element (i.e. )
  HTMLNode.setAttribute('class', orientation);
}
var init = function() {
  // initialize the orientation
  updateOrientation();
  
  // update every 5 seconds
  setInterval(updateOrientation, 5000);
}
window.addEventListener('DOMContentLoaded', init, false);
})();

Ok, so it’s not pretty, but it seems to work. The overhead in this fallback example is the fact that we have to use a polling technique (in this case every 5 seconds [5000 milliseconds]) to check for changes in orientation.

Note: there’s also the strong possibility that these browsers will not support the DOMContentLoaded event, but we’ll ignore that for the purposes of this article. (if you have problems, change DOMContentLoaded to load)

Putting it all together

Ok, so if you want the fallback example to work in addition to newer methods, unless you want to duplicate your CSS, then avoid using media queries to target orientation. Instead we’ll rely on adding a class to the html tag (or the body tag if you prefer).

Once we put everything together, we get something that looks like this:

(function(){
var supportsOrientation = (typeof window.orientation == 'number' && typeof window.onorientationchange == 'object');
var HTMLNode = document.body.parentNode;
var updateOrientation = function() {
  // rewrite the function depending on what's supported
  if(supportsOrientation) {
    updateOrientation = function() {
      var orientation = window.orientation;
    
      switch(orientation) {
        case 90: case -90:
          orientation = 'landscape';
        break;
        default:
          orientation = 'portrait';
      }
      
      // set the class on the HTML element (i.e. )
      HTMLNode.setAttribute('class', orientation);
    }
  } else {
    updateOrientation = function() {
      // landscape when width is biggest, otherwise portrait
      var orientation = (window.innerWidth > window.innerHeight) ? 'landscape': 'portrait';

      // set the class on the HTML element (i.e. )
      HTMLNode.setAttribute('class', orientation);
    }
  }
  
  updateOrientation();
}
var init = function() {
  // initialize the orientation
  updateOrientation();
  
  if(supportsOrientation) {
    window.addEventListener('orientationchange', updateOrientation, false);
  } else {
    // fallback: update every 5 seconds
    setInterval(updateOrientation, 5000);
  }

}
window.addEventListener('DOMContentLoaded', init, false);
})();

Minified (540 bytes):

(function(){var e=typeof window.orientation=="number"&&typeof window.onorientationchange=="object",f=document.body.parentNode;function c(){c=e?function(){var d=window.orientation;switch(d){case 90:case -90:d="landscape";break;default:d="portrait"}f.setAttribute("class",d)}:function(){f.setAttribute("class",window.innerWidth>window.innerHeight?"landscape":"portrait")};c()}window.addEventListener("DOMContentLoaded",function(){c();e?window.addEventListener("orientationchange",c,false):setInterval(c,5E3)},false)})();

Conclusion

And that’s it! Now we can reliably target different screen orientations with some straightforward CSS:

.portrait body div { width: 10%; }
.landscape body div { width: 15%; }

Again, in my experience I’ve used this to fix bugs. But I’m sure you can find more creative uses for it!

Related

iPad web development tips (Nicholas C. Zakas)
iPhone window.onorientationchange Code (Ajaxian)
The orientation media query (Quirksmode)

More from the Mobile Web series:

Part 1: The viewport metatag
Part 2: The mobile developer’s toolkit
Part 3: Designing buttons that don’t suck
Part 4: On designing a mobile webpage
Part 5: Using mobile-specific HTML, CSS, and JavaScript
Part 6: Dealing with device orientation
Part 7: Mobile JavaScript libraries and frameworks

BlackBerry Torch SunSpider results (JavaScript benchmark)

Wednesday, August 18th, 2010

Results

============================================
RESULTS (means and 95% confidence intervals)
--------------------------------------------
Total:                 322.2ms +/- 4.9%
--------------------------------------------

  3d:                   55.0ms +/- 15.3%
    cube:               19.8ms +/- 12.1%
    morph:              16.6ms +/- 35.8%
    raytrace:           18.6ms +/- 22.5%

  access:               32.8ms +/- 12.4%
    binary-trees:        1.8ms +/- 30.9%
    fannkuch:           14.2ms +/- 7.3%
    nbody:              12.6ms +/- 26.7%
    nsieve:              4.2ms +/- 24.8%

  bitops:               29.4ms +/- 10.2%
    3bit-bits-in-byte:   2.4ms +/- 28.4%
    bits-in-byte:        8.0ms +/- 15.5%
    bitwise-and:         8.6ms +/- 21.9%
    nsieve-bits:        10.4ms +/- 21.7%

  controlflow:           2.4ms +/- 28.4%
    recursive:           2.4ms +/- 28.4%

  crypto:               22.0ms +/- 10.6%
    aes:                 9.8ms +/- 27.5%
    md5:                 6.2ms +/- 22.0%
    sha1:                6.0ms +/- 20.7%

  date:                 33.2ms +/- 11.7%
    format-tofte:       16.6ms +/- 20.2%
    format-xparb:       16.6ms +/- 13.6%

  math:                 32.6ms +/- 15.2%
    cordic:             12.0ms +/- 29.3%
    partial-sums:       15.2ms +/- 20.4%
    spectral-norm:       5.4ms +/- 20.6%

  regexp:               15.6ms +/- 7.1%
    dna:                15.6ms +/- 7.1%

  string:               99.2ms +/- 14.0%
    base64:              9.2ms +/- 14.8%
    fasta:              14.4ms +/- 27.7%
    tagcloud:           27.2ms +/- 23.2%
    unpack-code:        31.8ms +/- 14.5%
    validate-input:     16.6ms +/- 18.0%

Related articles

iPhone 4 SunSpider test results (22% faster than iPhone 3GS)
JavaScript SunSpider test: iOS 3.1.3 versus iOS 4 GM
JavaScript SunSpider: HTC Evo versus HTC Incredible versus Nexus One
Sencha: BlackBerry Torch: The HTML5 Developer Scorecard

Is a hash faster than a Switch in JavaScript?

Tuesday, August 17th, 2010

I stumbled across this concept recently and I thought I’d share it, because I don’t generally see this pattern being used. More importantly, I also share test results that show that maybe it’s not always a good idea to use this pattern…

The problem with Switch statements

The basic switch statement in JavaScript looks something like this:

var foo = 'c';

switch(foo) {
  case 'a':
  break;

  case 'b':
  break;

  case 'c':
  break;

  default:
}

So what’s wrong with this? The JS engine has to examine a bunch of unrelated cases until it finds the relevant one, executes the code, then breaks out of the switch because the job is done (this is why it’s important to break!). In the above example we had to go through case A and case B until finally reaching case C. What’s worse is that if it didn’t match any of these cases, the JS engine has to jump through ALL of the cases before it reaches Default, the fall-through case.

Actually it’s not so bad, as long as there are a limited number of cases. It’s probably no big deal if you only have a few cases to jump through. The problem gets bigger as your number of cases increases (some of you may know this as O(n)). What happens when there’s 10 cases? Then there’s potentially 10 checks on cases (assuming what ended up being executed was the default). 100 cases? Then potentially 100 checks.

What would be better is if there were a way to reduce the number of checks. One way would be to put the most frequently used cases at the top. This would alleviate some of the pain, but you still end up with extra processing while the JS checks each case. It would be ideal to avoid this extra processing altogether.

An alternative: The hash table

There is a way to avoid this extra processing! It’s by leading the code directly where it needs to go, without unnecessary checking of unrelated cases.

You can do this using a hash. In JavaScript we accomplish this with an object:

var foo = 'c';
var cases = {};
cases['a'] = function() {
  alert('I am A!');
};
cases['b'] = function() {
  alert('I am B!');
};
cases['c'] = function() {
  alert('I am C!');
}

if(typeof cases[foo] == 'function') {
  // only executes if we've defined it above
  cases[foo]();  // I am C!
} else {
  // default (the fallthrough)
}

There we go! No extra case checking here. We’ve led the JS straight to the code we want to execute!

Performance improvement…?

So.. this hash lookup seems faster in theory, but what about in practice? Unfortunately I ended up with some mixed results…

I created a simple performance test on jsperf.com and got these results:

Browser Switch ops/sec Hash table ops/sec % Difference
Chrome 6.0.490.1 dev 34,606,469 43,329,587 25% faster
Safari 5.0 16,777,216 10,854,824 35% slower
Opera 10.61 4,405,782 2,719,336 38% slower
Firefox 3.6.3 2,785,802 2,400,586 14% slower
IE6 147,870 206,869 40% faster
IE7 144,735 191,179 32% faster
IE8 350,085 472,417 35% faster
Mobile Safari (iOS4 on iPhone 3GS) 668,053 416,366 37% slower
Android (2.2 on Nexus One) 605,693 864,591 42% faster

* Ops/sec = Operations per second. Higher is better
** Chrome, Safari, Opera, and Firefox were tested on Mac OSX 10.6.4 2.53GHz Intel Core i5. IE tests were run on Windows 7 64bit 2.4GHz Quad Core

The Results

From the results, it looks like the hash optimization is only a benefit for Chrome, IE6-IE8, and Android. That’s quite a specific sampling. My guess is that the other browsers have implemented some sort of Switch statement optimizations that actually turn the hash optimization into an antipattern.

More info

Although I first read about this online, by no surprise this trick also appears in Nicholas Zakas’s High Performance JavaScript in a section on “Lookup Tables” (p. 72).