← About Jeff

A small but important UX improvement for ScrollViewer on the Windows Phone

December 5, 2010

Here’s a quick UX improvement to the scroll viewer that adds a visual indicator for the user showing them briefly the scroll bars.

Interestingly, the actual phone operating system does have this concept of a hint, so I think it’s a smart addition any developer should consider while polishing their app before submission to the marketplace.

If you aren’t sure what the goal is here, go to the “settings” for the Windows Phone either in the emulator, on the real phone, or hey even the iPhone’s scroll bars all do this, so you have options.

The user experience problem with the ScrollViewer for WP7

There is no visual cue to the existence of the scroll bars in Silverlight applications for Windows Phone by default. I don’t know why this wasn’t done for the official control styles in the platform, but being so easy to fix, I think folks should pick this up.

Now that I’m finally doing some fun app coding on the weekends, I intend to share more small tidbits like this and I hope you all appreciate this sort of fit and finish attention.

I have also been downloading and experiencing many of the apps on the Windows Phone Marketplace, with varied success. I’ve had more than one occasion where I was briefly confused, not knowing there was more information just a pan away, since the scroll viewer is effectively invisible until you touch the surface.

Triggers to the rescue

Triggers were something WPF developers will remember; back in the day, before we had the magical and designable visual states system. Now if you’re saying “but dude Silverlight doesn’t support triggers,” you’re mostly right. There is, however, support for the Loaded event. So in XAML you can define a storyboard that will run when the control’s loaded event fires, super easy.

Exactly what we want! I want to animate that same opacity from 1 to 0 to match the normal state at startup.

The BetterScrollViewer Template

This is a control template, not a style. So remember to set the Template property to the static resource for it to work.

<ControlTemplate TargetType="ScrollViewer" x:Key="BetterScrollViewer">
    <Grid Margin="{TemplateBinding Padding}" Background="{TemplateBinding Background}">
        <Grid.Triggers>
            <EventTrigger RoutedEvent="Grid.Loaded">
                <BeginStoryboard>
                    <Storyboard>
                        <DoubleAnimation Duration="00:00:01.5" From="1" To="0" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="VerticalScrollBar"/>
                        <DoubleAnimation Duration="00:00:01.5" From="1" To="0" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="HorizontalScrollBar"/>
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger>
        </Grid.Triggers>
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="ScrollStates">
                <VisualStateGroup.Transitions>
                    <VisualTransition GeneratedDuration="00:00:00.5"/>
                </VisualStateGroup.Transitions>
                <VisualState x:Name="Scrolling">
                    <Storyboard>
                        <DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="VerticalScrollBar"/>
                        <DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="HorizontalScrollBar"/>
                    </Storyboard>
                </VisualState>
                <VisualState x:Name="NotScrolling"/>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
        <ScrollContentPresenter x:Name="ScrollContentPresenter" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}"/>
        <ScrollBar x:Name="VerticalScrollBar" HorizontalAlignment="Right" Height="Auto" IsHitTestVisible="False" IsTabStop="False" Maximum="{TemplateBinding ScrollableHeight}" Minimum="0" Opacity="0" Orientation="Vertical" Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}" Value="{TemplateBinding VerticalOffset}" ViewportSize="{TemplateBinding ViewportHeight}" VerticalAlignment="Stretch" Width="5"/>
        <ScrollBar x:Name="HorizontalScrollBar" HorizontalAlignment="Stretch" Height="5" IsHitTestVisible="False" IsTabStop="False" Maximum="{TemplateBinding ScrollableWidth}" Minimum="0" Opacity="0" Orientation="Horizontal" Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}" Value="{TemplateBinding HorizontalOffset}" ViewportSize="{TemplateBinding ViewportWidth}" VerticalAlignment="Bottom" Width="Auto"/>
    </Grid>
</ControlTemplate>

Using the template

I place this template in my App.xaml then just set the template every time I use a scroll viewer.

<ScrollViewer
    Template="{StaticResource BetterScrollViewer}">
</ScrollViewer>

Now if you want this in ListBox, you need to (unfortunately, it’s a pain) re-template ListBox. So any ListBox needs to set the template to something like BetterListBox. This template is exactly the same as the standard one except of course the static resource set in there.

<ControlTemplate TargetType="ListBox" x:Key="BetterListBox">
	<ScrollViewer x:Name="ScrollViewer" 
	Template="{StaticResource BetterScrollViewer}"
	BorderBrush="{TemplateBinding BorderBrush}" 
	BorderThickness="{TemplateBinding BorderThickness}" 
	Background="{TemplateBinding Background}" 
	Foreground="{TemplateBinding Foreground}" 
	Padding="{TemplateBinding Padding}">
		<ItemsPresenter/>
	</ScrollViewer>
</ControlTemplate>

And to use this with just regular scroll bars, well, you’re on your own, but can use the same trigger mechanism to add it easily I bet.

Notes + I removed the border!

This scroll viewer template I modified also reduces the visual count by 1. I didn’t like the idea of having the scroll viewer wrapped in a border and supporting those properties, since I don’t use them. If you want to use those properties though you’ll want to build your own template from the standard to keep those template bindings in place.

If you’re using one of the controls like panorama or pivot, that may do some pre-loading offscreen of items, it is likely that the Loaded event will fire and trigger this animation when the user  cannot actually see it.

So don’t expect this behavior to help in those scenarios. To really polish those you’ll need to jump into code.

And I picked the time of 1.5 seconds for that initial scroll appearance. If I find out from any UX folks I will try and learn what the official value is that’s used on the phone, but in the meantime, 1.5 looks about right to me.

Hope this helps.