Course notes 35
syllabus | schedule | exercises | assignments | class notes | resources | students | ARTC courses

Sliding Screens

One of the most important factors of executing a mobile app is the fact that everything is contained within a single page. With this restriction, a designer/developer must implement alternative methods to change the content presented to the user at a given time. One method, as described below, is to lay out content beyond the viewable area and "slide" the page to the desired location. The following example and breakdown covers the basic HTML,CSS,and Javascript involved in implementing this "sliding screen" functionality. It might be long, but it really isn't anything you haven't done before.

Show Entire Source Code
<head>
<style>
#pageBody {
	width:320px;
	height:480px;
	margin:50px auto;
	overflow:hidden;
	position:relative;
	outline:1px solid black;
	box-shadow:2px 2px 5px #999;
}
#wrapper {
	background:red;
	position:absolute;
	top:0px;
	left:-320px;
	height:100%;
	width:1280px;
	z-index:1;
}

#controls {
	width:100%;
	height:50px;
	position:absolute;
	bottom:0px;
	background:rgba(0,0,0,.8);
	z-index:2;
}
#controls ul {
	margin:0;
	padding:0;
	height:100%;
}
#controls ul li{
	list-style:none;
	float:left;
	width:50%;
	
	
}
#controls ul li a{
	display:block;
	line-height:50px;
	text-align:center;
	color:white;
	font-size:16px;
	text-decoration:none;
	cursor:default;
	opacity:.3;
	-webkit-transition: opacity .2s ease-in-out;
	-moz-transition: opacity .2s ease-in-out;
	-o-transition: opacity .2s ease-in-out;
	transition: opacity .2s ease-in-out;
	
}
#controls ul li a.active {
	opacity:1;
}
#controls ul li a.active:hover {
	background-color:#900;
	cursor:pointer;
}


