I’m prototyping a game interface, and one of the key UI behaviors is the user dragging an on-screen element to a target. If they drop it near the target, it should “snap” to that position. Otherwise it should move back to where they dragged it from initially:
To follow along, or if you just want grab the Macro that does that thing and get to work, download the completed file: TargetDragTutorial.qtz.zip (838k)
To achieve this behavior, we need to do the following:
- Have a draggable screen element that starts at an X and Y we define.
- Know where that element is when dropped.
- Use that position to see if we are close to a set of target coordinates.
- If so, animate to the target position.
- If not, animate back to where we started from.
The base file I start with on any mobile interface I’m mocking up looks like this:
Nothing too fancy, a Phone Dimensions to Render In Image to Phone chain that you should be familiar with, and then inside the Render In Image, I have a copy of my Where Da Mouse At? Macro, which lets me get the adjusted screen X and Y mouse positions. You can read all about it and grab a copy of it in my article on the subject. It’s inputs are published as well, which are connected to the Pixels Wide and Pixels High of the Phone Dimensions as well as the Aspect Ratio of the Rendering Desitation Dimensions which gives us the aspect ratio of the viewer window for our calculations.
Once again, I start with giving credit to Dawid Woldu at Macoscope, who’s shoulders I am sitting on to reach new heights. His article The Science Behind Snapping Scroll - Part 2 and some of the clever patches he created are integral to what I’m doing here. If you’re ever in Rochester Dawid, lunch is on me.
To allow the user to drag something, and have it behave in a manner they would expect (it drags smoothly no matter where you grab it) we need to capture where the layer is when the mouse goes down, where the mouse went down, and where the mouse is during the drag. We use all that to calculate how far the drag has been, and adjust the position of the layer that distance:
This gets us a drag behavior in the X direction that would make mom proud:
To get dragging working in both directions, we turn the dragging behavior into a Macro patch with Mouse Position and DragEvent inputs and a DraggedPosition output. Then by the magic of copy and paste, we get dragging in the Y direction done quick:
And now we have wonderful dragging all over the place:
Great, we’ve got step one and two complete: we can drag the layer around, and we store where the location when the drag stopped. On with the tough bit!
Moving after drag end
To have the layer animate to a fixed position after we’ve let go of it is actually pretty simple. When we detect the drag has stopped, we trigger an animation to a target position. Easy! This of course means we need to toggle back and forth between two different positions as the “valid” one to use based on what the user is doing: the “drag” positioning of the layer when we are dragging, and the animation position when we are not.
This is a perfect job for our buddy the Multiplexer, using the drag event as the source index. When we are actively dragging, we’ll get Source 1, which we feed with our already existing “dragged position” value, and when we stop, we’ll use Source 0 which is, um, what exactly?
Start Simple: Animate Back to 0,0
We’ll start by animating back to a fixed point no matter where we drop, as this behavior is the core of what we are after. To do that, we set up a Transition that has
0 as the end value, and the layer position when we stop dragging as the start value. Normally, we use a Classic Animation patch to drive our transition, but in this case that won’t work, because we want to always animate from the start value to the end value. We could get fancy with counters, conditionals and multiplexers, but it’s a whole lot easier to lean on the work of Dawid Woldu yet again and copy his One Way Animation patch (found in step 5 from his Scrolling article) and use that. When we stop dragging, we flip the switch on the one-way animation, which will always animate from start to end instead of toggling back and forth like a normal switch-driven classic animation. Here’s what one of the Layer Drag Coordinate macros looks like now with that in place:
Which works great! Until the 2nd time we try and drag:
What’s going on? Well, we never update the “old layer position” when we take over and animate the layer ourselves, so it is dragging form where it used to be when we let go, not from where it is now. Since we will always be animating when we drop the layer, we can just use the value of the transition as the “old” layer position:
And now we have correct dragging every time:
Choosing a Target
We’ve got the movement behavior we want, now we need to be able to choose a default position, target position, and “snap range”. We will then move the layer to the proper position based on their values when dragging stops.
To let me set those values, I add three Input Splitters to my Layer Drag Coordinate macro, set them to Number Splitters in their settings, and publish them as Default Position, Target Position, and Snap Range.
To choose which value to use, inside the Macro the Default Position goes into Source #0 of a Multiplexer, and the Traget Position goes into Source #1. Which value we use will be based on the result of a Mathematical Expression:
LayerPosition > TargetPosition - SanpRange && LayerPosition < TargetPosition + SanpRange
This returns true if we are within the set snap range on either side of the target position. The result of the multiplexer will be sent to the End Value of our snapping animation, changing where we move to to based on how close we are to the target:
If we now add a Target Position and Snap Range to our X macro, we should see it change where it snaps to based on where we drop. So, in go two Number Splitters (which are Input Splitters with the type set to Number in the settings, remember) named appropriately with the target position set to 450 and the range set to 200 (since the target is 200 pixels wide.) I’ve also added a little ‘drop zone’ image to visually indicate the target spot and the size to help me see if to worked:
Lo and behold, it does:
We can now simply add a Y target value and use the same snap range:
This gives us Y snapping as we expect, but leaves us the slight problem that our X and Y snaps behave independently:
If we are near the Y target, we move to the Y target position even if we aren’t near the X target and vice versa. We want to snap to the target only if both are near it. To do that, we’ll add a Boolean Splitter (if you guessed it’s an Input Splitter with it’s type set to Boolean, you were right) to the macros and only snap to the target if that individual coordinate is near and if that flag is set to true. We will also publish our Is Layer Near Target conditional output, and use that to set the other coordinate macro’s flag:
We now have everything working as we want: both the X and Y positions need to be inside the target for the snap to occur.
To clean up a little, I’ll add some Number Splitters for the default X and Y coordinates, then create a macro out those and both Coordinate Macros so there’s less spaghetti on screen:
Now we can set the default and target positions easily, and life is good:
We’re done! Let’s make it better tomorrow.
There’s two things that would make what we have even better! The first is to create a new output when both X and Y coordinates are near the target during a drag. This would allow us to have other patches “listen” for that, and alter their state. Perhaps you want your drop zone image to grow or highlight when you are in the zone: the possibilities are endless! The other thing that would be nice is to actually turn this into a “real” patch that people could install so it’s available from the patch Library inside Quartz Composer. I’ll save that for tomorrow, as I need to actually use this thing to get some work done right now!
Until next time, happy Quartz Composing. Here’s a link to the final file again if you missed it: TargetDragTutorial.qtz.zip. (838k)
back to top