Category Archives: mobile

iScroll 4 Hacks

29 Nov 2012

Here are some hacks that I’ve discovered to get the most out of iScroll. The basic customization parameters are documented at www.cubiq.org’s website but you have to look at the cleanly laid out source to find the gems.

The current list of tips:

  • Improved Lock Direction
  • Faster Snap
  • Snap to a new element and only that element
  • Controlling Scroll Bar Position
  • Enabling SELECT, INPUT and TEXTAREA focus
  • Enabling hidden iScroll divs (where the wrapper div has display:none)
  • The bounce of death – non-scrolling content
  • Disappearing Lists
  • iScroll scrolls up when a scrolled element is clicked

Improved Lock Direction

There will come a time when you have two iScrolls on a page – one vertical and one horizontal. You will find that even if you enable “lockDirection” that iScroll will still be touchy. Try to scroll up and the slightest movement left or right will be caught by the horizontal iScroll. Try to swipe left or right and the slightest up or down movement will be caught by the vertical iScroll. Lock direction provides some improvement so be sure to enable it:

...
var myScroll = myScroll = new iScroll('yourWrapper', {lockDirection:true });
..

But to get even better “lockDirection performance” try the following hacks. Locate lines 470 and 473 within iscroll.js. We’ll want to change a number from 5 to 0.

Line 470 before:

...
if (that.absDistX > that.absDistY - 5) {
...

And line 470 after:

...
if (that.absDistX > that.absDistY - 0) { // yes, you can just remove the math too
...

Do the same with line 473. You should now notice that lockDirection with overlapping iScrolls works better than before.

Faster Snap

To enjoy faster element snapping open iscroll.js and scroll to line 437 where you will find this:

...
that.lastScale = scale / this.scale;
..

Subtract 10 from the lastScale value as seen below:

...
that.lastScale = scale / this.scale - 10;
..

Next, go to line 780 and find this line:

...
var deceleration = 0.0006,
...

Change the deceleration value to 0.002 per the next example:

...
var deceleration = 0.002, // 0.0006
...

Done, your snapping should be much “snappier”.

Snap to a new element and only that element

This tip is pretty cool – and simple. You can prevent “over swiping” in a snap-enabled iScroll by disabling momentum in one of two ways. The first approach is the easiest and most simple – and that is to disable momentum when you initialize iScroll, for example:

...
var myScroll = myScroll = new iScroll('introSliderWrapper', {snapThreshold:.5, momentum:false });
..

Note the “momentum:false” in the above configuration object. Momentum will be turned off.

Ok, so this next way of doing it is a **complete** hack and is really only of geeky interest – but if you look at the iScroll source you will see that there is some old code present that is very similar to the code that precedes it.

The new and old “duplicate” blocks are at lines 589 (new, momentum) and 608 (old, no momentum). Each block of code is surrounded by “if” statements that contain the exact same conditional. The “return” statement at the end of the first “if” block prevents the second “if” from running. It is necessary to comment the entire “if” block that starts at line 590 all the way to and including the return statement at line 605. Be aware that if you do this you are in effect creating a “global” change that will impact all iScrolls that you may employ. As a result, its best to stick with the perfectly functional momentum:false flag.

Anyway, to continue, locate the following code at line 589:

...
    // Do we need to snap?
    if (that.options.snap) {
      distX = newPosX - that.absStartX;
      distY = newPosY - that.absStartY;
      if (m.abs(distX) < that.options.snapThreshold && m.abs(distY) < that.options.snapThreshold) { that.scrollTo(that.absStartX, that.absStartY, 200); }
      else {
            snap = that._snap(newPosX, newPosY);
            newPosX = snap.x;
            newPosY = snap.y;
            newDuration = m.max(snap.time, newDuration);
      }
    }

    that.scrollTo(m.round(newPosX), m.round(newPosY), newDuration);

    if (that.options.onTouchEnd) that.options.onTouchEnd.call(that, e);
    return;
  }
...

And with it commented out:

...
    // Do we need to snap?
    /*if (that.options.snap) {
      distX = newPosX - that.absStartX;
      distY = newPosY - that.absStartY;
      if (m.abs(distX) < that.options.snapThreshold && m.abs(distY) < that.options.snapThreshold) { that.scrollTo(that.absStartX, that.absStartY, 200); }
      else {
            snap = that._snap(newPosX, newPosY);
            newPosX = snap.x;
            newPosY = snap.y;
            newDuration = m.max(snap.time, newDuration);
      }
    }

    that.scrollTo(m.round(newPosX), m.round(newPosY), newDuration);

    if (that.options.onTouchEnd) that.options.onTouchEnd.call(that, e);
    return; */
  }
