Overhauling our collision engine
Alex Reardon
Posted on December 5, 2019
How we created a more delightful drag and drop experience
Drag and drop is a very human interaction: pick something up and move it somewhere else. react-beautiful-dnd
(rbd
) is an open source library we created at Atlassian that attempts to reflect the physicality of this human interaction in order to provide a more relatable experience.
rbd
contains logic for calculating what should move out of way of the dragging item, and what droppable area the dragging item is currently over (the collision engine). Over time, some limitations of the collision engine have emerged. This blog will share our journey rebuilding the collision engine in order to make drag and drop more delightful.
Problem 1: Knowing when to move
The original collision engine used the centre position of the dragging item to determine what other items should move out of the way. An item would move out of the way when the centre position of the dragging item goes over the start or end edge of another item.
"a dragging items impact is based on its centre of gravity — regardless of where a user grabs an item from" - Rethinking drag and drop
✅ When items are roughly the same size then the interaction is all good
✅ Using the centre position of the dragging item holds up when dragging large items next to small items
❌ Using the centre position of the dragging item does not work well when dragging small items next to large items
In this example, when moving a small item past a big item, the big item would satisfy the condition to move up (the dragging centre is after the top edge), and once moved up, it would satisfy the condition to move down (the dragging centre is before the bottom edge). This would cause big item not being dragged to flicker up and down.
We overcame this weakness in the old collision engine by checking different edges depending on what direction the user was heading in. For example, when moving down in a list we only check to see if the centre of the dragging item has gone past the top edge of another item.
This solution (hack) was not without drawbacks. When dragging large items next to small items, things quickly turn into a dumpster fire if the user changed directions quickly.
When dragging large items next to small ones, small direction changes could result in quite a different displacement of items.
So yeah, things were not great.
Solution: Flipping the logic
The shortcomings of the collision engine have lingered on in the rbd
repo for quite some time. There was an open question as to how to maintain physicality, while also overcoming problems with using the centre position of the dragging item.
About a month ago we got a fantastic suggestion from @caspersmith: flip the logic. Rather than using the centre position of the dragging item for determining displacement, look at the centre position of the other items. When an edge of the dragging item goes over the centre position of another item, then move that item out of the way. It turned out that this idea holds up extremely well mathematically and still feels great.
By using the edges of the dragging item we can never end up in a situation where we hit the same item before and after it is displaced, regardless of the items size. The new approach also means we no longer need to lean on the user direction (hack).
Problem 2: What is being dragged over?
When dragging something around we need to communicate to the user what droppable area the dragging item is currently over. Previously we did this by exclusively using the centre position of the dragging item. This is done in an attempt to make the dragging item feel more physical: by using the centre position we are using the dragging item's centre of mass to control its location.
✅ Things feel fantastic when lists are roughly the same size
❌ Significant problems arise when a user is trying to drag a large item into a small list
It can be jarring (and sometimes impossible) for users to drag a big item into a small list. They need to line up the centre position of the big item over this list - which can require overshooting the list with your cursor to get the centre position in the right spot.
Solution: Understanding intention
We spent many hours whiteboarding various potential strategies to improve the experience for dragging large items into small areas. I think we lost a few years of our lives during this process.
The new approach
- Find drop candidates 🙋♀️(droppable areas that could be the active area)
- Choose the drop candidate furthest away from where the drag start started
Finding drop candidates 🙋♀️
Previously there was only one way to determine what the droppable area would be: the dragging item centre position. We have now added more ways for droppable areas to be the active droppable area.
A droppable area can be a drop candidate when
- Centre hit (existing): the centre position of the dragging item is over the droppable area
-
Cross axis hit (new):
(this logic only comes into play when dragging large items into smaller sized areas)
- There is some overlap of the dragging item and the droppable area; and
- Only one edge of the dragging item is over the list on the cross axis of the droppable area; and
- The edge must have gone past the cross axis centre of the droppable area (
end edge > cross axis centre line
,start edge < cross axis centre line
)
-
Totally covered on cross axis (new):
- The dragging item is totally over a droppable area on the cross axis
- There is some overlap on the main axis
Choose candidate that is furthest away
When there are multiple drop candidates (multiple drop areas that we think the user is over) we choose the one that is furthest away from where the drag started. There will only be multiple drop candidates when dragging large items over small droppable areas, so this logic doesn't come into play for the standard use case of dragging items over areas that are of a similar size on the cross axis.
Determining what drop candidate is furthest away is not as straightforward as measuring the distance between the centre points of the droppable areas (which is what we started with). Doing that gives preference to giant droppable areas as their centres are often far away. Rather, we measure the distance between where the dragging item started, the point at which the dragging item hits the cross axis of the droppable area.
What does the new approach achieve?
When droppable areas are roughly the same size on the cross axis, things will operate in the same way they did before: the centre position will be used to control what droppable area an item is over (which feels great)
Strictly using the dragging items centre position was our ideal situation for all interactions. But, as we have seen, using the centre doesn't work well for moving big items into small lists. We looked for a solution would best respect the users intention. We landed on the view that when a draggable item hits a new droppable area it is likely that the user is trying to move into that area - so that is what we will try to give preference to. That is why we give preference to areas that are further away from where the user started. Doing this also requires the least amount of movement to move something into a new area.
We decided this new strategy for large items was a necessary divergence from the physicality of rbd
in order to provide an experience that feels intuitive and delightful for users, which in the end, is what we are pursuing physicality to achieve.
Thanks
We hope you have enjoyed this blog, and that you and the people who use your software enjoy the overhauled rbd
collision engine. The new collision is available in the 12.2.0
release of rbd
.
Thanks to all the Atlassian's who helped work on this and to Daniel Del Core, Michael Dougall and Ee Venn Soh who helped put this blog together.
Cheers
Posted on December 5, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.