The topic of this article
is to show you how to implement a sliding panel menu for a mobile
device (smartphone or tablet) using HTML and Javascript. The idea is to find a way
to implement the same behaviour as the one used by the
SlidingPanelLayout in Android with HTML and Javascript for using it
in a web based mobile application or an hybrid mobile application
(such as the one developed with Phonegap). In order to get the
display more user-friendly, Bootstrap CSS will be used only for its
CSS classes.
Explanations will be
completed with code snippets and a link to a Github repository will
be provided for testing a real example.
Here is the plan of this article:
- PART 1: Emulate a mobile device with your desktop browser
- PART 2: The strategy
- PART 3: HTML markup
- PART 4: Handling the touch events
- PART 5: Displaying and hiding the menu
- PART 6: Getting a working example
PART 1: Emulate a mobile device with your desktop browser
A convenient way of developing mobile application based on HTML and Javascript is to test it directly in your desktop browser in order to get access to your favourite development tools. Chrome provides some tools for emulating mobile devices; you can read how to enable them in this page.
PART 2: The strategy
The strategy for implementing the sliding panel menu is to get two panels in the DOM, one for the main display and one for the menu. The menu panel should slide from the browser view to outside the browser view when we want to hide the menu and it should slide from outside the browser view to the browser view when we want to display the menu.
The user should be able to
make the menu slide by touching the screen and the horizontal sliding
of the menu should be exactly the sliding of the user finger on the
screen. Of course, the user will not be able to make the menu slide
more than the menu width, in order to make the menu stick to the
browser view left border when it is displayed.
The slide of the menu
panel should be handled thanks to touch events and CSS animations.
The three following sections will respectively explain how to define
the HTML markups, the touch event handlers and the sliding animation.
PART 3: HTML markup
As we want to implement a mobile application, it could be nice to get a bar at the top of the application for displaying the icon and the name of the application. Then of course the main display zone below the application bar should contain two panels, one for the menu, and one for the main content.
<body onload="main.initialize()"> <div id="output"> <header id="headercontainer"> <!-- the header with the icon and the name of the application --> </header> <div id="swipingcontainer" class="maincontainer"> <div id="menu" class="leftcontainer"> <!-- menu panel --> </div> <div id="main" class="rightcontainer"> <!-- main content panel --> </div> </div> <div> </body>
The “main.initialize”
function called on the onload of the body is defined in a separate
javascript file and will mainly handle the size definition of the div
elements (panels) and will attach the event listener to the correct
div elements.
PART 4: Handling the touch events
In the “initialize” function, the div elements are resized as described above, and the width of the menu is store in a variable (this.menuWidth). Some listeners are also attached for touchstart, touchmove and touchend events on the swipingcontainer div element which is the parent of the menu and the main content div elements.
The idea is to detect the
beginning of the swipe and then compute the horizontal distance of
the swipe. During the swipe, the menu should be moved horizontally
according to the horizontal distance between the finger position at
the beginning of the swipe and the current finger position. As we
previously decided to let the menu stick to the left border of the
application, it is not possible to make the menu slide more than the
menu width. Then when the swipe ends, we need to decide if the user
makes the menu slide as much as necessary for hiding or displaying
it. We decide to consider that the menu should be hidden (if it was
previously displayed) when the swipe is from the right to the left
and the swipe distance is higher than half of the menu width. We also
decide to consider that the menu should be displayed (if it was
previously hidden) when the swipe is from the left to the right and
the swipe distance is higher than half of the menu width.
The callback function that
is attached to the listener of the touchstart event on the swiping
container needs to store the finger horizontal position which the
finger position at the beginning of the swipe. The left position of
the menu at the beginning of the swipe also needs to be stored in
order to be used when computing the menu left position during the
swipe.
touchStartHandler : function(evt) { evt.preventDefault(true); var touch = evt.changedTouches; if ((touch != null) && (touch.length > 0)) { this.startX = touch[0].screenX; if (this.display.menuVisible) { this.menuLeftPosition = 0; } else { this.menuLeftPosition = - this.menuWidth; } } }
The callback function that
is attached to the listener of the touchmove event on the swiping
container needs to get the current horizontal position of the finger
and then compute the new left position of the menu with the
constraint of sticking to the left border of the application when
totally displayed.
touchMoveHandler : function(evt) { evt.preventDefault(true); var touch = evt.changedTouches; if ((this.startX != null) && (touch != null) && (touch.length > 0)) { var moveX = touch[0].screenX; var diff = moveX - this.startX; var menuCurrentPosition = this.menuLeftPosition + diff; if ((- this.menuWidth <= menuCurrentPosition) && (menuCurrentPosition <= 0)) { var menu = document.getElementById('menu'); menu.style.left = menuCurrentPosition + "px"; } } }
The callback function that
is attached to the listener of the touchend event on the swiping
container needs to get the horizontal position of the finger when the
swipe ends for computing the distance between the horizontal finger
position at the beginning of the swipe and the one at the end of the
swipe. Thanks to this distance, we will be able to decide if the menu
should be displayed or hidden according to the criteria described
above.
touchEndHandler : function(evt) { evt.preventDefault(true); var touch = evt.changedTouches; if ((this.startX != null) && (touch != null) && (touch.length > 0)) { var moveX = touch[0].screenX; var diff = moveX - this.startX; var menuCurrentPosition = this.menuLeftPosition + diff; var menu = document.getElementById('menu'); if (menuCurrentPosition < (- this.menuWidth / 2)) { this.display.menuVisible = false; this.displayMenu(menu, false); } else { this.display.menuVisible = true; this.displayMenu(menu, true); } } }
PART 5: Displaying and hiding the menu
The displayMenu function called in the code snippet above in the callback function of the touchevent on the swiping container is in charge of displaying or hiding the menu (depending on the value of its second parameter) and trigger the animation for sliding the menu from its current position to its final position which is totally displayed or totally hidden.
The animation is declared in the CSS on the left
property, as you can see this is a fake animation (the values of the
property at the beginning and at the end are the same). The idea is
that this animation exists in the CSS, even if it does nothing.
@-webkit-keyframes displayMenuAccount { from {left : 0px;} to {left : 0px;} }
In the javascript file the displayMenu function is:
displayMenu : function(menu, display) { var rule = this.getMenuDisplayAnimation(display); this.addAnimationToMenu(menu, rule.animation, rule.duration, display); }
The getMenuDisplayAnimation function returns an object
that contains the definition of the animation as a string and the
duration of the animation:
getMenuDisplayAnimation : function(display) { var animation = '@-webkit-keyframes displayMenuAccount {\n' + 'from {left : ' + menu.style.left + ';}\n' + 'to {left : ' + (display ? '0px' : ((-this.menuWidth) + 'px')) + ';}\n' + '}'; var distance = display ? (-this.getMenuLeftPosition(menu)) : (this.getMenuLeftPosition(menu) + this.menuWidth); var duration = (distance * 500) / this.menuWidth ; return {'animation' : animation, 'duration' : duration}; }
Then the animation definition and the animation duration
are used for adding the animation to the menu in the
addAnimationToMenu function. This function is in charge of finding
the animation defined in the CSS (see above) and replacing it by the
new definition computed in the getMenuDefinitionDisplay function.
When the animation definition has been replaced in the CSS, this
animation is attached to the menu element.
Finally a callback is attached to the menu on the
webkitAnimationEnd event. This function is menuDisplayed and is in
charge of setting the correct left property to the menu div element
when the animation ends (otherwise the left property of the menu will
be set again to the value at the beginning of the animation).
addAnimationToMenu : function(menu, rule, time, display) { if( document.styleSheets && document.styleSheets.length) { var i = 0; var j = 0; var found = false; var currentStyleSheet = null; while ((i < document.styleSheets.length) && !found) { currentStyleSheet = document.styleSheets[i]; if (currentStyleSheet.rules && currentStyleSheet.rules.length) { j = 0; while ((j < currentStyleSheet.rules.length) && !found) { if (currentStyleSheet.rules[j].name === "displayMenuAccount") { found = true; } j++; } } i++; } if (found) { currentStyleSheet.deleteRule(j-1); currentStyleSheet.insertRule(rule, j-1); } else { document.styleSheets[i-1].addRule(rule); } } else { var s = document.createElement( 'style' ); document.getElementsByTagName( 'head' )[ 0 ].appendChild( s ); s.innerHTML = rule; } menu.addEventListener("webkitAnimationEnd", this.menuDisplayed.bind(this, {'display' : display}), false); menu.style.webkitAnimation = 'displayMenuAccount ' + time + 'ms linear 1'; }, menuDisplayed : function(args, evt) { var menu = document.getElementById('menu'); menu.style.left = (args.display ? 0 : -this.menuWidth) + 'px'; menu.style.webkitAnimation = ''; }
PART 6: Getting a working example
You
can find a working example in my github repository (the
slidingpanelmenu folder).