...

Done.... when swiping you will no longer swipe past the the adjacent snap-to element. As mentioned, this is done easier by adding the momentum attribute to the iScroll configuration object.

Controlling Scroll Bar Position

Enabling scrollbars in iScroll is easy - making sure they are positioned where you want - easy as well as long as you position your div's appropriately. Even if you run into problems, you can hack the scroll bar into position.

To make scrollbars appear just pass a properly formatted configuration object to the iScroll, like so:

...
new iScroll('scrollWrapper',{
   vScrollbar: true, // the vertical scroll bar has been enabled
   momentum:true
});
...

This will make the vertical scrollbar appear - but in some cases the scrollbar may not be aligned with your scrolling content but aligned along the right-edge of the window. Sure, it may work but if you have a multi-column layout then chances are that you may not want the scroll bar "removed" from the scrolling area. The fix here is simple and achieved via CSS - make sure that your "scrollWrapper" div has its position style set to one of the following:

  1. absolute
  2. relative
  3. fixed
  4. inherit (as long as the inherited value is one of the preceeding three values)

Once the above is done then iScroll will properly place the scrollbar along side your content.

What if you can't change your css or don't want to? Then you can attack it with a little jQuery.

iScroll will place the scrollbar divs within the scroll wrapper but without an id. Since the wrapper does have an id we can use jQuery to get the wrapper's children of which the scrollbar is a member. The last child will always be the scroll bar. For example:

...
window.addEventListener('load',function(){
   new iScroll('scrollWrapper',{ // the scrollbar will be the last child within the wrapper
      vScrollbar: true,
      momentum:true
   });

   // lets get the wrapper's children
   var children = $('#scrollWrapper').children();
   // the children array has a length of two, the last one is always the scrollbar
   // position the scroll bar as desired
   children[1].style.height = 350 + 'px';
   children[1].style.top = 0 + 'px';
   children[1].style.left = 190 + 'px';
},false);
...

As you can see from the above its a simple matter to attack the iScroll scroll bar. If you like you could obviously change this:

children[1].style.height = 350 + 'px';

To this:

children[children.length-1].style.height = 350 + 'px';

So that you **always** get the last child as who knows how you're structuring your content.

In addition to this you may not always have a scroll bar present depending on the length of your content. Therefore, we have to check for its existence in some way. In a typical setup the wrapper will only have a single child if the scrollbar is not there. The following check is based on this assumption:

...
window.addEventListener('load',function(){
   new iScroll('scrollWrapper',{ // the scrollbar will be the last child within the wrapper
      vScrollbar: true,
      momentum:true
   });

   // lets get the wrapper's children
   var children = $('#scrollWrapper').children();
   if (children.length > 1){ // if there is more than one child, the scrollbar must be present
      children[1].style.height = 350 + 'px';
      children[1].style.top = 0 + 'px';
      children[1].style.left = 190 + 'px';
   }
},false);
...

Enabling SELECT, INPUT and TEXTAREA focus

iScroll will disable the ability to give focus to certain form elements preferring instead to treat all interactions with the screen as something that must go through iScroll itself. You can get around this by adding the following function to the onBeforeScrollStart event - shown here as a configuration object while instantiating iScroll. You could also just add it to the iScroll source - view source, scroll down until you see the onBeforeStartEvent and add the function there.

...
var scroll = new iScroll('content',
   {scrollbarClass: 'myScrollbar',
    handleClick: false,
    useTransform:true,
    onBeforeScrollStart: function (e) {
        var target = e.target;
        while (target.nodeType != 1) target = target.parentNode;
        if (target.tagName != 'SELECT' && target.tagName != 'INPUT' && target.tagName != 'TEXTAREA')
            e.preventDefault();
	}
    });
...

