Prototyping swipe and drag gestures with Framer 3

Update late 2014: This post is now over a year old. Framer Studio works with CoffeeScript (this post was written for Javascript) and certain syntax has changed. Head over to the draggable example on the Framer website for updated code.

Here at Potluck we recently started using Framer.js to prototype and try out interaction ideas we come up with for our iOS app. Framer is a Javascript framework for prototyping interactions and animations for mobile and desktop apps. It’s a great alternative to Quartz Composer as it has a less steep learning curve, and prototypes easily run both on desktop and the iPhone. We found Framer to be the perfect step up from Keynote to make realistic, interactive prototypes.

Since it’s based on Javascript, Framer is especially helpful for complex gestures that involve dragging and swiping; where elements move or fade gradually depending to swipe distance or when certain actions are triggered when the user swipes past a certain threshold.

Framer is a really young project, so examples and documentation are sparse. We thought we’d share what we’ve learned with you so far.


Before we begin, you can find the final version of all the examples below on CodePen, if you don’t want to type them out.


Activate on release

Let’s start with a simple example where dragging table cells reveals different actions.

I’ll first create a container view representing the usable iPhone screen. Notice I went with 1096px height, this is because we’ll turn our prototype into a web app, which will have a system status bar.

iphone = new Layer({
x: 0,
y: 0,
width: 640,
height: 1096
});
iphone.style.background = '#111'
iphone.style.overflow = 'hidden'

Let’s then create a cell container to house the visible part of the cell, and the actions will slide from off-screen.

cell = new Layer({
x: 0,
y: 0,
width: 940, /* including off-screen areas */
height: 100
});
cellContent = new Layer({
x: 0,
y: 0,
width: 640,
height: 100
})
cellContent.style.backgroundColor = '#eee'
cell.superLayer = iphone;
cellContent.superLayer = cell;

At this point we should have:

Let’s now make the cells draggable. In the new version of Framer, it is as easy as:

 cell.draggable.enabled = true

When you do this, you’ll be able to drag the cell around, even though this isn’t what we want to do yet.


We can use drag event handlers to implement more interesting gestures. There are three kinds of events that fire as you are dragging something. Events.DragStart fires once as you start dragging, and Events.DragEnd fires once when you let go. Events.DragMove fires continuously as you move your mouse or finger.

Draggable has a “speed” attribute which lets you tweak how the mouse movement will map to the view coordinates. We can set it to 0 to make the object not move at all in a specific axis.

cell.draggable.speedY = 0

Let’s also define what happens when you finish dragging, in this case, the element snaps back into place.

cell.on(Events.DragEnd, function() {
cell.animate({
properties: { x: 0 },
time: 0.2
});
});

Now we can add an off-screen element that reveals itself upon dragging. This will be a child view of the cell container (so it will move along with the contents of the cell) and we’ll position it just outside the screen.

cellAction = new Layer({
x: 640, /* just outside the screen */
y: 0,
width: 300,
height: 100
})
cellAction.superLayer = cell;
cellAction.style.backgroundColor = '#e00';

After adding the action bar, when you pull the cell to the left, you should see it reveal from the right side of the screen.

Now the sweet part: since the drag handler runs every time you move the bar just a little bit, we can make a direct mapping that changes the color of the action bar depending on how far we move the cell.

To make it easier to map one range of numbers (how far you moved the cell) to another range of numbers (color of the bar), let’s use a function I adapted from the awesome Processing framework.

map_range = function(value, low1, high1, low2, high2) {
if (value < low1) { return low2; }
else if (value > high1) { return high2; }
else return low2 + (high2 - low2) * (value - low1) / (high1 - low1);
}

Update: This function and many more are included in Shortcuts for Framer.

This function, given any value within the range [low1, high1], will produce its equivalent value in the range [low2, high2]. What we will do is map the x coordinate of the cell to the opacity of the action bar.

cell.on(Events.DragMove, function() {
cellAction.opacity = map_range(cell.x, -300, 0, 1, 0);
// corresponds to:
// pulling the cell 300 px left: 1 opacity
// not pulling the cell: 0 opacity
});

Now when you drag the cell, the color of the Action bar should change accordingly.

Obviously if you’ve pulled the bar sufficiently, it should remain open when you let go. To achieve this, we’ll change the DragEnd handler to check how far the you dragged, and if it’s over a certain amount, keep it open.

cell.on(Events.DragEnd, function() {
if(cell.x < -200) {
/* sufficiently dragged to snap action bar open */
cell.animate({
properties: { x: -300 },
time: 0.2
});
cellAction.animate({
properties: { opacity: 1 },
time: 0.2
});
} else {
/* snap closed */
cell.animate({
properties: { x: 0 },
time: 0.2
});
cellAction.animate({
properties: { opacity: 0 },
time: 0.2
});
}
});

Activate on threshold

So far we looked into having items snap open when you let go of the tap. What about actions that trigger without needing you to let go? A good example is the iOS6 pull to refresh: when you pull a sufficient amount, the refresh triggers even before you let go of the tap.

Let’s initiate a second cell that moves up and down:

cell2 = new Layer({
x: 0,
y: 300,
width: 640,
height: 100
});
cell2.style.background = '#00e'
cell2.superLayer = iphone;
cell2.draggable.enabled = true;
cell2.draggable.speedX = 0;

I want the cell to shrink and disappear if it’s moved down 200px. My first attempt was this:

cell2.on(Events.DragMove, function() {
if(cell2.y > 500) {
cell2.animate({
properties: {scale: 0.1},
time: 0.2
})
}
});

Why the erratic animation? As the DragMove event fires repeatedly as you keep dragging, we are trying to activate the animation many times, over and over. So let’s try to fire the animation only once after the condition is satisfied.

cell2.superView = iphone;
cell2.draggable.enabled = true;
cell2.draggable.speedX = 0;
cell2.animated = false;
cell2.on(Events.DragMove, function() {
if(!cell2.animated && cell2.y > 500) {
cell2.animate({
properties: {scale: 0.1},
time: 1
})
cell2.animated = true
}
});

You’ll notice that the animation doesn’t run anymore. Dragging and animating the item both work by changing its position, so dragging will override the animation.

We can solve this by animating a child element, inside a draggable container.

cell2 = new Layer({
x: 0,
y: 300,
width: 640,
height: 100
});
cell2.superView = iphone;
cell2.draggable.enabled = true;
cell2.draggable.speedX = 0;
cell2.animated = false;
cell2Content = new Layer({
x: 0,
y: 0,
width: 640,
height: 100
})
cell2Content.style.background = ‘#00e’
cell2Content.superLayer = cell2
cell2.on(Events.DragMove, function() {
if(!cell2.animated && cell2.y > 500) {
cell2Content.animate({
properties: {scale: 0.1},
time: 1
})
cell2.animated = true
}
});

This gives us the desired effect.


Testing on the iPhone

We found Dropbox public folders to be the easiest way to try out Framer prototypes in the iPhone. Simply upload all the files to a Public folder, open the public URL in Safari in iPhone and add the page to your home screen. When you use the PSD exporter, Framer comes with all the default settings to make the prototype look and feel like a real app.

If you’re making a lot of prototypes, we’ve found it helpful to make a “directory” file containing links to each prototype, and create a home screen icon for that instead.


Wrapping up

Hope these examples help you get a better understanding of prototyping drag and swipe gestures in Framer.

You can find the code for all examples here on CodePen.

We’d love your feedback on this post, and topics you’d like us to write about next. Reach out to @gem_ray on Twitter and let us know!