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.