Thursday, November 24, 2011

Swiping Slides On Mobile

These days working on our current project was very interesting. Creating a web page for mobile devices is not so easy task. Getting page to work on all platforms can be fun, interesting and in some moments a hard task. It is like making page to works with IE 6 :).
We already have templates which works in mobile, and support both (landscape and portrait) page orientation. But for the first time we were supposed to implement more complicated logic in our mobile sites. We got a task to create "coverflow". We were supposed to create slides and allow user to swipes them.
Making this for desktop would be an easy task. We should choose a favorite javascript framework as jQuery or prototype, after that we should find needed effect, and in few hours we will be ready. 
We tried to use same approach for the mobile platform. But after some researching we decided that we will not use any framework and we will make our own solution using plain javascript.
You may asking your self: Why they do that? Why they didn't use any frameworks?
But answer on these two questions is simple. First off all we do it because we don't need it on every page, just on few of them. It is a mobile page so the size of library also is important for us. Prototype and jQuery are excellent libraries, they are robust, they have everything what we need but they are too heavy. And  for us size is very important. Less script mean better performance.


Creating a page which will work on mobile device for us mean that the page should be fully functionally on iPhone 3 and 4 and device which are running on android 2.xxx and above, but style should not be broken on other device. 
This rule allow us to use new features of CSS3 and HTML5, so i was very exited to start working on the script.


This is a short list of requirements:

  • Coverflow should contain slides, which will be automatically changed after some time. 
  • The time of holding one slide should be user defined
  • The time of duration of effect also should be define by user
  • After user do swipe left or right, animation stops and slides can be changed only when user do swipe left or right
  • After page reload animation should work again.
First step in implementing "coverflow" was defining what should be done. On the next image is shown basic illustration how the problem should be resolved.



So we decided to create javascript function which will take container element, effect duration and slide holding time.
As container we use div element which represent the placeholder for slides, and then every element with name "coverflow" will be treated as slide. 
As we already said, all slides should be placed inside container and then script will change zIndex property of slides, so that first slide will be shown to the user for the specified slide time. After the slide time is expired new slide is on the top and previously slide is pulled to the last position.
Here is snippet of javascript code for initialization coverflow, creating slides and processing animation.

....
if (duration) {

   effectDuration = duration + 'ms';
}
if (period) {
   scrollPeriod = period;
}
if (getCoverflowElements()) {
   coverflowElementsSize = getCoverflowElements().length;
}
if (coverflowElementsSize > 1) {
   container = document.getElementById(conatinerDiv);
   window.addEventListener('load', createSlides);
   window.addEventListener('load', resizeContainer);
   window.addEventListener('resize', resizeContainer, true);
   container.addEventListener('touchstart', onTouchStart, false);
   container.addEventListener('touchmove', onTouchMove, false);
   container.addEventListener('touchmove', eventPropagator, false);
   container.addEventListener('touchend', onTouchEnd, false);
}
....

Here we checked initial parameters, and if elements exists we registered events which will be used for creating slides, adjustment  pages and detection of page swiping.
So After window is loaded we will call "createSlides" and "resizeContainer" functions. First function call will create slides and second one will resize  slides container to fit dimension of largest slide, so on that why will we avoid breaking of page layout. Also the second function will be called every time when window is resized, for example when we change page orientation. 
Now we can make short look at code used in this two functions:


var createSlides = function(event) {
   var cowerflow = getCoverflowElements();
   for ( var i = 0; i < cowerflow.length; i++) {
        var contentDiv = cowerflow[i];
        contentDiv.style.zIndex = cowerflow.length - i;
        contentDiv.addEventListener('webkitTransitionEnd', processEvent, true);
        contentDiv.addEventListener('transitionend', processEvent, true);
        contentDiv.addEventListener('oTransitionEnd', processEvent, true);
  }
  setDeltas();
  animation=setTimeout(changePage, scrollPeriod);
  window.removeEventListener('load', createSlides);
}   


var resizeContainer = function () {
   var cowerflow = getCoverflowElements();
   var containerHeight = 0;
   for ( var i = 0; i < cowerflow.length; i++) {
       var contentDiv = cowerflow[i];
       if (contentDiv.offsetHeight > containerHeight) {
          containerHeight = contentDiv.offsetHeight + 1;
       }
   }
   container.style.height = containerHeight + 'px';
   setDeltas();
}

As you can see, "resizeContainer" function iterates through the slides and found the biggest height and set container with that new value, it is used to prevent breaking of layout when page orientation is changed. We choose resize event because the orientation event is fired before the page content is resized. But resize event is fired after content is resized and at that moment we can resize container and prevent page layout  to be broken.
First function "createSlides" iterate through the slides and register event listener   for transition end event. As you can see, we registered three events, because at the moment each major browser vendor handle this event with different name. So for webkit based browser we use "webkitTransitionEnd" (Safari and Chrome), mozilla browser use "transitionend" and opera use "oTransitionend". After that we start slides animation by setting up timer. Be aver that if you want to be able to create timer for Andriod devices with version from 1.5 up to 2.2 timer function mus be pass in as variable, using anonymous function or making direct call to function will not have effect on this devices.
Next function which we will see is "changePage", it is simple function which will checking is animation on, and if it is on it will move to the next slide using function "moveSlide". Here is code of these two functions:

var changePage = function() {
   if(animationOn){
       moveSlide('left', window.innerWidth, effectDuration);
   }
}

function moveSlide(dir, position, duration) {
   var cf = getCoverflowElements();
   var style = setStyle(dir, position, duration);
   cf[current].setAttribute('style', style);
}

