A better Slider for Windows Phone 7
The Slider control as shipped with the Windows Phone Developer Tools has some problems, both due to how it was templated, and also because it was originally designed for the desktop, where mice are very good at hitting small targets.
The visible Thumb was removed, and the invisible Thumb, even when you know where it is, is quite difficult to touch and drag. This means that most often, you end up pressing one of the RepeatButtons that will slowly increment or decrement the Slider’s value. That may be what you intended, but the Slider on the phone shouldn’t be used for precision adjustments. That will just frustrate the user.
It is quite difficult to manipulate a horizontal Slider to get the maximum value, and if you somehow do manage to do that, you will find that you have moved the invisible Thumb off of the Slider, where it will be clipped, and you have to press the decrement button a while to get it back.
I have re-templated the Slider, and added a small tick mark in the thumb for some sort of a visual cue, as when the Slider is on its minimum value, the darkened track is sometimes hard to see. It is easy to remove the tick if your application provides enough context to make it clear that there is a Slider and how to use it. I made the touchable area much wider—in fact, the Thumb has been expanded to cover the entire Slider area, so that the user can drag anywhere on the Slider without having to hit a small, invisible target.
Note that the Slider should be used judiciously. Controls that are manipulated by dragging should not be placed in situations where they are on a parent that uses drags of the same orientation. So, for example, you should not put a horizontal Slider inside of a Panorama or Pivot control, and you should not put a vertical Slider inside of a ListBox (I have a hard time imagining why you’d want to do the latter.)
Here is the new Slider template. I started with the template that I found in
%ProgramFiles%\Microsoft SDKs\Windows Phone\v7.0\Design\System.Windows.xaml
although I could have used Blend, added a Slider, and then right-clicked on it and selected “Edit Template…” and then “Edit a Copy…”. I put my template in App.xaml:
Code Snippet
- <Application
- x:Class="SliderApp.App"
- xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
- xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
- xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
- xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
- xmlns:local="clr-namespace:SliderApp">
- <!--Application Resources-->
- <Application.Resources>
- <ControlTemplate x:Key="PhoneSimpleRepeatButton" TargetType="RepeatButton">
- <Rectangle/>
- </ControlTemplate>
- <ControlTemplate x:Key="PhoneSimpleThumbHorizontal" TargetType="Thumb">
- <Border Background="Transparent" Margin="-480,-18">
- <Rectangle Width="2" Height="6" Fill="{StaticResource PhoneForegroundBrush}"/>
- </Border>
- </ControlTemplate>
- <ControlTemplate x:Key="PhoneSimpleThumbVertical" TargetType="Thumb">
- <Border Background="Transparent" Margin="-6,-800">
- <Rectangle Width="6" Height="2" Margin="24,0,0,0" Fill="{StaticResource PhoneForegroundBrush}"/>
- </Border>
- </ControlTemplate>
- <Style x:Key="sliderStyle" TargetType="local:PhoneSlider">
- <Setter Property="BorderThickness" Value="0"/>
- <Setter Property="BorderBrush" Value="Transparent"/>
- <Setter Property="Maximum" Value="10"/>
- <Setter Property="Minimum" Value="0"/>
- <Setter Property="Value" Value="0"/>
- <Setter Property="Margin" Value="{StaticResource PhoneHorizontalMargin}"/>
- <Setter Property="Background" Value="{StaticResource PhoneContrastBackgroundBrush}"/>
- <Setter Property="Foreground" Value="{StaticResource PhoneAccentBrush}"/>
- <Setter Property="Template">
- <Setter.Value>
- <ControlTemplate TargetType="local:PhoneSlider">
- <Grid Background="Transparent">
- <VisualStateManager.VisualStateGroups>
- <VisualStateGroup x:Name="CommonStates">
- <VisualState x:Name="Normal"/>
- <VisualState x:Name="MouseOver"/>
- <VisualState x:Name="Disabled">
- <Storyboard>
- <DoubleAnimation Duration="0" Storyboard.TargetName="HorizontalTrack" Storyboard.TargetProperty="Opacity" To="0.1" />
- <DoubleAnimation Duration="0" Storyboard.TargetName="VerticalTrack" Storyboard.TargetProperty="Opacity" To="0.1" />
- <ObjectAnimationUsingKeyFrames Storyboard.TargetName="HorizontalFill" Storyboard.TargetProperty="Fill">
- <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneDisabledBrush}" />
- </ObjectAnimationUsingKeyFrames>
- <ObjectAnimationUsingKeyFrames Storyboard.TargetName="VerticalFill" Storyboard.TargetProperty="Fill">
- <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneDisabledBrush}" />
- </ObjectAnimationUsingKeyFrames>
- </Storyboard>
- </VisualState>
- </VisualStateGroup>
- </VisualStateManager.VisualStateGroups>
- <Grid x:Name="HorizontalTemplate">
- <Grid.ColumnDefinitions>
- <ColumnDefinition Width="*"/>
- <ColumnDefinition Width="0"/>
- <ColumnDefinition Width="auto"/>
- </Grid.ColumnDefinitions>
- <Rectangle x:Name="HorizontalTrack" IsHitTestVisible="False" Fill="{TemplateBinding Background}" Opacity="0.2" Grid.ColumnSpan="3" Height="12" Margin="0,22,0,50"/>
- <Rectangle x:Name="HorizontalFill" IsHitTestVisible="False" Fill="{TemplateBinding Foreground}" Grid.Column="0" Height="12" Margin="0,22,0,50"/>
- <RepeatButton x:Name="HorizontalTrackLargeChangeDecreaseRepeatButton" IsTabStop="False" Template="{StaticResource PhoneSimpleRepeatButton}" Grid.Column="0" />
- <RepeatButton x:Name="HorizontalTrackLargeChangeIncreaseRepeatButton" IsTabStop="False" Template="{StaticResource PhoneSimpleRepeatButton}" Grid.Column="2" />
- <Thumb x:Name="HorizontalThumb" Width="1" Margin="-1,0,0,0" Grid.Column="1" Template="{StaticResource PhoneSimpleThumbHorizontal}" RenderTransformOrigin="0.5,0.5" CacheMode="BitmapCache"/>
- </Grid>
- <Grid x:Name="VerticalTemplate">
- <Grid.RowDefinitions>
- <RowDefinition Height="*"/>
- <RowDefinition Height="0"/>
- <RowDefinition Height="Auto"/>
- </Grid.RowDefinitions>
- <Rectangle x:Name="VerticalTrack" IsHitTestVisible="False" Fill="{TemplateBinding Background}" Opacity="0.2" Grid.RowSpan="3" Width="12" Margin="24,0"/>
- <Rectangle x:Name="VerticalFill" IsHitTestVisible="False" Fill="{TemplateBinding Foreground}" Grid.Row="2" Width="12" Margin="24,0"/>
- <RepeatButton x:Name="VerticalTrackLargeChangeDecreaseRepeatButton" IsTabStop="False" Template="{StaticResource PhoneSimpleRepeatButton}" Grid.Row="0"/>
- <RepeatButton x:Name="VerticalTrackLargeChangeIncreaseRepeatButton" IsTabStop="False" Template="{StaticResource PhoneSimpleRepeatButton}" Grid.Row="2"/>
- <Thumb x:Name="VerticalThumb" Height="1" Margin="0,-1,0,0" Grid.Row="1" Template="{StaticResource PhoneSimpleThumbVertical}" RenderTransformOrigin="0.5,0.5" CacheMode="BitmapCache"/>
- </Grid>
- </Grid>
- </ControlTemplate>
- </Setter.Value>
- </Setter>
- </Style>
- </Application.Resources>
- <Application.ApplicationLifetimeObjects>
- <!--Required object that handles lifetime events for the application-->
- <shell:PhoneApplicationService
- Launching="Application_Launching" Closing="Application_Closing"
- Activated="Application_Activated" Deactivated="Application_Deactivated"/>
- </Application.ApplicationLifetimeObjects>
- </Application>
This could also be placed in your page’s resources or in generic.xaml. You will note that in lines 28 and 39, I have am not using TargetType=”Slider”, but I have used the name of my subclassed type. This is because I had to use some code-behind to handle the Clip area. Since I am using negative margins to make the Thumbs cover the entire Slider, and I had to make the Thumbs large enough to cover the entire Slider, no matter where it is positioned, I need code to handle the clipping, otherwise the Thumbs would extend way beyond the Slider’s bounds, and cover up other controls. This is particularly a problem for vertical Sliders.
Fortunately, the code-behind isn’t much:
Code Snippet
- using System.Windows;
- using System.Windows.Controls;
- using System.Windows.Media;
- namespace SliderApp
- {
- public class PhoneSlider : Slider
- {
- public PhoneSlider()
- {
- SizeChanged += new SizeChangedEventHandler(PhoneSlider_SizeChanged);
- }
- void PhoneSlider_SizeChanged(object sender, SizeChangedEventArgs e)
- {
- if (e.NewSize.Width > 0 && e.NewSize.Height > 0)
- {
- Rect clipRect = new Rect(0, 0, e.NewSize.Width, e.NewSize.Height);
- if (Orientation == Orientation.Horizontal)
- {
- clipRect.X -= 12;
- clipRect.Width += 24;
- object margin = Resources["PhoneHorizontalMargin"];
- if (margin != null)
- {
- Margin = (Thickness)margin;
- }
- }
- else
- {
- clipRect.Y -= 12;
- clipRect.Height += 24;
- object margin = Resources["PhoneVerticalMargin"];
- if (margin != null)
- {
- Margin = (Thickness)margin;
- }
- }
- this.Clip = new RectangleGeometry() { Rect = clipRect };
- }
- }
- }
- }
It just has to set the margins and clip correctly, but the values depend on the value of Slider.Orientation. If your application was going to use Sliders with only one orientation, you could simplify the code-behind and the template, but you’d still need the code-behind for the clip.
You can download a sample project that contains both horizontal and vertical Sliders, with the default and modified templates. Here’s what it looks like:
You really have to try this on an actual Phone to appreciate how hard it can be to use the default Slider. The emulator makes it easy—we have all been trained to use a relatively high-precision mouse to hit small targets on the screen, and the invisible Thumb is quite easy to hit when using the emulator and mouse.
Comments
Anonymous
October 04, 2010
Great post, thank you for putting it up :)Anonymous
October 05, 2010
Great job. It make a huge difference and makes the control actually usable now ;) Can you please clarify if this code is okay to use in commercial apps?Anonymous
October 05, 2010
@SmartyP - You are welcome to use any of the code on my blog however you would like.Anonymous
October 22, 2010
The comment has been removedAnonymous
November 04, 2010
Hi Dave, Thanks a lot for your nice job. As you've permitted, I'm going to use your re-templated slider in my app. But I have a problem: your slider doesn't response properly in "Panorama" applications. As you may guess, trying to move the slider in those apps changes the current Panorama item. I tried to put that "e.Handled = true" in all of the mouse events of PhoneSlider, but nothing happened. Do you have any solution? Thanks again ;)Anonymous
November 06, 2010
@Siavash - There is a problem with Panorama. It listens to all manipulations, whether they have been handled or not. But the UIX recommendation is to not use controls that require horizontal manipulation (like Slider) on Panoramas or Pivots, which use horizontal manipulation for navigation.Anonymous
November 07, 2010
Thanks for your info, I really appreciate it ;) with best wishes Siavash www.smortazavi.netAnonymous
December 08, 2010
Hey Dave, First off, thanks much for the helpful post! I'm noticing a strange issue with the slider in my app. Both the default and modified sliders are very choppy and seem to constantly lose the drag gesture so they require repeated "drags" to move the value from one side to the other. However, if I tombstone and return to the same page, the issue seems to be gone and they function like in your sample. The only page in the backstack when this occurs contains a panorama control. I experimented with removing that panorama control and that also seems to resolve the slide issue. Just for the clarity, the slider is NOT in a panoram but in a separate page navigated to FROM a panorama. Very strange... Any possible thoughts as to what might be happening here? -JamesAnonymous
December 08, 2010
Just been doing some more testing and traced my issue above to the use of the GestureListener from the PhoneToolkit on some of the visual elements inside the panorama control. When I remove those the slider on the subsequent page is smooth. Now... why is the Gesture Listener causing this issue... hmmmAnonymous
December 19, 2010
James, I've run into the exact same problem. Did you manage to find out why this bug occurs? It's a very weird one, considering how tombstoning seems to fix the issue...Anonymous
December 29, 2010
Hey Paul, Unfortunately no... I was just toying with using a slider and, so far, haven't actually needed it so I left this alone. In my case the tombstoning issue was a factor because the pages in my backstack (when I got to the page with the slider) were the ones using the Gesture Listener. Right after tombstoning on the slider page, those pages in the backstack weren't yet re-instantiated so the slider worked as expected but once I navigated backwards causing those pages with the gesture listener to be instantiated again I was back to the same slider issue. Sorry that I don't have anything to offer on the root issue at the moment... -JamesAnonymous
January 26, 2011
I also have the exact same problem as James and Paul. Has anyone found the source of this issue? BTW: My control is on a simple static page (not panorama, pivot). Thanks.Anonymous
February 10, 2011
Same problem here as Joe, James, and Paul: jumpy behavior, and thumb drags don't complete correctly. Not using GestureListener in my code, and not using Panorama, but I AM using a Bing map control below the slider. I'm guessing that the map control is using GestureListener. Anybody know a work around? MouseCapture?Anonymous
March 11, 2011
Et si on veut que cette valeur change dynamiquement sans utiliser le curseur (à partir d'un code inséré ) je veux dire une variable qui change à chaque fois et change l'état du slider?Anonymous
March 11, 2011
Et si on veut que le slider change sans l'utilisation du curseur ; ç-à-d on utilise une variable que le slider représente çà valeur d'une façon dynamique ?Anonymous
August 10, 2011
Thank you Dave! Great slider. It teached me how to make a usercontrol from another control! But can you tell one thing? I have horizontal slider as one of ScrollViewer child. If first touch position while I am scrolling was on a slider, Viewer is not scrolled and value of a slider is changing. Can I add something like checking of dragging X and Y and react only X > Y for horizontal slider and Y > X for vertical? Otherwise to "pass action" (?) to a parent (for example ScrollViewer).Anonymous
September 18, 2011
Hi Dave, Thanks for the post. It really helped in using a better slider in my application. I have a requirement of placing two sliders in a same row grid. But I am facing a small problem when i use two sliders in a same row of my "slider grid". Only the second slider moves, even when i try to move the first slider? It very strange i am unable to understand how the even of 1st slider goes to second one :( Can you please help me? Thanks once again!Anonymous
January 19, 2012
Just for info: Regarding issue of Jaybo, Joe, James, and Paul. I had the same thing and solved it with advice from MarkChamberlain post 1/6/2011 1:19 AM from forums.create.msdn.com/.../436721.aspx which is "Not use the GestureService and use TouchPanel directly instead (GS is just a wrapper around TP)". How to use TouchPanel you may find here www.nickharris.net/.../using-touchpanel-for-gestures-in-windows-phone-7