A Draggable Mobile App Showcase

metro_showcase_header

By Sara Soueidan

Today I’d like to share with you an interactive and touch-optimized mobile app showcase concept for showcasing a mobile app screenshot (in this case, on Windows Phone). The screenshot will be draggable and swipable, and you’ll have a couple of extra options to view how the app would look like within a mobile phone frame.

You can view the demo or fork the project on GitHub. Here’s an animated GIF showing how the demo would work (GIF ~= 4.5MB, sorry about the big size). The screen is draggable and swipable, and the buttons work as shown in the image below.

metro-showcase-how-to

Please note that this demo works only in browsers that support the Javascript APIs used. I provided a couple of polyfills but the demo will only work in browsers that these polyfills provide fallback for. See the Javascript section for details.

This demo was inspired by the large number of dribbble shots showcasing Windows phone app concepts, so I thought I’d recreate this showcasing concept but add some interactivity to it.

The Flat Lumia Phone PSD Mockup used in the demo is by Corey Ginnivan from Dribbble. I provided two colors in the demo resources that include a red and a white frame.

The Markup

We’ll wrap our showcase in a wrapper with the id as-wrapper that will hold a container for the mobile frame and app screenshot, and a section for the app description which will appear at some point during the interaction (we’ll get to that in a moment).

The mobile frame and the screenshot will be positioned absolutely. The frame needs to be positioned absolutely to overlap the screenshot, and the screenshot will be positioned this way too so that we can change its position via Javascript.

The phone frame we’re using has 3 buttons in its lower section. We’re going to add 3 buttons on top of these buttons with a transparent background, so that it seems like these built-in buttons are clickable. Then we’re also going to add two navigation arrows to the right of the frame to scroll the screenshot left and right.

The left-most arrow on the phone frame will scroll the app screen to the left to get it completely inside the boundaries of the phone frame. The windows button will scroll it back out to its initial position. The magnifier will launch the “focus” mode of the showcase, and the left and right navigation arrows will scroll the screenshot left and right respectively.

<div class="as-wrapper">
  <div class="as-container">
    <div class="as-frame preventSelect" id="as-frame">
      <img src="images/lumia-red.png" alt="Omnia Phone Frame" />
    </div>
    <div class="as-instructions" id="as-instructions">
        <p>Drag or swipe app screenshot left and right with your mouse or finger.</p>
        <p>Use buttons at the bottom of the frame to scroll screen and focus mobile frame.</p>
        <button>Got it!</button>
    </div>
    <div class="as-nav-buttons" id="as-nav-buttons">
      <button class="as-button as-nav-button as-left-nav-button preventSelect" id="as-left-nav-button"><img src="images/nav-arrow-left.png" alt="Left"></button>
      <button class="as-button as-nav-button as-right-nav-button preventSelect" id="as-right-nav-button"><img src="images/nav-arrow-right.png" alt="Right"></button>
    </div>
    <button class="as-button as-slide-button preventSelect" id="as-slide-button"></button>
    <button class="as-button as-reset-button preventSelect" id="as-reset-button"></button>
    <button class="as-button as-focus-button preventSelect" id="as-focus-button"></button>

    <div id="draggable" class="as-screen preventSelect">
      <img src="images/app-screen.jpg">
    </div>
  </div>
  <div class="as-app-description preventSelect" id="as-app-description">
    <h2>Your awesome app features and upsell</h2>

    <p>Minima vero quibusdam error accusamus explicabo commodi deleniti ipsa debitis enim quae tempore molestias veritatis. Quo saepe voluptatibus officiis debitis necessitatibus magnam id possimus maxime atque amet. Officiis cupiditate deserunt!</p>
    <p>Minima vero quibusdam error accusamus explicabo commodi deleniti ipsa debitis enim quae tempore molestias veritatis.</p>
    <p>Minima vero quibusdam error accusamus explicabo commodi deleniti ipsa debitis enim quae tempore molestias veritatis.</p>
    <a href="#">
      <img src="images/ws-button.png" alt="Download App from Windows Store" class="download-button" id="download-button"/>
    </a>
  </div>
</div>

You have probably noticed the class preventSelect that I added to almost all elements, especially those inside the as-container. What this class does is prevent these elements (via CSS) from being selected. Otherwise selected elements would get in the way of the drag action and things would get messy!

The CSS