Enabling hidden iScroll divs

This isn't a hack per se as iScroll has a method to deal with the issue. In cases where you instantiate an iscroll on a hidden wrapper div and then reveal the wrapper by changing its display style to something other than "none" you will find that the iScroll-ed div will just bounce in its wrapper, refusing to scroll.

The fix is to do the following **after** you have revealed the wrapper - the example uses the refresh and scrollTo methods to enable scrolling:

...
// instantiate a new iScroll
var myScroll = new iScroll('myDiv');

function reveal_iScroll_div(){
   document.getElementById('myDiv').style.display = 'block';
   myScroll.refresh();
   myScroll.scrollTo(0,0,0);
}
...

...

The bounce of death - non-scrolling content

Sometimes your content will just bounce in its container and not scroll at all. There are a couple things that cause this - 1 of them is the section preceding this one, the other is likely to be a simple line of CSS. Make sure that the div that is wrapping your content has a css display value of "block".

Disappearing Lists

Sometimes you'll have an iScroll whose contents are updated according to whatever the user has selected. If you have a long list and quickly scroll through them - and then try to update the contents of the scroll wrapper while your list items are flying by you may find that the list may update with new content but not be visible. That is, if you inspect the DOM you plainly see that your elements are there correctly positioned but they do not display (observed in Android 4.1.2).

It appears that we may need to stop the scrolling before updating the list. Something like this:

...
if (_myScroll){
    _myScroll.scrollTo(0,0,0);
}
// now update the list
...
// _myScroll.refresh() might not work here, the "scrollTo" before the update seems to help
...

iScroll scrolls up when a scrolled element is clicked

(7/15/2016) I just had this particular issue that took me quite a while to figure out - I had inited an iScroll with content that appeared to function correctly, but no matter what buttons I clicked in my scrolled content the result was a) what I programmed the button to do, and b) the iScroll would scroll back to the top (interestingly the scroll bar would NOT scroll to the top).

Turned out that I had instantiated the iScroll twice in quick succession on the same variable - maybe creating some sort of race condition? You would think that one would overwrite the other... but fixing the code so that I created an iScroll instance only once resolved the issue.

Playing Audio in Android/iOS Web Apps

11 Sep 2012

If you’re familiar with the state of HTML5 in Android land you know that many CSS3 features are not supported, among them is the AUDIO tag. There is a library out called SoundManager2 that claims to offer a “reliable cross-platform audio” solution but after trying it on mobile platforms I think it falls short of that promise.

SoundManager 2’s approach is to use HTML5 where possible with Flash as a fallback mechanism. Makes sense for desktop OS’s, not so much for mobile. iOS, even with its own quirks (no inline playing, no autoplay, etc) has functional HTML5 audio/video so that’s not much of an issue. Android is very different in that the HTML5 AUDIO tag is not implemented. Flash exists on Android but that has its own issues, among them: we are forced to assume that the Flash Player exists on the Android device in the first place which isn’t always the case. Add to that the fact that many users have to install the Flash Player manually on their devices as there is an absence of any auto-install capability. The final nail in that coffin is Adobe’s abandonment of the Android Flash Player altogether meaning no one can install it.

SoundManager 2 can no longer claim to be useful for mobile when its completely unusable on Android. Maybe when a version of Android with HTML5 Audio support takes the lion’s share of the Android ecosystem SoundManager 2 may again have use within the mobile space.

In any event, as mentioned, the lack of HTML5 AUDIO tag support continues to make supporting sound in Android Web Apps a challenge. The solution I’ve come up with (not that its a novel one) is to use the HTML5 VIDEO tag instead. VIDEO has support at least back to Android 2.2. Most events are supported though some don’t do quite behave in the manner that you would like for them to which makes the creation of a custom audio player somewhat of a challenge. In the end you may just have to bail on some lofty ideas/features that you may have for your own custom player but at least you can still get some sound working reliably.

It has been said that you can in fact feed the VIDEO tag an MP3. However, what happens within Android differs across its varied flavors. In Android 2.2 what will happen is that Android will not play the audio in-page but instead launch the native media player – and what you have is a black screen, devoid of controls albeit with your MP3 playing. In Android 4 you can get MP3 audio to work inline.

