Google Closure Tutorials

SlideShow tutorial

Posted in Animation, Closure Code, DOM, UI by googleclosuretutorials on November 19, 2009

LOOKING FOR SOURCE? Zip containing all the required files, and a copy of this tutorial.

For my next tutorial, I’m going to look into building a SlideShow. There is a Slide object in goog.fx.dom that we could use, but I really wanted to have a look at goog.fx.Animation so I’ve used that instead. If you’ve not read the InlinePopup tutorial, now would be a good time as I’ll be skipping over some of the more basic concepts that I explained last time.

First up, let’s define our namespace and requires:

goog.provide( ‘neame.ui.SlideShow’ );

goog.require( ‘goog.events’ );
goog.require( ‘goog.dom’ );
goog.require( ‘goog.dom.query’ );
goog.require( ‘goog.style’ );
goog.require( ‘goog.fx.Animation’ );
goog.require( ‘goog.array’ );

You should be able to tell what sort of things we’ll be working with from that (events, DOM, inline styles, animation and some array methods). Next up is the largest method of our SlideShow object – the constructor:

/**
* SlideShow constructor
* @args slideshow:String – ID of the slideshow container
* speed:int – how long in ms it should take to slide
* axis:char – ‘x’ or ‘y’
* automated:bool – if true, it will slide by itself until ‘stop’ method called
* pause_ms – how long it should pause when sliding automagically
* All arguments are optional except slideshow
*/
neame.ui.SlideShow = function( slideshow , speed , axis , automated , pause_ms ){

// set vars from arguments passed, or default
this.automated = automated || false;
this.pause_ms = !isNaN( pause_ms ) ? pause_ms : 8000;
this.speed = speed || 750;
this.axis = axis || ‘x’ ;

// get useful slide information – all based on first list, then first list item
this.slidecontainer = goog.dom.query( ‘#’ + slideshow + ‘ > ul’ )[ 0 ];
this.slides = goog.dom.query( ‘#’ + slideshow + ‘ > ul > li’ );

// get dimension and length of slides – making the assumption all are the same size
this.slideSize = goog.style.getBorderBoxSize( this.slides[ 0 ] );
this.numslides = this.slides.length;

// set float by mapping the array according to axis
if( axis == ‘x’ )
goog.array.map( this.slides , function( slide ){ goog.style.setFloat( slide , ‘left’ ); } );
else
goog.array.map( this.slides , function( slide ){ goog.style.setFloat( slide , ‘none’ ); } );

// make global reference
this.id = neame.ui.SlideShow.Instances.length;
neame.ui.SlideShow.Instances[ this.id ] = this;

// not currently sliding
this.sliding = false;
this.timer = null;

if( this.automated )
this.nextSlide();

};

We start off the constructor by defining our object variables. The first 4 lines should be pretty self-explanatory – all we’re doing here is looking for the arguments this instance was called with and if not present, falling back to some sensible default values.

The next 2 lines are a bit more interesting – here you can see 2 calls to goog.dom.query. If you’re looking for an equivalent to jQuery’s $() method, you’ve just found it. Since goog.dom.query returns an array of matched Elements, we can just pull off the first matching UL for the slidecontainer by adding [ 0 ] after the method call.

Now that we have references to the container and slides, we can get the dimensions of the slides with a call to goog.style.getBorderBoxSize. Note here again I’m just pulling the first Element from the slides array (I’m assuming all slides are of a regular width and height).

Ok so we’ve done plenty of gets so far… Time for a couple of sets. By calling goog.array.map, we can apply a function to every element in the array, which in this case is every slide. All we are doing here is explicitly setting the float of every slide to ‘left’ or ‘none’ depending on the axis passed when the object was instantiated.

Next, we add a global reference to the slide (required for setTimeout later), set some initial variables (not sliding, timer is null) and finally, if automated, call nextSlide. And what does nextSlide do, you ask? Well, let’s have a look…

/**
* nextSlide
* calculates the position of the next slide and calls slideFromTo
*/
neame.ui.SlideShow.prototype.nextSlide = function(){

// if we are already sliding then exit
if( this.sliding ) return;
// get container position
var containerPos = goog.style.getPosition( this.slidecontainer );
// calculate next slide position based on axis
if( this.axis == ‘x’ ){
// calculate target x position
var tx = ( Math.abs( containerPos.x – this.slideSize.width ) / this.numslides < this.slideSize.width ) ? ( containerPos.x – this.slideSize.width ) : 0;
// slide from current x to tx
this.slideFromTo( [ containerPos.x , 0 ] , [ tx , 0 ] );
}else{
// calculate target y position
var ty = ( Math.abs( containerPos.y – this.slideSize.height ) / this.numslides < this.slideSize.height ) ? ( containerPos.y – this.slideSize.height ) : 0;
// slide from current y to ty
this.slideFromTo( [ 0 , containerPos.y ] , [ 0 , ty ] );
}

};

nextSlide is pretty simple – first off, if we are already sliding, then just return straight away – don’t execute any of the other logic. If we get to the next line, then we aren’t sliding and so we need to calculate the position of the next slide and make a call to slideFromTo, which moves the slideshow from one position to another. I’m not going to go into the longer lines too much, but all they are saying is ‘if the next slide is off the end of the list of slides, then go back to 0. Otherwise the co-ordinates for the next slide are the same as the current position minus one slide’s width (or height)’.

prevSlide is exactly the same as nextSlide but it does it the other way around – plus one slide’s width (or height). I won’t include it here but it’s in the source file.

/**
* slideFromTo
* @args oc:Array(2) – origin co-ordinates
* tc:Array(2) – target co-ordinates
*/
neame.ui.SlideShow.prototype.slideFromTo = function( oc , tc ){

// create animation object
this.anim = new goog.fx.Animation( oc , tc , this.speed , this.animationAcceleration );

// array of animation events we want to capture
var animationevents = [ goog.fx.Animation.EventType.BEGIN,
goog.fx.Animation.EventType.ANIMATE,
goog.fx.Animation.EventType.END ];
// bind listeners
goog.events.listen( this.anim , animationevents , this.tick , false, this );
goog.events.listen( this.anim , goog.fx.Animation.EventType.END , this.animationDone , false , this );

// Start animation
this.anim.play( false );
this.sliding = true;

};

slideFromTo is one of the most interesting methods. The first thing we do is create an Animation object provided by goog.fx.Animation, passing it the origin co-ordinates, target co-ordinates, time that the animation should take and an easing function (defined later).

Next we create an array of animation events we would like to listen for (I have to say, I was pleasantly surprised when I found out you can pass arrays to goog.events.listen – you could probably live without the BEGIN and END listeners but for the sake of this tutorial I’ll leave them in). We then bind this.tick to these events, so that every time BEGIN, ANIMATE or END are dispatched our tick method will be called. We also bind this.animationDone to the END event so we can do some extra things once the animation is complete.

Then we just have to start the Animation object with this.anim.play and set this.sliding to true, so that any calls to nextSlide or prevSlide will be ignored until this flag is removed. When this.anim.play is called, the BEGIN event is dispatched and then ANIMATE is dispatched every time the Animation object starts a new ‘frame’.

/**
* tick
* @args e:Event – Animation Event
*/
neame.ui.SlideShow.prototype.tick = function( e ){
if( this.axis == ‘x’ )
goog.style.setPosition( this.slidecontainer , e.x );
else
goog.style.setPosition( this.slidecontainer , 0 , e.y );
};

tick, as mentioned above, is the method that is called on every ‘frame’ in the animation. It’s pretty simple but there’s one thing you should note here – we are setting the position of the slidecontainer with the event ordinates. Since the event being passed to tick is an Animation Event, the constants x and y refer to the position that the slidecontainer should be at, at that time. So it’s a simple check of axis and then a call to goog.style.setPosition. Next up, we need to think about what to do when the animation is complete:

/**
* animationDone
* @args e:Event – Animation Event
*/
neame.ui.SlideShow.prototype.animationDone = function( e ){
// ok to slide
this.sliding = false;
if( this.automated ) this.timer = setTimeout( ‘SlideShow_Instances[‘ + this.id + ‘].nextSlide()’ , this.pause_ms );
};

This method is pretty simple and a little bit ugly… We set this.sliding to false so that we can move the slideshow again and then – if the SlideShow is automated – make a call to setTimeout to fire nextSlide() on the current object in this.pause_ms ms time. I’m sure there’s a neater way to do this but I’ve relied on a global variable SlideShow_Instances (see constructor) to make the call. We now have every method we need to automate the slideshow, except a method to make it start and to make it stop:

/***
* stop
* stops the slideshow if automated
*/
neame.ui.SlideShow.prototype.stop = function(){
this.automated = false;
clearTimeout( this.timer );
};

/***
* go
* starts automated slideshow
*/
neame.ui.SlideShow.prototype.go = function(){
this.automated = true;
clearTimeout( this.timer );
this.nextSlide();
};

These two methods are pretty simple – stop sets automated to false and halts the timer, whereas go sets automated to true, halts the timer and then starts sliding. There’s only one method we haven’t covered now, which is animationAcceleration. I have to admit I hit a bit of a snag here – there’s an easeOut method in goog.fx.easing, but for some reason I couldn’t get it to work with my script. I could require() the package with no errors, but then I would get errors telling me the method was not available. So in the end I just looked up the source and copied it into my own method, seeing as it’s so short anyway:

/***
* animationAccelleration
* ease out function
*/
neame.ui.SlideShow.prototype.animationAcceleration = function( t ) {
return 1 – Math.pow( 1 – t , 3 );
};

And last but not least, we need to instantiate our global array (to hold references to each slide in, required for automated animation) and export symbols to the document. Since there is a good chance our method names will be changed when the code is compiled, exportSymbol ensures that we always have a reference to them, even if their names change.

/***
* make Instances array and export symbols
*/
neame.ui.SlideShow.Instances = new Array();
// export symbols
goog.exportSymbol( ‘SlideShow_Instances’ , neame.ui.SlideShow.Instances );
goog.exportSymbol( ‘SlideShow’ , neame.ui.SlideShow );

Now that we have exportSymbol‘d our constructor and instances array, neame.ui.SlideShow will be available as ‘SlideShow’. This means we can make new slideshows by calling new SlideShow( id ) rather than new neame.ui.SlideShow( id ). Note that in animationDone we refer to neame.ui.SlideShow.Instances as SlideShow_Instances.

And that’s it! Make sure you grab a copy of the source and have a look at how the XHTML is structured. The SlideShow object requires an unordered list of list items, wrapped in a div element. The CSS in the .html file is also required, though SlideShow *will* overwrite any float declarations on the list items.