</style>
<script>
		var canvas, ctx, intervalID, wrapper,rightBTN,leftBTN;
		var screenWidth = 320 // width of one screen
		var screenNum = 2; // the current screen
		var totalScreens = 4; // the total number of screens
		var currentPos; // left value of the wrapper, set in init for easier adaptability
		var inTransition = false;
		
        function init(){
			document.getElementById('source').style.display = 'none';
			
			//define basic canvas & document information
            canvas = document.getElementById('canvas');
			ctx = canvas.getContext('2d'); 
			
			wrapper = document.getElementById('wrapper');
			
			rightBTN = document.getElementById('right');
			leftBTN = document.getElementById('left');
              
			
			// set the wrapper to equal the height and width of the wrapper. 
			// since css is not inline, must use getComputedStyle
			canvas.width = parseInt(document.defaultView.getComputedStyle(wrapper,null).getPropertyValue('width'));
			canvas.heigth = parseInt(document.defaultView.getComputedStyle(wrapper,null).getPropertyValue('height'));
		
			//get and store the current wrapper position
			currentPos = parseInt(document.defaultView.getComputedStyle(wrapper,null).getPropertyValue('left'));

			// add event listeners for clicking on the control buttons
			rightBTN.addEventListener('click',moveRight,false);
			leftBTN.addEventListener('click',moveLeft,false);
			
			// draw the canvas content in its initial state
           	drawCanvas();
		}
		function drawCanvas(){
			// this is the same function from the old random notes!
			var points = 500;
			var rand = 0;
			ctx.strokeStyle = 'white';
			for(var i = 1; i <= points; i++){
				// increment x position
				var x = (canvas.width/points)*i;
				// determine random y position based from the vertical center
				var y = (canvas.height/2)+Math.floor(Math.random()*(rand+rand+1)-rand);
				// draw as a square with the same random size
				var w = rand;
				var h = rand;
				ctx.strokeRect(x,y,w,h);
				// update random with a value between -5 and 5
				rand+=Math.floor(Math.random()*(10+1)-5);
			}
		}
		
		function moveRight(e){
			//prevent default behavior
			e.preventDefault();
			// check that the screen is not already sliding
			if(inTransition == false){
				// check that we are not at the final screen
				if(screenNum != totalScreens){
					// toggle the transition variable
					inTransition = true;
					// increment the current screen
					screenNum++;
					// check if the button css should be changed
					toggleActivity();
					//set a timeout to begin the sliding motion
					setTimeout(slideWrapper,50);
				}
			}
		}
		function moveLeft(e){
			//prevent default behavior
			e.preventDefault();
			// check that the screen is not already sliding
			if(inTransition == false){
				// check that we are not at the first screen
				if(screenNum != 1){
					// toggle the transition variable
					inTransition = true;
					// decrement the current screen
					screenNum--;
					// check if the button css should be changed
					toggleActivity();
					//set a timeout to begin the sliding motion
					setTimeout(slideWrapper,50);
				}
			}
		}
		
		function slideWrapper(time,duration) {
			// if the time and duration variables do not exist, set them to to some defaults
			if(!time) time = 1;
			if(!duration) duration = 10; // 10 * 50 = 500 = .5 second default transition
			
			/** Penner's Cubic Ease-Out  **
			 * position = change * (Math.pow (time/duration-1, 3) + 1) + start
			 */
			 
			// calcuate the total change in position.  
            //Since the value decreases as the screen increase, must multiply by negative one
			var change = (currentPos - (screenWidth * (screenNum-1)*-1))*-1;
			//check that our current time has not exceeded the total duration
			if(time <= duration){
				// update the wrapper position based on the easing equation
				wrapper.style.left = (change *(Math.pow(time/duration-1,3)+1) + currentPos) + "px";
				//call this function agian with an updated time variable
				setTimeout(function(){slideWrapper(time+1);},50);
			} else {
				// the animation has finished, so toggle the transition variable, and update currentPos
				inTransition = false;
				currentPos = parseInt(document.defaultView.getComputedStyle(wrapper,null).getPropertyValue('left'));
			}
			
		}
		
		function toggleActivity(e){
			if(screenNum == totalScreens){ // on the last screen
				rightBTN.className = ''; 
				leftBTN.className = 'active';
			}else if(screenNum == 1){ // on the first screen
				rightBTN.className = 'active';
				leftBTN.className = '';
			} else { // on any other screen
				rightBTN.className = 'active';
				leftBTN.className = 'active';
			}
		}
</script>
</head>
<body onload="init()">
    <div id="pageBody">
        <div id="controls">
            <ul>
            <li><a href="#" id="left" class="active">←</a></li>
            <li><a href="#" id="right" class="active">→</a></li>
            </ul>
        </div>
        <div id="wrapper">
            <canvas id="canvas" width="320px" height="480px"></canvas>
        </div>
    </div>
</body>

HTML

<div id="pageBody">
    <div id="controls">
        <ul>
        <li><a href="#" id="left" class="active">←</a></li>
        <li><a href="#" id="right" class="active">→</a></li>
        </ul>
    </div>
    <div id="wrapper">
    	<canvas id="canvas" width="320px" height="480px"></canvas>
    </div>
</div>
    

There are three important elements to this structure :

  1. div#pageBody → this will represent the viewable area. Using CSS we will turn it into a window through which the child elements can be seen. In an immersive environment(like your web app), you will use the actual body for this!

  2. div#wrapper → this is the most important element. the wrapper contains all the content of the page, seen and unseen. We will move the wrapper around to change what is viewable through the pageBody's window.

  3. div#controls → notice that our controls are OUTSIDE the wrapper. Anything inside the wrapper is going to move with it! Since we want the controls to always be visible, we have placed them outside the wrapper. In addition, a class of "active" is set so that we can visibly toggle the ability to click, depending on which screen is currently in view.
* The example uses an over-sized canvas as the contents across the screens. You will notice that the width and height have been set on the canvas element, these values are arbitrary to this example. Javascript is going to change them to match the wrapper on initialization, but they must have a value set in order to easily change them.