Due to the different behaviors and presentations between Android versions I’ve personally switched to using video files(mp4) to deliver sound. The result is that in Android 2.2 the native media player will display the video content – an image keyframed every second – which satisfies some mystery requirement that allows the on-screen controls to appear. This is much better than a black screen. On Android 4 we don’t care about the visual aspect of the video because my implementation avoids it entirely.

The concept is to use single VIDEO tag that is placed off-screen, use Javascript to change its src and control the sound via the relevant HTML5 media events.

It is important to note that some VIDEO tag attributes are necessary for this to work as without them you’ll be greeted by silence. Below is the HTML to use:

...
<video id="videoEl" autobuffer height="10" width="10" controls style="position:absolute;top:0px;left:-99px;">
  <source src="something.mp4">
</video>
...

The autobuffer is there because it can’t hurt. The one thing that is necessary is the presence of the video controls (yes, even though you won’t see them). Otherwise this is all pretty simple – you can see that the VIDEO tag is placed off screen.

I won’t post my complete setup as thats a bit of code, however, here’s a great start; first some HTML to be styled however you like:

...
<ul id="songs" class="music_list">
  <li src="vid/song_1.mp4">This is song number one</li>
  <li src="vid/song_2.mp4">This is the second song</li>
  <li src="vid/song_3.mmp4">Number three</li>
  <li src="vid/song_4.mp4">The last song</li>
  <li qwe="noSound">Just a list item, no video/sound to play here</li>
</ul>
...

In this case I’ve elected to keep the URL to the video in the LI tag. Here’s my JavaScript that utilizes the above, populates the VIDEO src, and controls the VIDEO (our sound):

...
var videoPlayer = (function(){
  var _isPlaying = false;
  var _elTarget;
  var _video;
  function _resetAllVideoStyles(){
    var lists = document.getElementsByTagName('ul');
    for (var i=0;i<lists.length;i++){
      if (lists[i].getAttribute('class').indexOf('music_list') != -1){
        var desc = lists[i].childNodes;
        for (var j=0;j<desc.length;j++){
          if (desc[j].nodeType == 1){
            desc[j].setAttribute('class','');
          }
        }
      }
    }
  }
  function _ifOldAndroid(){
    return navigator.userAgent.indexOf('Android 2.3.3') != -1 ? true : false;
  }
  function _loadVideo(t,str){
    t.setAttribute('class','mp_selected');
    _video.src = str;
    _elTarget = t;
    _video.load();
    setTimeout(function(){
      _video.play();
      _isPlaying = true;
    },1000); // NECESSARY timeout, play too soon and nothing will happen
  }
  function _stopVideo(){
    _video.pause();
    _isPlaying = false;
    _resetAllVideoStyles();
  }
  function init(v_id){;
    var el = _video = document.getElementById(v_id);
    el.addEventListener('error',function(){
      console.log('Cannot load the file');
    });
    el.addEventListener('pause', function(){
      console.log('paused/stopped');
    });
    el.addEventListener('loadstart', function(){
      console.log('starting to lo
    });
    el.addEventListener('play', function(){
      console.log('playing');
    });
    el.addEventListener('durationc
      console.log('duration changed');
    });
    el.addEventListener('progress', function(){
      console.log('progress: file is being downloaded');
    });
  }
  function dispatcher(t,str){
    if (t == _elTarget){
      _elTarget = null;
      _stopVideo();
    } else if (_isPlaying){
      _stopVideo();
      setTimeout(function(){
        _loadVideo(t,str);
      },100);
    } else {
      _loadVideo(t,str);
    }
  }
	
  return{
    init:init,
    dispatcher:dispatcher
  }
})();
window.addEventListener('load',function(){
  var list = document.getElementsByTagName('li');
  for (var i=0;i < list.length;i++){
    if (list[i].getAttribute('qwe') !== 'noSound'){
      list[i].addEventListener('click',function(e){
        videoPlayer.dispatcher(e.target,e.target.attributes[0].nodeValue);
      },false);
    }
  }
},false);
...

As you can see events are bound to all qualifying LI tags – the qualifier being that the parent UL must have a class called “music_list” assigned to it.

