Categories:

Monitoring touch actions at every stage, swipe image gallery

On the previous page, you saw how to detect swipes on a touch surface, and packaged that knowledge into a generic swipedetect() function:

swipedetect(el, function(swipedir){
	// swipedir contains either "none", "left", "right", "top", or "down"
	if (swipedir =='left')
		alert('You just swiped left!')
})

That's all fine and dandy, but swipedetect() is limited in that it only lets us react to after a swipe has been made, and not during, or as the user is moving his finger across the touch surface. The later is useful in applications that need to react in tandem to a touch movement across the touch surface, such as an image gallery whereby the user can drag his finger to get a preview of the next or previous slide.

A generic ontouch function

Lets set out to create a generic ontouch() function that can be used to execute custom code at every step of a touch action, from the finger's initial contact with the surface, movement across, to lifting the finger up to end it. Surprisingly it's not much different from our swipedetect() function, only that we'll be scrutinizing the touchmove event more closely this time:

function ontouch(el, callback){

	var touchsurface = el,
	dir,
	swipeType,
	startX,
	startY,
	distX,
	distY,
	threshold = 150, //required min distance traveled to be considered swipe
	restraint = 100, // maximum distance allowed at the same time in perpendicular direction
	allowedTime = 500, // maximum time allowed to travel that distance
	elapsedTime,
	startTime,
	handletouch = callback || function(evt, dir, phase, swipetype, distance){}

	touchsurface.addEventListener('touchstart', function(e){
		var touchobj = e.changedTouches[0]
		dir = 'none'
		swipeType = 'none'
		dist = 0
		startX = touchobj.pageX
		startY = touchobj.pageY
		startTime = new Date().getTime() // record time when finger first makes contact with surface
		handletouch(e, 'none', 'start', swipeType, 0) // fire callback function with params dir="none", phase="start", swipetype="none" etc
		e.preventDefault()

	}, false)

	touchsurface.addEventListener('touchmove', function(e){
		var touchobj = e.changedTouches[0]
		distX = touchobj.pageX - startX // get horizontal dist traveled by finger while in contact with surface
		distY = touchobj.pageY - startY // get vertical dist traveled by finger while in contact with surface
		if (Math.abs(distX) > Math.abs(distY)){ // if distance traveled horizontally is greater than vertically, consider this a horizontal movement
			dir = (distX < 0)? 'left' : 'right'
			handletouch(e, dir, 'move', swipeType, distX) // fire callback function with params dir="left|right", phase="move", swipetype="none" etc
		}
		else{ // else consider this a vertical movement
			dir = (distY < 0)? 'up' : 'down'
			handletouch(e, dir, 'move', swipeType, distY) // fire callback function with params dir="up|down", phase="move", swipetype="none" etc
		}
		e.preventDefault() // prevent scrolling when inside DIV
	}, false)

	touchsurface.addEventListener('touchend', function(e){
		var touchobj = e.changedTouches[0]
		elapsedTime = new Date().getTime() - startTime // get time elapsed
		if (elapsedTime <= allowedTime){ // first condition for awipe met
			if (Math.abs(distX) >= threshold && Math.abs(distY) <= restraint){ // 2nd condition for horizontal swipe met
				swipeType = dir // set swipeType to either "left" or "right"
			}
			else if (Math.abs(distY) >= threshold && Math.abs(distX) <= restraint){ // 2nd condition for vertical swipe met
				swipeType = dir // set swipeType to either "top" or "down"
			}
		}
  		// Fire callback function with params dir="left|right|up|down", phase="end", swipetype=dir etc:
		handletouch(e, dir, 'end', swipeType, (dir =='left' || dir =='right')? distX : distY)
		e.preventDefault()
	}, false)
}

// USAGE:
/*
ontouch(el, function(evt, dir, phase, swipetype, distance){
 // evt: contains original Event object
 // dir: contains "none", "left", "right", "top", or "down"
 // phase: contains "start", "move", or "end"
 // swipetype: contains "none", "left", "right", "top", or "down"
 // distance: distance traveled either horizontally or vertically, depending on dir value

 if ( phase == 'move' && (dir =='left' || dir == 'right') )
  console.log('You are moving the finger horizontally by ' + distance)
})
*/

During each phase of a touch action- "start", "move", and "end", we get things like the direction of the touch point, distance traveled, and at the "end" phase, whether the touch movement constituted a swipe in one of the four directions, based on the same rules as that defined inside swipedetect() we saw earlier. The following illustrates ontouch in action to show various info about a touch action over a DIV:

Example (mouse simulation added for non touch devices):

<script>

