How to create swiping gesture list items for Windows Phone 7

A popular style for UI interaction these days is gesture based controls. Windows Phone 7 doesn’t inherently support many of these, but it’s possible to create a facsimile of some of the interactions. Here I will discuss how to handle making a horizontal swipe gesture for list items to provide interactions similar to the Clear application for iPhone (found here). This demo will be using the Windows Phone 7.1.1 SDK which you can download here.

First off, we’ll add a normal ListBox to our view. For the demo, I just bound it to a list of arbitrary objects in my code behind. To create a swiping view, we’ll add a horizontal ScrollViewer to the item template of our ListBox, as show below. I’ll get to the ScrollViewer_Loaded event handler in just a minute.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<ListBox x:Name="listbox" ItemsSource="{Binding Items}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<local:ScrollViewerOffsetMediator x:Name="Mediator" ScrollViewer="{Binding ElementName=ItemScroller}" HorizontalOffset="150" />
<ScrollViewer x:Name="ItemScroller" Tag="{Binding ElementName=Mediator}" Width="456" Style="{StaticResource ListItemScrollViewer}" Loaded="ScrollViewer_Loaded">
<StackPanel Orientation="Horizontal">
<Grid Width="150">
<Grid.Background>
<LinearGradientBrush>
<GradientStop Color="#FF0000" Offset="0" />
<GradientStop Color="#000000" Offset="1" />
</LinearGradientBrush>
</Grid.Background>
<TextBlock Text="X" FontSize="33" HorizontalAlignment="Center" />
</Grid>
<Grid Background="Black" Width="456" Margin="10">
<TextBlock FontSize="30" Text="{Binding}" />
</Grid>
<Grid Width="150">
<Grid.Background>
<LinearGradientBrush>
<GradientStop Color="#000000" Offset="0" />
<GradientStop Color="#00FF00" Offset="1" />
</LinearGradientBrush>
</Grid.Background>
<TextBlock Text="✓" FontSize="33" HorizontalAlignment="Center" />
</Grid>
</StackPanel>
</ScrollViewer>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>

Notice that in the ScrollViewer we have 3 Grids, one to handle the left panel, one to handle the main content, and one to handle the right content. If you only want to swipe in one direction or the other, you can remove panels as necessary.

The mechanism we will use to determine if someone’s intent is to actually use the swiping action will be to handle horizontal compression, otherwise known as overscroll. This is the state of a ScrollViewer when a user has scrolled to the end and is trying to scroll past the end. To detect compression in ScrollViewers, you need to override the style of the ScrollViewer with the following style

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<Style TargetType="ScrollViewer">
<Setter Property="VerticalScrollBarVisibility" Value="Disabled"/>
<Setter Property="HorizontalScrollBarVisibility" Value="Hidden"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="HorizontalOffset" Value="150" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ScrollViewer">
<Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="ScrollStates">
<VisualStateGroup.Transitions>
<VisualTransition GeneratedDuration="00:00:00.5"/>
</VisualStateGroup.Transitions>
<VisualState x:Name="Scrolling">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="VerticalScrollBar" Storyboard.TargetProperty="Opacity" To="0" Duration="0"/>
<DoubleAnimation Storyboard.TargetName="HorizontalScrollBar" Storyboard.TargetProperty="Opacity" To="0" Duration="0"/>
</Storyboard>
</VisualState>
<VisualState x:Name="NotScrolling">
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="VerticalCompression">
<VisualState x:Name="NoVerticalCompression"/>
<VisualState x:Name="CompressionTop"/>
<VisualState x:Name="CompressionBottom"/>
</VisualStateGroup>
<VisualStateGroup x:Name="HorizontalCompression">
<VisualState x:Name="NoHorizontalCompression"/>
<VisualState x:Name="CompressionLeft"/>
<VisualState x:Name="CompressionRight"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid Margin="{TemplateBinding Padding}">
<ScrollContentPresenter x:Name="ScrollContentPresenter" Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}"/>
<ScrollBar x:Name="VerticalScrollBar" IsHitTestVisible="False" Height="Auto" Width="5" HorizontalAlignment="Right" VerticalAlignment="Stretch" Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}" IsTabStop="False" Maximum="{TemplateBinding ScrollableHeight}" Minimum="0" Value="{TemplateBinding VerticalOffset}" Orientation="Vertical" ViewportSize="{TemplateBinding ViewportHeight}" />
<ScrollBar x:Name="HorizontalScrollBar" IsHitTestVisible="False" Width="Auto" Height="5" HorizontalAlignment="Stretch" VerticalAlignment="Bottom" Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}" IsTabStop="False" Maximum="{TemplateBinding ScrollableWidth}" Minimum="0" Value="{TemplateBinding HorizontalOffset}" Orientation="Horizontal" ViewportSize="{TemplateBinding ViewportWidth}" />
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