Thats a decent start for you – all the events are setup – you will quickly see that they don’t really fire when you expect them to. For example, the onplay event seems to start as the video is buffering, not when the video actually starts to play. ondurationchange may fire more than once – etc…. so as i mentioned before making a slick UI will be a bit of a chore. One thing that should be useful, though granted maybe not a cureall is the video.paused property. At least you will know when the video is playing or not, but as I already noted the video is considered playing when what I believe based on observation is buffering and not actual playing.

As far as iOS the video does not play inline, rather, the native video player will come up full screen. This happens because contrary to every other mobile browser Apple has decided in its totalitarian wisdom to buck the standard and suspend the mobile app in favor of its built-in media player. That’s a rather ironic tactic IMO as they profess to be on the side of HTML 5 & standardization but as usual they change the game per their whim and there’s nothing that can be done about it. Apple gives no explanation for this so it is what it is.

So that’s about – doing the above gets you something that works on both major mobile OS’s – Android with its caveats, and iOS with its mobile media player hijacking the

Scrolling in LungoJS HTML5 Framework

12 Jul 2012

I’ve been looking at other frameworks lately, among them LungoJS. After fiddling with the framework I had difficulties creating scrolling views. The docs say that getting a scrolling area is as simple as adding a “scrollable” class to a div – not quite, there are some other requirements.

To get scrolling articles you must have the following structure – notice the DIV that wraps the content:

...
<article id="something" class="scrollable">
   <div> <!-- This DIV is important!! -->
      Your content in here
   </div>
</article>
...

If you attempt to create a scrollable div inside an article tag you **must** also set a height for that div, like so:

...
<article id="something">
   <div id="myDiv" class="scrollable" style="height:300px;">
      <div> <!-- This DIV is important!! -->
         Your content in here
      </div>
   </div>
</article>
...

Notice the ID in the scrollable divs – ID is **required**, you will see as much in the console if you forget this.

Note the DIV that is the immediate child of the scrollable div – that is also required. No need to style it or add an ID. Your content must be wrapped by a DIV – P’s, Lists and header elements also work. Spans do not.

If the scrolling area is not an article tag then you **must** include the height for the scrolling div, which is illustrated in the second example above.

I noticed a lot of confusion in the forums and was confused myself – IMO the docs should be amended with the above.

Mobile Safari Debug Console Breaks @Media Query

03 Jul 2012

Spent a lot of time spinning my wheels on this – in these cases you always think about whats changed since the last time your layout did what you wanted…and I backed up all the way to settings I enabled/disabled on the iPhone, and found the issue!

If you’re using an @media query like the following:

...
@media screen and (max-width:320px) and (orientation: portrait){
    /* your css here */
}
...

You will notice that it **breaks** when Mobile Safari’s debug console is enabled.

We can look into this a little further by using JavaScript to get the device’s reported width and we can infer the orientation from the number of degrees of rotation. To discover this information:

...
alert('width: ' + window.innerWidth + '\n orientation: ' + window. orientation);
...

Holding the iPhone in a portrait orientation with the debug console on we see the following values when reloading the test page:

  • width: 320
  • orientation: 0

The same test with the debug console off… is the same as with it on – there’s no smoking gun here as to why this is happening. The above @media query for unknown reasons is just plain broken with the debug console enabled!

Prevent Element Hilighting via CSS

17 May 2012

This is a tip that a guy in our office provided to me – preventing elements on a page from being hilighted if clicked on multiple times. This is useful if you bind events to elements that are being used as interface widgets such as navigation controls/buttons but you do not want the element to become hilighted if the user clicks more than once on them. This will happen if you have for example a DIV as a button container where the background of the DIV is the button art.

[edit 1/21/2014]

I added -webkit-touch-callout, -ms-user-select and the IE-specific JS.

...
#your_element_id{
    -webkit-touch-callout: none;
    -ms-user-select: none;
    -webkit-user-select: none;
    -khtml-user-select: none;
    -moz-user-select: none;
    -o-user-select: none;
    user-select: none;
}
::selection {
    background: transparent;
    color: none;
} 
::-moz-selection{
    background:transparent;
    color: none;
}
...

IE is a little different – in IE 8 (at least) you can use this bit of javascript:

document.onselectstart = function(){ return false };