Let’s go over the styles quickly. All the styles are basic and easy to understand so I won’t be getting into too much detail. The “heart” of this demo is the JavaScript part.. I added comments to the CSS code where necessary. We’ll start with the general styles relevant to the demo.

/* lazy girl/man's reset :) */
*{
  /*box sizing should be border box on .as-frame and .as-screen otherwise js calculations will need to change to include padding*/
  -moz-box-sizing: border-box;
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  list-style: none;
}
body {
  background: #F0E9DD url("../images/02.jpg") repeat;
  color:#eee;
  font: 300 1.2em "Source Sans Pro", sans-serif;
  overflow-x: hidden;
}
/* cross-browser prevent user select: http://stackoverflow.com/a/4358620 */
.preventSelect {
  -moz-user-select: -moz-none;
  -khtml-user-select: none;
  -webkit-user-select: none;
  -ms-user-select: none;
  user-select: none;
}
.as-wrapper{
  width:95%;
  margin:0 auto;
  min-height:550px;
  position:relative;
}
.as-container {
  position: relative;
  height: 550px;
  width: 1300px;
  overflow: hidden;
  margin:20px auto;
  transition: width .6s ease;
}
/*class will be added via Javascript to shrink the frame + screenshot container and center it*/
.shrink{
  width:297px;
}

The frame and screenshot container is given a height equal to the height of the phone frame (simply because we don’t need it to be bigger than that), and now we’ll move on to the frame and screenshot styles.

/*div containing the app screenshot*/
.as-screen {
  height: 75.6%;
  width: 1190px;
  top: 8.5%;
  left:0;
  position: absolute;
  cursor: move;
  cursor:grab;
  cursor:-moz-grab;
  cursor:-webkit-grab;
  z-index: 1;
  overflow: hidden;
  transition: all .5s ease-out;
}
 /*app screenshot*/
.as-screen img{
  pointer-events:none;/*to prevent image being dragged and interfering with the screen drag*/
}
/*div containing the phone frame*/
.as-frame {
  position: absolute;
  z-index: 1;
  left: 0;
  width: 300px;
  height:550px;
  z-index: 2;
  pointer-events:none;
}
/*the phone frame*/
.as-frame img{
  width:100%;
  pointer-events:none;
}
.as-instructions{
  position: absolute;
  top:100px;
  left:50px;
  width:200px;
  padding:20px;
  color:white;
  background:rgba(0,0,0,0.75);
  z-index:20;
  pointer-events:none;
}
.as-instructions button{
  background: none; 
  border:none;
  background-color: #B33E41;
  color:white;
  padding:5px 10px;
  margin-top:15px;
}

Note that we need to set the pointer events on the frame to none to make sure it doesn’t block the events on the screenshot.

Now we’ll style and position the control and navigation buttons.

.as-slide-button, .as-reset-button, .as-focus-button{
  width:40px;
  height:40px;
  position:absolute;
  bottom:48px;  
  left:30px;
  background:none;
  border:none;
  cursor:pointer;
  z-index:20;
}
.as-reset-button{
  left:130px;
}
.as-focus-button{
  left:225px;
}
.as-nav-buttons{
  height:30px;
  position:absolute;
  bottom:30px;
  left:320px;
  z-index:20;
}
.as-nav-button{
  width:40px;
  height:40px;
  background:none;
  border:none;
  color:black;
  text-align:center;
  cursor:pointer;
}

The last thing we’re going to style is the description section which will appear when the screenshot has been dragged fully into the inside of the phone frame.

/*initially the description will be hidden with opacity set to 0*/
.as-app-description {
  opacity: 0;
  width:100%;
  position:absolute;
  right:0;
  top:0;
  bottom:0;
  margin: 5% 0;
  padding: 0 50px 0 450px;
  transition: opacity .3s ease-out;
}
.as-app-description h2{
  margin-bottom:1em;
}
/*this class will be added via Javascript to show the description*/
.visible-description {
  opacity: 1;
}
.download-button {
  margin: 30px 0;
}

We’ll make the demo as responsive as possible. I’m saying “as responsive as possible” because a draggable showcase like this will look best on big/desktop screens, because of the width of the screenshot, but we’ll make it work for all screen sizes.

@media screen and (max-width: 64em){
  .as-app-description{
    padding-left:380px;
  }
}
@media screen and (max-width: 50em){
  .as-wrapper{
    padding:1%;
  }
  .as-app-description{
    position:static;
    margin-top:100px;
    padding:0;
    opacity:1;
  }
}

