A couple of weeks ago I blogged about a todo list application which uses gestures to achieve its basic functions, a left swipe deletes an item, while a right-swipe marks it as complete. In this blog post I am adding re-ordering which is initiated via a tap-and-hold gesture and performed via a drag.
As mentioned previously, this application is heavily inspired by the iPhone application Clear, who I give full credit for coming up with such an innovative user-interface design. This blog post is for fun an education, you are strictly prohibited from using this as the basis of a 'Clear' clone for WP7.
The video below shows the application so far, with the new re-order functionality:
Initiating a Drag
In the previous blog post we saw how the Toolkit GestureListener can be used to convert the low-level manipulation events into high level gestures. In order to initiate an item drag we could use the Hold event that the GestureListener exposes. The Hold event is fired when the user taps and holds the location of their finger for a few seconds. When I published my previous blogpost one of my readers Simon (Darkside) Jackson kindly pointed out that some of the gesture events are now supported by FrameworkElement directly, Hold is one such event. For this reason I'll use FrameworkElement.Hold, the equivalent event on GestureListener should be considered deprecated.
The easiest way to allow the user to 'pick up' and drag the item is to clone it using a WriteableBitmap, hiding the original. This technique allows us to place the item at a higher Z-index than the list which it comes from, so that it will always be top-most as it is moved up and down the list.
We'll add the element that is used to render the dragged item to the XAML:
The markup contains an image, and a couple of Rectangle elements, each of which is filled with a subtle opacity gradient and offset in order to give the impression of a drop shadow. The Source of the image is set in the Hold event handler as shown:
The code above also hides the real object and offsets our 'fake' vertically so that it occupies the same position as the original. The list is also faded slightly to give a visual indication that the dragged item is now above the rest of the list:
Dragging the item
In order to allow the user to support dragging of the item we also need to handle ManipulationDelta on the Border element, as shown below:
So why am I handling these events on the Border rather than re-using the DragDelta / GestureComplete on the GestureListener? There are a couple of reasons:
The GestureListener 'drag' has a tolerance (as discussed in the previous blog post). The user has to move their finger beyond a certain distance before a drag is started. In the current context we want a drag to occur as soon as the Hold event fires.
When handling the element 'drag' we need to suppress the event by setting e.Handled=true, otherwise a drag will bubble up to the ScrollViewer that hosts our elements causing it to scroll. I found (through trial and error) that the GestureListener.DragDelta.Handled property doesn't appear to have any effect.
The handler for the drag event is pretty simple, moving the copy of our item by the required distance:
Offsetting the items underneath
The ShuffleItemsOnDrag method is where the fun starts, we'll get to that shortly. First we'll take a look at a simple utility method that is used to determine the index that the item being dragged would occupy if it were dropped at the present location. This is achieved by a simple measurement:
The above code needs to take the current scroll location into consideration, which is why the ScrollViewer property above uses Linq-to-VisualTree to find the ScrollViewer that the ItemsControl generates to hosts our elements.
ShuffleItemsOnDrag is where the fun begins, we want to create an effect where the dragged item 'pushes' the other items out of the way as it hovers over them, giving the impression that the list is re-ordering as we drag.
The method below iterates over all of the items in the list to determine whether they need to be offset. An item needs to be offset if it is between the current dragged item index and the items original location.
The OffsetItem method performs the actual offset by animating the Y position of each item. The target location is stored in the elements Tag property so that we don't repeatedly fire the same animation on an element.
Completing the drag
When the user stops dragging the item, the ManipulationCompleted event is fired. Here we perform a number of tasks:
Fade the list back to full opacity
Animate the dragged item so that it 'snaps' into location
When the above is complete, we need to re-order the underlying collection of model items, then re-populate the ObservableCollection exposed to the view. This causes all the items to be re-rendered, removing all of the TranslateTransforms that have been applied.
Finally, remove the image which is our copy of the dragged item.
This sounds like a lot of work, but our Animate utility method makes it quite simple:
Scrolling the list
The current implementation only allows the user to drag the item within the bounds of the current screen. What if the list is larger than the screen and the users want to drag right from the bottom to the top?
A common solution to this problem is to auto-scroll the list if the item is dragged near to the top. The following method is invoked periodically by a timer to see whether the item has been dragged within the top or bottom 'scroll zones'. The velocity of the scroll is proportional to just how far within these zones the item has been dragged. Scrolling is simply a matter of setting the scroll location on the ScrollViewer we located earlier:
You can see the scroll zones illustrated below:
And finally, we are all done! With our todo application you can use flick and drag gestures to mark items as complete or delete them, and now use hold and drag to re-order. I think it's about time I made it so that you can add or edit items. We'll get to that next time!