Xamarin Forms SwipeListView

 

The running appLet’s see how to build a fully customizable and cross platform Swipe-Enabled ListView in Xamarin Forms.
[Download] You can download from my GitHub the working project. This article aims to show I solved the problem in order to let you build your own Xamarin Form component.

The starting idea

A SwipeListView is a ListView where you can swipe left or right to perform some actions.

ListViewTo achive our goal we would replace the content of every item into the ListView with three different Box called MainContent, SwipeLeftContent and SwipeRightContent. As you can imagine only the MainContent will be draggable. SwipeLeftContent and SwipeRightContent will be displayed depending on MainContent position.

As a Xamarin Forms developer you would try to build all the SwipeListView inside a PCL, but you will find that Xamarin Forms has some limits due to his cross-platform vocation.
At some point, for example, we would like to perform some pan operations but Xamarin Forms lacks of any decent gestures recognition functionality:

			var panRecognizer = new PanGestureRecognizer();
			panRecognizer.PanUpdated += (object sender, PanUpdatedEventArgs panEvent) {
				//All we have is panEvent.TotalX and panEvent.TotalY;
			}

We assume we have to create a native ViewCell, which means we may deal with a custom renderer. If you don’t know what a custom renderer is look at there: https://developer.xamarin.com/guides/xamarin-forms/custom-renderer/

Writing a ViewCell custom renderer (or, in other words, deriving from ViewCellRenderer) is more than what we need for now. I think that we should try to write all our design in a XAML PCL and use a custom renderer to manage gestures only.
So our Main.xaml would look like:

		<ListView ItemsSource="{Binding ListItems}">
			...<ViewCell>
				<xam:SwipeItemView BoundItem="{Binding .}">
					<xam:SwipeItemView.MainContent>
					<!-- Something bindable !-->
					</xam:SwipeItemView.MainContent>
				</xam:SwipeItemView>
			</ViewCell>...
		</ListView>

And we can expect a xam:SwipeItemView defined this way:

<ContentView>
	<Grid>
		<ContentView IsVisible="{Binding IsRightContentVisible}">
			<!-- Content showed when swiping right !--></ContentView>
		<ContentView IsVisible="{Binding !IsRightContentVisible}">
			<!-- Content showed when swiping right !--></ContentView>
		<ContentView>
			<ContentView x:Name="innerContent"><!--Default content !--></ContentView>
		</ContentView>
	</Grid>
</ContentView>

This object represents each list item object. I added this reference to make our component flexible: I wanted a fully customizable mainContent, which means a property which can contain a view with his own bindings.

The BindingContext is inherited inside all the hierarchy. You can imagine we don’t need a BoundItem object. But for our pourposes we need some internal binding inside our custom view in order to know if the user is swiping left or right (and than to choose which button have to be displayed).

The property IsRightContentVisible is the reason why we need a BoundObject:

namespace XamSwipeList.SwipeList
{
	public partial class SwipeItemView : ContentView, INotifyPropertyChanged
	{
		public bool SwipeCompleted { get; set; }
		public View MainContent { get ... set ... }
		public object BoundItem { get ... set ... }
		public static readonly BindableProperty MainContentProperty;
		public static readonly BindableProperty BoundItemProperty;
		public SwipeItemView()
		{...
			mainContent.BindingContext = this; //The important part
		}
		static SwipeItemView()
		{
			MainContentProperty = BindableProperty.Create(..., propertyChanged: MainContentChanged);
			BoundItemProperty = BindableProperty.Create(..., propertyChanged: BoundItemChanged);
		}

		private static void BoundItemChanged(BindableObject bindable, object oldValue, object newValue)
		{...
			(bindable as SwipeItemView).innerContent.BindingContext = newValue; //The important part
		...}

		private static void MainContentChanged(BindableObject bindable, object oldValue, object newValue)
		{...
			(bindable as SwipeItemView).innerContent.Content = (View)newValue;
		...}
	}
}

Dealing with the custom renderer

At this point we are going to write our custom renderers to manage Gestures and pass the swipe as a Translation for our View. Of course we have to override DispatchTouchEvent, but we will two problem.touchAreas

  • Our swipe action must start and ends inside the ViewCell or our swipe will be lost, because we can’t conserve touch outside the bounding box the the item which started the swipe (which is the one we want to swipe also if the swipe is completed outside his bounding box).
  • We will notice a second problem when we try to delete an Item from our ObservableCollection. Xamarin Forms has a bug that doesn’t allow you to properly remove an item. If you SwipeRight an item (changing his TranslationX property) in order to delete this item, the TranslationX property of the removed item will be applied to the next element. I reported this bug many times ago but nothing appened. To hardfix this bug we have to reset the TranslationX property before deleting the item. This bug is detailed on Xamarin Forms BugZilla

Because of those two problems, we have to create a new View, with his own CustomRenderer which inherits from ListView. This SwipeListView  will catch all the touch events fired inside the List.

namespace XamSwipeList.SwipeList
{
	public class SwipeListView : ListView
	{
		private List&lt;SwipeItemView&gt; TouchedElements;
		public void PreventXamarinBug()
		{
			foreach(var elem in TouchedElements)
			{
				elem.PristineItem();
			}
		}

		public void AppendTouchedElement(SwipeItemView item)
		{
			TouchedElements.Add(item);
		}
	}
}

Now, let’s see how the custom renderer works:

[assembly: Xamarin.Forms.ExportRenderer(...)]
namespace XamSwipeList.Droid.CustomRenderer
{
	class SwipeItemRenderer : ViewRenderer
	{
		public override bool DispatchTouchEvent(MotionEvent touch)
		{
			if(TouchDispatcher.TouchingView == null &amp;&amp; touch.ActionMasked == MotionEventActions.Down)
			{
				TouchDispatcher.TouchingView = this.Element;
				TouchDispatcher.StartingBiasX = touch.GetX();
				TouchDispatcher.StartingBiasY = touch.GetY();
				TouchDispatcher.InitialTouch = DateTime.Now;
				return true;
			} //TouchDispatcher should be a static class containing the swiping view
			return base.DispatchTouchEvent(touch);
		}
	}
}

We manage only the “first touch” on the ListItem‘s custom renderer. All the other actions will be managed into the SwipeList custom renderer:

[assembly: Xamarin.Forms.ExportRenderer(...)]
namespace XamSwipeList.Droid.CustomRenderer
{
	class SwipeListRenderer : ListViewRenderer
	{
		public override bool DispatchTouchEvent(MotionEvent touch)
		{
			if (TouchDispatcher.TouchingView != null)
			{
				double currentQuota = ((touch.GetX() - TouchDispatcher.StartingBiasX) / (double)this.Width); //swiping percent
				float x = touch.GetX();
				float y = touch.GetY();
				switch (touch.ActionMasked)
				{
					case MotionEventActions.Up:
						touchedElement.CompleteTranslation(currentQuota);
						this.Element.AppendTouchedElement(touchedElement);
						TouchDispatcher.ResetAll();
						break;
					case MotionEventActions.Move:
						TouchDispatcher.TouchingView.PerformTranslation(currentQuota);
						break;
				}
			}
			return base.DispatchTouchEvent(touch);
		}
	}
}

And this is it. The implementation of PerformTranslation() is trivial.

The real code

In this post I haven’t used working code for brevity and to focus on the logic of the implementation.

You can find the working project on my github here.

Contributors

I wrote the Android part only beacause I’m not confident with Xamarin iOs. Any contribution to this project on Github will be welcomed.
If you have any idea abount how to solve this Xamarin Forms bug, please send me a note. We all know Xamarin is young and buggy… but we can help fix these little issues.