Scrolling a div on multitouch devices using JavaScript

With the emergence of touch devices and the slow but steady tendency of replacing the mouse as an input device, this leaves us with fingers. Both the iOS platform and the Android platform sports APIs for dealing with touches from fingers, for native apps that is. But what about web-apps? Enter the JavaScript touch DOM events. In this post I'll discuss how to use the touch events to scroll a container (a div) using a finger, e.g. on an iPhone.

What you'll need
First of all you need a touch device or a simulator like the iPhone simulator to test your code. The code itself can be written in any text editor, however it is recommended to use an editor with a build in debugger or a test environment that supports debugging JavaScript touch events. You might be thinking that Firebug or Drosera is great for debugging, but how exactly would you trigger the touch events in your desktop browser? Well, you can't. This is why I'm using Dashcode from the iOS SDK for my JavaScript coding. More on using Dashcode in a later post.
Note that this code is written for WebKit based browsers, but should work in principle on other touch browsers as well.

The HTML
<div id="holder">
   <div class="content">
       your content here
   </div>
</div>


Notice that the content is inside of a div within the holder div, this is because you can't scroll the div in which the touch event is attached to.

The CSS

#holder{
   width: 300px;
   height: 200px;
   background-color: blue;
   -webkit-box-shadow: 3px 3px 5px #000;
   padding: 10px;
}

#holder div{
    width:100%;
    height:100%;
    color: white;
    overflow: auto;
    -webkit-user-select: none;
}


Not much revolutionary here, but do note that we set the "overflow" property to auto on the content div, not the holder itself. Also, we set the "-webkit-user-select" to "none" on the content div, thereby preventing the user of selecting the content. This is optional.

The JavaScript
There are three main touch events:
  • touchstart - Called when a finger touches the listening container (finger down)
  • touchmove - Called repeatedly when a finger moves (finger down and moving)
  • touchend - Called when a finger leaves the listening container (finger up)
Each of these methods gives us an event with a touches array. Remember, there might be more than one finger since these APIs are made to handle multitouch devices. To get a hold of the vertical position of one finger you would query the touches array for one finger and get it's pageY property, like this: event.touches[0].pageY

From this we can calculate and set the scrollTop property of the content container. But, wait! If you try this the rest of the page will also scroll, not what we wanted. The reason why this happens is because the touch event bubbles all the way up to the window, making the browser think it needs to scroll the window because you have dragged your finger on it. We need to tell the event to stop bubbling, we will handle the event ourselves. We do this by calling: event.preventDefault()

The complete JavaScript code will then look something like this:

var startPos;
function init()
{
    var scrollArea = document.getElementById('holder');
    scrollArea.addEventListener('touchstart', function(event){
        touchstartHandler(event);
    }, false);

    scrollArea.addEventListener('touchmove', function(event){
        touchmoveHandler(event);
    }, false);

    scrollArea.addEventListener('touchend', function(event){
        touchendHandler(event);
    }, false);
}

function touchstartHandler(e)
{
    startPos = e.touches[0].pageY;
}

function touchmoveHandler(e)
{
    var touch = e.touches[0];
    var targetBox = e.currentTarget.getElementsByTagName("div")[0];
    e.preventDefault();

    var fingerMoved = startPos - touch.pageY;
    startPos = touch.pageY;

   targetBox.scrollTop = targetBox.scrollTop + fingerMoved;

}

This is just a simple example, but it's something you'll probably end up using a lot. Speaking of...

When should you override the default window scroll
On an iPhone there is not as much need for this method as you'll probably scroll the entire screen in most cases anyway. However, the iPad is a different story. Take a look at the iPad version of gMail created by Google. They are using this method with success, and they have added inertia scrolling as well. This can be accomplished using the last touch event we did not implement, touchend. Maybe more on this in a later post. Anyway, you should use you own scrolling when it's appropriate to scroll only a part of the app, like a spilt view list or something like that.

Downscaling images in Flash applications

The other day I ended up in an interesting discussion regarding how to downscale a large image in a Flash based application. What we figured out was that even though the FlashPlayer can do bilinear smoothing on scaled images automatically, this will fail when the scaling factor is more than 2 times of the source image. Hence a custom method is needed.

The simple approach (scaling down less than 2 times the size)
If the loaded image is to be scaled less than 2 times (400 -> 200) simply use the build in "smoothing" property of the Bitmap Class. This can be set directly on the Bitmap itself or via different DisplayElements such as the mx:Image in Flex. If you set this property to true the Bitmap will use bilinear scaling, achieving quite good results.

Grayscale bilinear interpolation
Now, if you need to do actual resampling of an image, in other words create a new smaller image, then you need to do this manually using the "draw()" method of the BitmapData Class. Use the bitmapData.draw() method and set the "smoothing" property to true. This will give you the same result as setting the smoothing property on an Bitmap container, however it uses less memory as only the pixels for the resulting image is stored (if you remember to dispose of the original data!).

Scaling down more than 2 times
Let's say that you have an image that is quite large and you want to create a thumbnail from it. A thumbnail is often way smaller than 2 times smaller than the original image. It would be quite nice to use the above method to just scale the image, or better resample the image with the "smoothing" property set to true. However, if you try to do this the FlashPlayer will simply fail and default back to using the "nearest neighbor" algorithm. If you try it you'll soon discover that this will give you a horrible result.

What you'll need to do in this case is to do an iterative or recursive resampling of the image at hand. In layman terms this means resampling the image several times, each time no more than 2 times downscaling factor. You could call this a multi pass resampler (actually it would be a multi-multi pass resampling as the draw() method is also multi pass). You would still use the build in "BitmapData.draw()" method to accomplish this, you only divide the work into multiple jobs. This will give you a much better result. Note that in my experience, function calls in ActionsScript 3 are quite expensive, hence a iterative approach would maybe be preferable to a recursive one.

Using PixelBender
If you have decided to go for the manual approach of resampling the picture, you can use a PixelBender shader to do the job. In this case the actual resampling will be a lot faster (as the PB language is based on C), however applying the PB shader and passing the image back and forth takes time. See the speed section..

Combining the two
So, what if you have an application and you don't know in advance the downscale factor. Well, then you'll need to use both of the approaches like so:

  1. Calculate the scale factor
  2. If the scale factor is less than 2, set "smoothing" to true on the container or do a singe pass resampling.
  3. If the scale factor is more than two, use the multi pass scaling approach. And set the "smoothing" property of the container to false (else it will be blurry).
What about speed?
Let's say that you have an image that is 1200 x 1200 pixels and you're resampling it to 200 x 200. This is a 6 times resample and will look like ass using only the "smoothing" property, but it is faster.

Let look at some hard results. In this test we see how much time is used extra compared to no "nearest neighbor" scaling (default in Bitmaps if smoothing is off).

MethodTime in ms
Smoothing property on
0 ms
Multi pass resample
9 ms
Multi pass PixelBender
32 ms
Using the smoothing property takes no extra time, because it fails and defaults back to nearest neighbor. The other two are both pretty fast, however even though the PB algorithm is way faster than the others, we see that the applying of the shader eats a lot of time, thereby making the multi pass resampler method the way to go on a large rescaling operation.