I made mine with a key of ListItemScrollViewer, and as you can see from the first code snippet, the ScrollViewer inside the ListItems are all set with this style.

Now onto the guts of the functionality. First we need to implement the ScrollViewer_Loaded that we declared in the first snippet. That will look as follows

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private void ScrollViewer_Loaded(object sender, RoutedEventArgs e)
{
var sv = sender as ScrollViewer;
if (sv != null)
{
sv.ScrollToHorizontalOffset(150);
// Visual States are always on the first child of the control template
FrameworkElement element = VisualTreeHelper.GetChild(sv, 0) as FrameworkElement;
if (element != null)
{
VisualStateGroup group = FindVisualState(element, "ScrollStates");
if (group != null)
{
group.CurrentStateChanging += new EventHandler(group_CurrentStateChanging);
}
VisualStateGroup hgroup = FindVisualState(element, "HorizontalCompression");
if (hgroup != null)
{
hgroup.CurrentStateChanging += new EventHandler(hgroup_CurrentStateChanging);
}
}
}
}

Here is the helper method to find the visual state

1
2
3
4
5
6
7
8
9
10
11
12
private VisualStateGroup FindVisualState(FrameworkElement element, string name)
{
if (element == null)
return null;
IList groups = VisualStateManager.GetVisualStateGroups(element);
foreach (VisualStateGroup group in groups)
if (group.Name == name)
return group;
return null;
}

We need to invoke the ScrollToHorizontalOffset method to scroll past the left panel and start out in the middle. That can also be accomplished by using the HorizontalOffset property on ScrollViewerOffsetMediator, and there’s a link at the bottom of the post to the source for that. The ScrollStates visual state will tell us whether the ScrollViewer is scrolling or not, which we’ll use to determine if the intent is complete. The HorizontalCompression visual state will tell us when we’ve overscrolled.

Now that we have that, here’s the code for handling both the visual state changes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
void hgroup_CurrentStateChanging(object sender, VisualStateChangedEventArgs e)
{
if (e.NewState.Name == "CompressionLeft")
{
deleteItem = true;
}
else if (e.NewState.Name == "CompressionRight")
{
markAsComplete = true;
}
else if (e.NewState.Name == "NoHorizontalCompression" && !scrolling)
{
deleteItem = false;
markAsComplete = false;
}
}
void group_CurrentStateChanging(object sender, VisualStateChangedEventArgs e)
{
if (scrolling && deleteItem)
{
var item = (e.Control as ScrollViewer).DataContext.ToString();
Items.Remove(item);
deleteItem = false;
}
else if (scrolling)
{
if (markAsComplete)
{
//Add a strikethrough to show that it's complete
var sv = e.Control as ScrollViewer;
var sp = sv.Content as StackPanel;
var centralPanel = sp.Children[1] as Grid;
if (centralPanel.Children.Count == 1)
{
var textBlock = centralPanel.Children[0] as TextBlock;
var line = new Line()
{
Stroke = new SolidColorBrush(Colors.White),
StrokeThickness = 2,
X1 = 0,
Y1 = textBlock.ActualHeight / 2,
X2 = textBlock.ActualWidth,
Y2 = textBlock.ActualHeight / 2
};
centralPanel.Children.Add(line);
}
}
//Move the ScrollViewer back to the starting position
(e.Control as ScrollViewer).ScrollToHorizontalOffset(150);
}
scrolling = e.NewState.Name == "Scrolling";
}

In this example, I’m using the left swipe to mark something as complete and the right swipe to delete it. It’s pretty straight forward, we just detect the new state of our ScrollView and set markers appropriately, then when the scroll has finished, we act on whatever happened. Do pay special attention that you don’t forget the line at the bottom of the handler for the ScrollStates visual state changing, since you’ll want to know whether you’re currently scrolling or not.

Since we are only using the horizontal scroll bar on the list items, this list style can also be used in conjunction with a “pull to refresh” style panel, like the one found here: http://blogs.msdn.com/b/jasongin/archive/2011/04/13/pull-down-to-refresh-a-wp7-listbox-or-scrollviewer.aspx

I also used the ScrollViewerOffsetMediator from this blog post to be able to animate the ScrollViewer so it has a smooth snap back as opposed to just using ScrollToHorizontalOffset(). The only change is that specific implementation only works vertically, so you’ll want to change everything to work horizontally as well. That version is included in the demo source code.

If you would like to peruse the source code or download it, you can download it or clone the repository from https://github.com/dstafford/SwipeableListDemo.

 

原貼:http://blogs.burnsidedigital.com/2012/08/how-to-create-swiping-gesture-list-items-for-windows-phone-7/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章