@media screen and (max-width: 30em){
  .as-container{
    width:297px;
    margin:30px auto;
  }
}

On small screens, we’ll let the screenshot remain inside the phone frame with overflow set to hidden on the container.

That’s pretty much it for the styles. Now let’s move on to the interactive part!

THE JAVASCRIPT

First things first: polyfills and plugins. For starters, I won’t be using any JavaScript framework; we’ll be going vanilla.

I’ll be using the awesome Javascript classList API, which is not fully supported in all browsers, but it’s awesome so I’ll be using it anyway, and I’ll add Eli Grey’s classList polyfill which works in IE8, and provides basic classList.add(), classList.remove(), and classList.toggle() support (which is more than enough for this demo) to at least as far back as Android 2.1.

For browsers that don’t support addEventListener, I’ll be using Jonathan Neal’s eventListener polyfill.

Finally, I’ll be using Hammer.js to add touch swipe support for the draggable screenshot.

One last thing: I’m no JavaScript ninja, so if you are and think that this code can be optimized, feel free to drop a comment. Now let’s get started.

We’ll start by caching some variables and initializing others with some basic calculations which we’ll need throughout the code.

(function(){
    var el = document.getElementById('draggable'),
      //get screen width and offset..
        elWidth = parseInt(window.getComputedStyle(el,null)['width']),
        elLeft = el.offsetLeft,
      //..use those to calculate right offset
        elRight = elLeft + elWidth,
      //do the same for the phone frame
        frame = document.getElementById('frame'),
        frameLeft = frame.offsetLeft, 
        frameWidth = parseInt(window.getComputedStyle(frame,null)['width']),
        frameRight = frameLeft + frameWidth,
      //cache app description and control and navigation buttons 
        desc = document.getElementById('as-app-description'),
        scrollInButton = document.getElementById('as-slide-button'),
        resetButton = document.getElementById('as-reset-button'),
        focusButton = document.getElementById('as-focus-button'),
        leftNavButton = document.getElementById('as-left-nav-button'),
        rightNavButton = document.getElementById('as-right-nav-button'),
      //instruction that appears at beginning of demo
        tip = document.getElementById('as-instructions'),
      //cache container
        container = el.parentNode;

Wow, that’s a lot! So what exactly are all those needed for?

First, I cached all DOM elements that we’re using to listen for events so that we can attach event handlers to them. Then, I determined the left and right offsets for each of the draggable screen and the mobile frame, because we’ll be needing these for the scrolling and dragging functions. The right offset is calculated by adding the left offset to the width of the element.

Next, we’ll attach event listeners to the control and navigation buttons, and we’ll also add the swipe support with Hammer.js.

//call the scrollScreen function when the screen is swiped left or right
var scrollLeftOnSwipe = Hammer(el).on("swipeleft", function(event) {
    scrollScreen(220, 'left');
    hideTip();
});
var scrollRightOnSwipe = Hammer(el).on("swiperight", function(event) {
    scrollScreen(220, 'right');
    hideTip();
});

scrollInButton.addEventListener('click', function(){
    scrollScreen(elWidth, 'left');
}, false);
leftNavButton.addEventListener('click', function(){
    scrollScreen(220, 'left');
}, false);
rightNavButton.addEventListener('click', function(){
    scrollScreen(220, 'right');
}, false);
resetButton.addEventListener('click', resetScreen, false);
focusButton.addEventListener('click', focusFrame, false);

The scrollScreen(val, dir) function takes two arguments: a val, which is the amount (in px) by which we want to scroll the screen, and a dir, which determines the direction in which we want to scroll it.

function scrollScreen(val, dir){
    hideTip();
    var left = el.offsetLeft;

    if(dir == 'left'){
        var deltaRight = elRight - frameRight;
        if(deltaRight >= val){
            left -= val;
        }
        else{
            left -= deltaRight + 5;
        } 
    }
    else if(dir == 'right'){
        var deltaLeft = frameLeft - left;
        if(deltaLeft >= val){
            left += val;
        }
        else{
            left += deltaLeft;
        }
    }

    if(left <= frameLeft && elRight >= frameRight - 5){
        el.style.left = left + 'px';
        elRight = left + elWidth;// in case elRight = frameRight the desc shows
        showHideDesc();
    }    
}

function showHideDesc(){
    if( elRight <= frameRight + 30 && !focus){
        desc.classList.add('visible-description');
    }
    else{
        desc.classList.remove('visible-description');
    }
 }

 function hideTip(){
    tip.style.display= "none";
}

 //when the reset button is clicked the screen is returned to its start position
 function resetScreen(e){
    el.style.left = 0;
    elLeft = 0;
    elRight = elWidth;
    showHideDesc();
}

This function calculates the difference between the screenshot offsets and that of the frame offsets, and scrolls the screen by the value passed to it as long as the difference is bigger than this value. If it’s smaller, it scrolls it by the value of the difference. At the end of the function, another function showHideDesc() is called, which shows and hides the app description section based on the position of the screenshot with respect to the frame: if the screenshot’s right offset equals that of the frame’s right offset (i.e the screenshot is fully inside the frame) then the description is shown, else it’s hidden.

When the left arrow button (the one on the phone frame) is clicked, the scroll function is called with a value equals to the width of the screenshot, which basically means: scroll the screen to the max until it’s fully inside the frame.

The focus button (the magnifier) will cause a mode change for the demo. When it is clicked, the container containing the phone frame and the app screenshot will shrink (by adding the .shrinkclass) to fit the size of the frame. It’s overflow is hidden and it’s centered on the screen, this way the frame will contain the app screenshot and you can drag/swipe left and right to view the app inside of it. (see image below)

metro-showcase-new-focus-modeThe app showcase in ‘focus’ mode.

var focus = false;

function focusFrame(){
    hideTip();
    if(focus == false){
        container.classList.add('shrink');
        focus = true;
        //show/hide description based on whether we're in the 'focus' state or not
        desc.classList.remove('visible-description');
    }
    else{
        focus = false;
        container.classList.remove('shrink');
        el.style.left = '0';
        elRight = elWidth;//so that the description remains hidden
    }
}

The last thing we’re going to do is add the drag functionality to the app screen. We’ll be attaching event handlers for mousedownmousemove, and mouseup events, and their equivalent touchstarttouchmove, and touchend events to support touch devices.

What will happen is that every time the mouse is down (i.e the drag starts), the position of the mouse/finger is saved, and the current left offset of the screen is calculated, and a value, delta, is also calculated, which determines the difference between the mouse position on drag start and the left offset of the draggable element (app screen).

After that, as the mouse moves, its position is updated, and, as its position changes, so will the left offset of the draggable screen, as long as the boundaries of the screen don’t exceed the boundaries of the frame from the left and right respectively. The right offset of the screen should not go below the right offset of the frame, and the left offset of the screen should not go above the left offset of the frame.

Now that we’ve cleared up the logic behind the dragging function, here’s the code for that function.

//these values are reset on every mousedown event
 var mouseDownStartPosition, delta, mouseFrameDiff; 

 el.addEventListener("mousedown", startDrag, false);
 el.addEventListener("touchstart", startDrag, false);

 function startDrag( event ) {
     hideTip();
     //prevent contents of the screen from being selected in Opera and IE <= 10 by adding the unselectable attribute      el.setAttribute('unselectable', 'on');        elLeft = el.offsetLeft,      mouseDownStartPosition = event.pageX,      delta = mouseDownStartPosition - elLeft;             document.addEventListener("mousemove", moveEl, true);      document.addEventListener("mouseup", quitDrag, false);      document.addEventListener("touchmove", moveEl, true);      document.addEventListener("touchend", quitDrag, false);  }    function moveEl(e){    var moveX = e.pageX,        newPos = moveX - delta;        elLeft = newPos;        elRight = newPos + elWidth;             //-5 is a magic number because the phone frame has extra 5 px on the right side with a transparent bg    //if you're using a different phone frame img u may not need this, but keeping it won't do any harm :)    if(elRight >= frameRight - 5 && elLeft <= frameLeft){
       el.style.left = newPos + 'px';
       showHideDesc();
   }
}

function quitDrag(){
    document.removeEventListener('mousemove', moveEl, true);
    el.setAttribute('unselectable', 'off');
}

To make sure the screen doesn’t keep moving when the dragging stops, we attached an event handler to the mouseup (and touchend) event, that will call a function which in turn will remove the corresponding event handlers from the mousedown and mousemove events.

Where to go from Here

That’s it, I hope you like this showcase and find it useful! You can view the demo or fork the project on GitHub

This article was originally published at http://sarasoueidan.com/blog/draggable-metro-app-showcase/.

Modern Web Newsletter

Subscribe to receive the Modern Web tutorials, sent out every second Wednesday.

Top