The "moveSlide" function set new style to the current slide, new style is defined using function "setStyle". Basically setting of new style to the current slide will cause it's transition to new position what will cause raising of transition end event and then "processEvent" function will be called. In next few lines you can check code of "setStyle" function:

function setStyle(direction, newPostion, effectDuration) {
   var style = '';
   style += 'position:absolute;';
   style += 'width:100%;';
   style += 'top:0px;';
   style += 'left:' + (direction == 'left' ? '-' : '') + newPostion + 'px;';
   style += 'z-index:' + getCoverflowElements()[current].style.zIndex + ';';
   /* W3C */
   style += 'transition-duration:' + effectDuration + ';';
   style += 'transition-property:left;';
   /* Firefox 4 */
   style += '-moz-transition-duration:' + effectDuration + ';';
   style += '-moz-transition-property:left;';
   /* Safari Chrome WebKit */
   style += '-webkit-transition-duration:' + effectDuration + ';';
   style += '-webkit-transition-property:left;';
   /* Opera */
   style += '-o-transition-duration:' + effectDuration + ';';
   style += '-o-transition-property:left;';
   return style;
}

You can ask your self, why we used this kind of approach for changing slide style. Why we didn't simply changed complete style class. Because aplication architecture do not allow us to simply add new css file, because we don't know where this will be used, and it can be included in any part of application.
Now we can check what's happened when transition is ended. What will happened when this event is raised is defined in "processEvent" function so first of all we will check code used in this function:

function processEvent() {
   var cf = getCoverflowElements();
   var style = resetStyle();
   cf[current].setAttribute('style', style);
   for ( var i = 0; i < cf.length; i++) {
      var zIndex = cf[i].style.zIndex;
      if (++zIndex > cf.length) {
        zIndex = 1;
      }
      cf[i].style.zIndex = zIndex;
   }
   if (direction === 'left') {
      if (++current > cf.length - 1) {
          current = 0;
      }
   } else {
       if (--current < 0) {
          current = cf.length - 1;
       }
   }
   if (animationOn) {
       animation=setTimeout(changePage, scrollPeriod);
   }
}

So we can see that we first reset style of current slide, and after that we reorder slides (we adjust z index of every slide), update information about current element, and at end we check is animation still turned on and if it we set animation timer again.
And at end we will see how we allow user to swipe slides. We don't have one event who is responsible for handling stuff likes slide swiping. When we said that user want to swipe slide, we think next: user touch the screen and move finger to the right or to the left. Because of this we used three different event to detect does user want to swipe slide. We've used touch start event for initialization of detection process (you can check "onTouchStart" function for more details).

function onTouchStart(event) {
   if (event.touches.length == 1) {
      // take first position
      startX = event.touches[0].clientX;
      startY = event.touches[0].clientY;
   }
}

Touch move event we used for detection of finger move, if you take look at "onTouchMove" function you can see logic which we used for fallowing finger moving on the screen and how we make decision did user want to swipe.

function onTouchMove(event) {
   if(stopX){
      var tempX = stopX;
   }
   // Update last position
   stopX = event.touches[event.touches.length - 1].clientX;
   stopY = event.touches[event.touches.length - 1].clientY;
   if (swipeDirection =='') {
       if (stopX > startX) {
           swipeDirection = 'right';
       } else if (stopX < startX) {
           swipeDirection = 'left';
       }
   }
   // check direction from last two moves if there is change in direction from left to right cancel
   if (swipeDirection == 'right' && stopX < tempX) {
       swipeCanceled = true;
    } else if (swipeDirection == 'left' && stopX > tempX) {
       swipeCanceled = true;
    }
}

On this event we also registered "eventPropagator" function. This function is used to decide, does default behavior should be disabled. 

var eventPropagator = function(event){
     var deltaY = stopY - startY;
     var deltaX = stopX - startX;
     if (deltaY < deltaYMax && deltaY > -deltaYMax) {
         // Prevent default behavior
         if(!preventDefualtsEnabled){
             preventDefualtsEnabled = true;
             container.removeEventListener('touchmove', allowDefaults, false);
             container.addEventListener('touchmove', preventDefault, false);
         }
    }else{
        if (preventDefualtsEnabled){
             preventDefualtsEnabled = false;
             container.removeEventListener('touchmove', preventDefault, false);
             container.addEventListener('touchmove', allowDefaults, false);
             swipeCanceled = true;
         }
     }
}

And last used event is touch end. At this point we check is swiping happened, if yes then call swipe function otherwise reset event status variables. As you can see from code below:

function onTouchEnd(event) {
    if (swipeCanceled) {
         reset();
         return;
    }
    var deltaX = 0;
    if (stopX > 0) {
         deltaX = stopX - startX;
    }
    var deltaY = 0;
    if (stopY > 0) {
         deltaY = stopY - startY;
    }
    if (deltaX > deltaMinX && deltaY < deltaYMax && deltaY > -deltaYMax) {
         swipe('right');
    } else if (deltaX < -deltaMinX && deltaY < deltaYMax && deltaY > -deltaYMax) {
         swipe('left');
    }
    reset();
}

At begin of this blog I've said that we implementing this on our own because we don't need some super javascript framework with thousand unused functions, and that we want to have some small javascript function which will be included only when it is needed. So size of this script is 6.92 KB, this was good enough for us, but this can be even smaller if you use some tools for reducing size of javascript. One of that kind of tools is Closure compiler. Closure compiler offer three different methods for compiling your javascript. First and simplest is "Whitespace only" which will reduce size of this script to 5.21 KB, the second method is simple compilation and it will reduce size of this script to 2.69KB what is more then enough. If you need more details about Closure you can visit next link Closure Compiler
To get complete script you can download it from next link: CoverflowAnimator.js
Example of HTML page which use this coverflow you can download from: test page