While only a canvas is inside the wrapper for the example, in a real application ALL of your content would be inside.It would be laid out and positioned similar to a css sprite sheet, waiting to be moved into view.

CSS

#pageBody {
	width:320px;
	height:480px;
	overflow:hidden;
	position:relative;
}
    

On the pageBody we are defining the size of our window and hiding everything beyond those dimensions. For a fullscreen experience, the width and height could be set to 100%. If so, make sure to set the % values on the html element also. overflow:hidden is what hides all the child contents beyond the width and height of the pageBody. Without it, the entire wrapper would be visible! We also have to set position:relative so that we can position the wrapper in relation to the pageBody.

#wrapper {
	position:absolute;
	top:0;
	left:-320px;
    
	height:100%;
	width:1280px;
}
    

The wrapper is where the magic happens. position:absolute lets us position the element exactly where we want it by manipulating the top and left values. The height and width values are going to reflect the entire size of our content. In this case it takes up a long, horizontal area. Notice how all the numerical values reflect multiples of the pageBody's size. This allows us to easily create and move through different "screens". The current values would put us on the second screen, or one screen to the right of the wrapper's entire area.

The rest of the CSS, including the controls, is normal, basic CSS. It should pose no trouble in understanding; if it does, review earlier notes. In fact, you should be able to duplicate it without even looking at the source!

Javascript

var canvas, ctx, intervalID, wrapper,rightBTN,leftBTN;
var screenWidth = 320 // width of one screen
var screenNum = 2; // the current screen
var totalScreens = 4; // the total number of screens
var currentPos; // left value of the wrapper, set in init for easier adaptability
var inTransition = false;
	

The global variables define several important values. Along with the canvas normals, three variables are declared for holding references to elements on the document. The rest of the values represent characteristics of the setup:

  1. screenWidth → this is the value, in pixels, of the width of the viewable screen. It should match the width of the pageBody

  2. screenNum → this variable holds the current screen that is viewable. As seen in the CSS, we are starting on on the second screen.

  3. totalScreens → the total number of screens available.

  4. currentPos → the current pixel value of the the wrapper's position. This is being set in the init function so as not to hard code the starting value.

  5. inTransition → this variable is used to prevent changing screens while a slide is in progress.

function init(){
    document.getElementById('source').style.display = 'none';
    
    //define basic canvas & document information
    canvas = document.getElementById('canvas');
    ctx = canvas.getContext('2d'); 
    
    wrapper = document.getElementById('wrapper');
    
    rightBTN = document.getElementById('right');
    leftBTN = document.getElementById('left');
      
    
    // set the wrapper to equal the height and width of the wrapper. 
    // since css is not inline, must use getComputedStyle
    canvas.width = parseInt(document.defaultView.getComputedStyle(wrapper,null).getPropertyValue('width'));
    canvas.heigth = parseInt(document.defaultView.getComputedStyle(wrapper,null).getPropertyValue('height'));

    //get and store the current wrapper position
    currentPos = parseInt(document.defaultView.getComputedStyle(wrapper,null).getPropertyValue('left'));

    // add event listeners for clicking on the control buttons
    rightBTN.addEventListener('click',moveRight,false);
    leftBTN.addEventListener('click',moveLeft,false);
    
    // draw the canvas content in its initial state
    drawCanvas();
}
    

the initialization function should look pretty familiar. Canvas information is set up, document references are set, and event listeners are attached. currentPos is also initialized to the starting left value of the wrapper element. At the end the drawCanvas() function is called. In the example, this function is simply a modified version of the random squares seen in earlier notes.

