I came across a forum question which was asking whether someone could provide the starting point for a ListView control which acted in a similar way to the built-in mail app on Windows 10 such that you can use a swipe-left/swipe-right gesture to flag/delete mail on Windows 10.
I wanted to answer the question and I gave it quite a bit of thought in terms of how it might be done and whether it was better to try and;
- Come up with a custom ListView which made this possible.
- Come up with a custom ListViewItem which made this possible.
- Re-template a ListViewItem to make this possible.
and I did explore those routes a little but I didn’t have a lot of success in a short period of time and it was mainly because of the relationship between a ListView and the ListViewItem or because of the slightly hard-wired nature of a ListViewItem’s template.
What I mean here is that if you try and re-template a ListViewItem you can quickly bounce up against it expecting to have the ListViewItemPresenter as the first item in its template which proved a bit limiting for me here.
Rather than take any of these routes, I knocked up an experimental control that I called a SwipeContentControl which I could use like this;
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <local:SwipeContentControl VerticalAlignment="Center" HorizontalContentAlignment="Stretch" Swiped="OnSwiped"> <local:SwipeContentControl.LeftContent> <Grid Background="LimeGreen"> <TextBlock Text="Flag" Foreground="White" Margin="0,0,4,0" HorizontalAlignment="Right" VerticalAlignment="Center" /> </Grid> </local:SwipeContentControl.LeftContent> <local:SwipeContentControl.RightContent> <Grid Background="Red"> <TextBlock Text="Delete" Margin="4,0,0,0" Foreground="White" HorizontalAlignment="Left" VerticalAlignment="Center" /> </Grid> </local:SwipeContentControl.RightContent> <Grid Background="AliceBlue"> <TextBlock FontSize="18" Margin="20" HorizontalAlignment="Center" Text="This is my content" /> </Grid> </local:SwipeContentControl> </Grid>
so the control has this notion of 3 pieces of content;
- the main content to display
- the content to display if the main content is swiped left
- the content to display if the main content is swiped right
and it fires an event called Swiped containing the direction of the swipe if it detects that the user swipes the content significantly to the left or right. Here’s a quick screen capture;
I’m not sure that I did a fully fledged job on the SwipeContentControl here but I wrote a control that derived from Control and added what seemed like the bare bones to it in order to get it built;
using System; using Windows.Foundation; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Input; using Windows.UI.Xaml.Markup; using Windows.UI.Xaml.Media; // these names line up with visual states in generic.xaml so // rename at your peril. enum SwipeDirection { Left, Right, Default }; class SwipeEventArgs : EventArgs { public SwipeDirection Direction { get; set; } } [ContentProperty(Name = "Content")] class SwipeContentControl : Control { public event EventHandler<SwipeEventArgs> Swiped; public static DependencyProperty LeftContentProperty = DependencyProperty.Register( "LeftContent", typeof(object), typeof(SwipeContentControl), null); public static DependencyProperty RightContentProperty = DependencyProperty.Register( "RightContent", typeof(object), typeof(SwipeContentControl), null); public static DependencyProperty ContentProperty = DependencyProperty.Register( "Content", typeof(object), typeof(SwipeContentControl), null); public static DependencyProperty LeftContentTemplateProperty = DependencyProperty.Register( "LeftContentTemplate", typeof(DataTemplate), typeof(SwipeContentControl), null); public static DependencyProperty RightContentTemplateProperty = DependencyProperty.Register( "RightContentTemplate", typeof(DataTemplate), typeof(SwipeContentControl), null); public static DependencyProperty ContentTemplateProperty = DependencyProperty.Register( "ContentTemplate", typeof(DataTemplate), typeof(SwipeContentControl), null); public object LeftContent { get { return (base.GetValue(LeftContentProperty)); } set { base.SetValue(LeftContentProperty, value); } } public DataTemplate LeftContentTemplate { get { return ((DataTemplate)base.GetValue(LeftContentTemplateProperty)); } set { base.SetValue(LeftContentTemplateProperty, value); } } public DataTemplate RightContentTemplate { get { return ((DataTemplate)base.GetValue(RightContentTemplateProperty)); } set { base.SetValue(RightContentTemplateProperty, value); } } public DataTemplate ContentTemplate { get { return ((DataTemplate)base.GetValue(ContentTemplateProperty)); } set { base.SetValue(ContentTemplateProperty, value); } } public object RightContent { get { return (base.GetValue(RightContentProperty)); } set { base.SetValue(RightContentProperty, value); } } public object Content { get { return (base.GetValue(ContentProperty)); } set { base.SetValue(ContentProperty, value); } } public SwipeContentControl() { this.DefaultStyleKey = typeof(SwipeContentControl); this.ManipulationMode = ManipulationModes.TranslateX; this.ManipulationDelta += OnManipulationDelta; this.ManipulationCompleted += OnManipulatedCompleted; } void OnManipulatedCompleted(object sender, ManipulationCompletedRoutedEventArgs e) { this.TranslateContent(0); if (IsManipulationSignificant(e.Cumulative.Translation.X)) { SwipeEventArgs args = new SwipeEventArgs() { Direction = e.Cumulative.Translation.X < 0 ? SwipeDirection.Left : SwipeDirection.Right }; this.Swiped?.Invoke(this, args); } } bool IsManipulationSignificant(double x) { bool significant = Math.Abs(x) > (SIGNIFICANT_TRANSLATE_FACTOR * this.contentElement.ActualWidth); return (significant); } void OnManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e) { this.SetVisualStateForManipulation(e.Cumulative.Translation); this.TranslateContent(e.Cumulative.Translation.X); } void SetVisualStateForManipulation(Point p) { var direction = this.DirectionOfManipulation(p); VisualStateManager.GoToState(this, direction.ToString(), true); } void TranslateContent(double x) { if (this.contentElement != null) { // This may well break if there's already a transform on this element 😦 TranslateTransform transform = (this.contentElement.RenderTransform as TranslateTransform); if (transform == null) { transform = new TranslateTransform(); this.contentElement.RenderTransform = transform; } transform.X = x; } } SwipeDirection DirectionOfManipulation(Point p) { SwipeDirection d = SwipeDirection.Default; if (p.X != 0) { d = p.X < 0 ? SwipeDirection.Left : SwipeDirection.Right; } return (d); } protected override void OnApplyTemplate() { base.OnApplyTemplate(); this.contentElement = this.GetTemplateChild("contentPresenter") as FrameworkElement; } static readonly double SIGNIFICANT_TRANSLATE_FACTOR = 0.20d; FrameworkElement contentElement; }
and I tried to come up with a generic.xaml template for it in my project’s Themes folder;
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:App9"> <Style TargetType="local:SwipeContentControl"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="local:SwipeContentControl"> <Grid Background="{TemplateBinding Background}"> <VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="leftRightStates"> <VisualState x:Name="Left"> <VisualState.Setters> <Setter Target="leftContentPresenter.(UIElement.Visibility)" Value="Visible" /> </VisualState.Setters> </VisualState> <VisualState x:Name="Right"> <VisualState.Setters> <Setter Target="rightContentPresenter.(UIElement.Visibility)" Value="Visible" /> </VisualState.Setters> </VisualState> <VisualState x:Name="Default" /> </VisualStateGroup> </VisualStateManager.VisualStateGroups> <ContentPresenter Visibility="Collapsed" x:Name="leftContentPresenter" Content="{TemplateBinding LeftContent}" /> <ContentPresenter Visibility="Collapsed" x:Name="rightContentPresenter" Content="{TemplateBinding RightContent}" /> <ContentPresenter x:Name="contentPresenter" Foreground="{TemplateBinding Foreground}" Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" /> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
and it seemed to work out reasonably well and then I could make use of it in a ListView;
<ListView> <ListView.ItemContainerStyle> <Style TargetType="ListViewItem"> <Setter Property="HorizontalContentAlignment" Value="Stretch" /> </Style> </ListView.ItemContainerStyle> <ListView.Items> <x:String>Mail One</x:String> <x:String>Mail Two</x:String> <x:String>Mail Three</x:String> </ListView.Items> <ListView.ItemTemplate> <DataTemplate> <local:SwipeContentControl HorizontalContentAlignment="Stretch" Swiped="OnSwiped"> <local:SwipeContentControl.LeftContent> <Grid Background="LimeGreen"> <TextBlock Text="Flag" Foreground="White" Margin="0,0,4,0" HorizontalAlignment="Right" VerticalAlignment="Center" /> </Grid> </local:SwipeContentControl.LeftContent> <local:SwipeContentControl.RightContent> <Grid Background="Red"> <TextBlock Text="Delete" Margin="4,0,0,0" Foreground="White" HorizontalAlignment="Left" VerticalAlignment="Center" /> </Grid> </local:SwipeContentControl.RightContent> <Grid Background="AliceBlue"> <TextBlock FontSize="18" Margin="20" HorizontalAlignment="Center" Text="{Binding}" /> </Grid> </local:SwipeContentControl> </DataTemplate> </ListView.ItemTemplate> </ListView>
and it’s a long way from perfect but it’s perhaps a starting point for this type of thing that someone might take and develop into something better.
The code for the entire thing is here for download if you want to take it on from here and, remember, it was a 20-minutes of effort attempt rather than a full-on attempt to build a real control.
Hi Mike, I ran into needing this just the other day. There’s actually a fairly nice control called SwipeListView (https://github.com/FrayxRulez/SwipeListView) – I’ve got a PR open on it at the moment to add commands to the swipe left/right actions, but other than that id did everything I needed.
Thanks Mike – that looks nicer than my little effort 🙂
Mike.