window.addEventListener('load', function(){
	 var el = document.getElementById('touchsurface2')
	 ontouch(el, function(evt, dir, phase, swipetype, distance){
	 	 var touchreport = ''
	 	 touchreport += '<b>Dir:</b> ' + dir + '<br />'
	 	 touchreport += '<b>Phase:</b> ' + phase + '<br />'
	 	 touchreport += '<b>Swipe Type:</b> ' + swipetype + '<br />'
	 	 touchreport += '<b>Distance:</b> ' + distance + '<br />'
	 	 el.innerHTML = touchreport
	 })
}, false)

</script>

<div id="touchsurface2">
</div>

A swipe image gallery

With the ontouch function, we can use it to create a swipe image gallery that responds to not just swiping for changing a slide, but dragging to get a preview of the next slide. Take a look at the below:

Example (drag or swipe to move gallery, mouse simulation added for non touch device):

<style>

.touchgallery{
position: relative;
overflow: hidden;
width: 350px; /* default gallery width */
height: 270px; /* default gallery height */
background: #eee;
}

.touchgallery ul{
list-style: none;
margin: 0;
padding: 0;
left: 0;
position: absolute;
-moz-transition: all 100ms ease-in-out; /* image transition. Change 100ms to desired transition duration */
-webkit-transition: all 100ms ease-in-out;
transition: all 100ms ease-in-out;
}

.touchgallery ul li{
float: left;
display: block;
width: 350px;
text-align: center;
}

.touchgallery ul li img{ /* CSS for images within gallery */
max-width: 100%; /* make each image responsive, so its native width can occupy up to 100% of gallery's width, but not beyond */
height: auto;
}

</style>

<script>

window.addEventListener('load', function(){
	 var el = document.getElementById('swipegallery') // reference gallery's main DIV container
	 var gallerywidth = el.offsetWidth
	 var ul = el.getElementsByTagName('ul')[0]
	 var liscount = ul.getElementsByTagName('li').length, curindex = 0, ulLeft = 0
	 ul.style.width = gallerywidth * liscount + 'px' // set width of gallery to parent container's width * total images

	 ontouch(el, function(evt, dir, phase, swipetype, distance){
		if (phase == 'start'){ // on touchstart
		   ulLeft = parseInt(ul.style.left) || 0 // initialize ulLeft var with left position of UL
	  	}
		else if (phase == 'move' && (dir =='left' || dir =='right')){ //  on touchmove and if moving left or right
			var totaldist = distance + ulLeft // calculate new left position of UL based on movement of finger
			ul.style.left = Math.min(totaldist, (curindex+1) * gallerywidth) + 'px' // set gallery to new left position
		}
		else if (phase == 'end'){ // on touchend
			if (swipetype == 'left' || swipetype == 'right'){ // if a successful left or right swipe is made
				curindex = (swipetype == 'left')? Math.min(curindex+1, liscount-1) : Math.max(curindex-1, 0) // get new index of image to show
			}
			ul.style.left = -curindex * gallerywidth + 'px' // move UL to show the new image
 		}
	}) // end ontouch
}, false)

</script>

<div id="swipegallery" class="touchgallery">
<ul>
<li><img src="../script/script2/food1.jpg" /></li>
<li><img src="../script/script2/food2.jpg" /></li>
<li><img src="../script/script2/food3.jpg" /></li>
<li><img src="../script/script2/food4.jpg" /></li>
<li><img src="../script/script2/food5.jpg" /></li>
</ul>
</div>

Markup wise our gallery consists of a relatively positioned main DIV with an absolutely positioned UL element inside it. Each of the LI elements (images) are floated left so they appear side by side horizontally. We set the total width of the UL element to the width of the gallery DIV multiplied by the number of LI elements inside our script. With such a set up, to show the 2nd image for example, we'd simply set the left position of the UL element to (nth image -1) * width of gallery DIV (ie: 1 * 350).

Now to our ontouch function, which like a magic wand is what transforms our otherwise static series of images into a touch and swipe image gallery! We call ontouch() and pass in a reference to the gallery's DIV container as the first parameter, or the element to monitor touch actions on. The anonymous function that follows lets us respond to all touch actions taking place inside on a granular level. When the user first touches down on the gallery, we get the current UL element's left position (0 when page first loads). Then as the user moves his finger across the touch surface, if the dir is left or right we access the distance parameter to get the distance traveled horizontally. That value when added to the UL's initial left position gives us the new left position of the UL, which we then set the UL to, causing the gallery to move left or right in tandem with the user's finger movement.

When the user lifts his finger off the gallery (phase equals "end"), we use the swipetype parameter to determine if a legitimate left or right swipe has occurred (versus just a slow drag for example). If so, we increment or decrement the current index (curindex) of the gallery before moving the gallery to show the new image.

And there you have it. As you can see, implementing touch friendly UIs in JavaScript is relatively straightforward thanks to the Touch API

End of tutorial