Your Very Own Drag Show with JLNDragEffectManager
Cocoa developers were treated to a nice Interface Builder makeover in version 3. One effect we’ve all been admiring is the morphing animation when dragging an item from the Library palette onto a window. It’s actually not too hard and just takes a little showmanship. I’ll show you how it’s done.
The source code and demo app have been moved to their own page.
The Drag Show
The video says it all. Beautiful, isn’t it? I wept a little. Don’t think I didn’t notice the ironic placement of the play arrow, either. No, I didn’t plan that but I’m happy to take credit for it if you’re so inclined.
By the way, Cocoa is a drag queen’s name if ever there was one, isn’t it? To quote a statistic I made up just now, a full 51% of all drag queens are named “Cocoa”. If we’re to keep our girl looking fabulous, we need to update her look regularly.
Why “beglitter” our hammers and saws (and Interface Builders) when the things we build aren’t getting at least as good a treatment? Simply put, basic drag and drop is dreadfully plain. As in “give-her-a-credit-card-and-send-her-to-Macy’s-the-poor-thing” plain. Honey, it’s more worn out than Vanna White’s pumps, m’kay?
I digress. Let’s look at how Interface Builder does it.
Interface Builder’s Routine
With Interface Builder’s Library palette, as long as the item being dragged from the palette is still within the palette’s bounds, the item is represented with its icon image (the representation used in the collection view that contains all the available library items). Once it’s dragged out of the palette, however, it morphs into the size and shape it will be when the object is instantiated (well, in the case of a control dragged to a window, that is).
While IB’s approach is mostly eye candy, it solves two problems. First, the control can’t stay an icon because it gives you no visual context regarding what happens when you drop it into a subview. Second, the dragged item can’t immediately become the control (with no animation), because it’d be confusing when you’re suddenly “holding” a very-different-looking item than you “picked up”. It’d be like reaching out and grabbing a marshmallow peep only to find yourself holding a very real (and very pissed off) python. Bewildering.
Outside IB, however, there’s a far sweeter prize I’ve been eyeing personally. Consider a library of objects (a list of items with a small icon and a title when dragged). The library can be reordered and reorganized via drag and drop. What if you want to add promised file drags so the item creates a file when dragged out of the app and onto the desktop or a folder? Imagine the dragged item (still looking like a list item in the library view) morphs into an appropriate document icon when it leaves the window? Now we’re talking!
I’m sure there are other scenarios that could benefit from this kind of UI feedback. Let’s examine how this could be achieved with a little showmanship.
A Possible Approach
The problem with Cocoa’s dragging mechanism is that it takes a single, static image as the representation of the dragged item. There’s no supported way to modify this image mid-drag, nor is it possible to animate anything. But that’s not entirely necessary.
It’s important to note how much good Cocoa UI glitz is really mostly trickery (or can be). Overlay windows are commonly used for various effects (Interface Builder uses them in several places that I can see, or maybe it’s just one big one that handles all the effects). I started daydreaming about how a borderless, transparent window could achieve this.
The window would use basic view animation (not necessarily Core Animation) to morph between the two images. When the drag begins, this window would take the place of the dragging image, following the pointer around as the drag progresses. We just have to find a way to keep the existing drag mechanism but ditch the static image.
It turns out that’s not so difficult. You’re not allowed to pass nil as the image, so we just give it an empty one. It doesn’t really like a zero-sized image (it complains it can’t lock focus on the image), so we give it one with a size of 1×1. There’s one more snag – if you let the drag and drop mechanism perform a slide-back, you have to wait for it to finish before being notified the drag has ended. This would cause a slight delay before we’d know to perform our own slide-back routine with our prettier solution.
That leaves the animation of the “morphing” effect. If you look closely (i.e., obsessively play with Interface Builder until your eyes bleed), you’ll see that all it’s doing is animating the size and cross-fade. This is easy with Cocoa. Mostly. To do this requires two image views (for the inside and outside images) and some careful choreography.
Let’s take a moment to consider this. I expect to use this effect in all my new applications, but I’m lazy. I want one self-contained class. I want to start it up with one line of code and manipulate it with a bare minimum of interaction.
To this end, the first requirement is that it be a singleton. I don’t feel like fussing over it, maintaining it, disposing of it, etc. Since the user can effectively only perform one drag session at a time (for now), only one instance is ever needed per app.
The second requirement is that it should have, at most, the same number of “drag and drop interaction” methods the drag and drop protocol itself provides: start, update position, and finish.
The third is flexibility. In addition to its standard ability to remember its starting point, I wanted to allow it to designate any rectangle on the screen (as in “screen coordinates”) as its “inside” area. This gives us the ability to specify a part of a view, a whole view, a window, or even an entire screen as the inside area. In the future, I might even like to use different transition effects. That’s for another time but the design lends itself to that change fairly easily. I just ran out of time.
The only negative mark (for the cause of laziness) is that it uses QuartzCore (for customizing the animation). Because of this, you have to remember to link against the QuartzCore framework in your target.
I even managed to do all this without becoming a private API skank!
Note: I know what you’re thinking and no, I would make a terrible drag queen. I certainly have attitude but I have a feeling I’d be a train wreck in heels and mascara …
The source, license, and a code snippet are available on the JLNDragEffectManager page.
Update: See a real-world example in XTabulator 2 (drag-rows-to-desktop-to-make-a-file-of-those-rows).