function moveRight(e){
    //prevent default behavior
    e.preventDefault();
    
    // check that the screen is not already sliding
    if(inTransition == false){
        // check that we are not at the final screen
        if(screenNum != totalScreens){
            // toggle the transition variable
            inTransition = true;
            // increment the current screen
            screenNum++;
            // check if the button css should be changed
            toggleActivity();
            //set a timeout to begin the sliding motion
            setTimeout(slideWrapper,50);
        }
    }
}
function moveLeft(e){
    //prevent default behavior
    e.preventDefault();
    
    // check that the screen is not already sliding
    if(inTransition == false){
        // check that we are not at the first screen
        if(screenNum != 1){
            // toggle the transition variable
            inTransition = true;
            // decrement the current screen
            screenNum--;
            // check if the button css should be changed
            toggleActivity();
            //set a timeout to begin the sliding motion
            setTimeout(slideWrapper,50);
        }
    }
}
    

Both moveRight() and moveLeft() are the callback functions of the listeners attached to the arrow controls. The functions start by preventing the default behavior of action, which would be following the href of their element. Following that, it checks if the page is currently performing a transition. This prevents the user from stacking screen slides by accidentally pressing the button multiple times before the movement has finished. After that we check if it is possible to move farther in the desired direction. We don't want to move the wrapper beyond the viewable area, so we make sure we have not reached the respective edge of the content. If we can move in the direction, we toggle inTransition, update the current screen, toggle the visible factors of the button, and run our function which actually moves the content.

function slideWrapper(time,duration) {
    // if the time and duration variables do not exist, set them to to some defaults
    if(!time) time = 1;
    if(!duration) duration = 10; // 10 * 50 = 500 = .5 second default transition
    
    /** Penner's Cubic Ease-Out  **
     * position = change * (Math.pow (time/duration-1, 3) + 1) + start
     */
     
    // calcuate the total change in position.  
//Since the value decreases as the screen increase, must multiply by negative one
    var change = (currentPos - (screenWidth * (screenNum-1)*-1))*-1;
    //check that our current time has not exceeded the total duration
    if(time <= duration){
        // update the wrapper position based on the easing equation
        wrapper.style.left = (change *(Math.pow(time/duration-1,3)+1) + currentPos) + "px";
        //call this function agian with an updated time variable
        setTimeout(function(){slideWrapper(time+1);},50);
    } else {
        // the animation has finished, so toggle the transition variable, and update currentPos
        inTransition = false;
        currentPos = parseInt(document.defaultView.getComputedStyle(wrapper,null).getPropertyValue('left'));
    }
    
}
	

The slideWrapper() function performs the actual movement involved in sliding between screens. It takes two parameters, the current time in the transition, and the entire duration of the movement. You may notice that in the callback functions we did not pass any parameters. In javascript you do not have to pass all the variables of the function you are calling, however if they are used inside the function, you must take care to deal with the fact that they might not be there. This is what the next two lines accomplish. In this situation we avoid passing the parameters because of the extra difficulty in passing values within a setTimeout. If time and duration are not set when the function is called, they are set to some default values.

Next, the wrapper position is changed based on one of Robert Penner's easing functions. If the current time is not greater than the duration of the transition, we update the position of the wrapper based on the current time, and then use a timeout to recursively call the slideWrapper function again. This time it is absolutely necessary to pass the time variable because it needs to increment in order to complete the animation. Since you can't set parameters directly in a timeout, we instead have to use an anonymous function. our setTimeout runs a function which in turn calls slideWrapper(). This way, we can pass the value of time to slideWrapper()!

If time is greater than duration it means the sliding animation is complete. In this case we toggle inTransition back to false, and update currentPos.

function toggleActivity(e){
    if(screenNum == totalScreens){	 // on the last screen
        rightBTN.className = ''; 
        leftBTN.className = 'active';
    }else if(screenNum == 1){ 		// on the first screen
        rightBTN.className = 'active';
        leftBTN.className = '';
    } else { 						// on any other screen
        rightBTN.className = 'active';
        leftBTN.className = 'active';
    }
}
    

The toggleActivity() function serves to dim out the necessary buttons then they can't be clicked. It runs through an if statement which checks to see if the current screen is one which would result in a button not being able to be pressed. It then updates the button's classes to reflect the current condition. DOMreference.className lets one change the classes applied to an element. This way we add or remove the "active" class, which changes the opacity of the button.