Users expect Windows8.1 apps to resize gracefully - either when they "snap" the app to a particular size, or when they change the orientation of their device. This turns out surprisingly difficult with a ScrollViewer. The details are below...
My app displays a collection of full-screen images. The user can swipe left and right to look through them. I do this with a ScrollViewer that contains a horizontal StackPanel.
If the user shrinks down my app, or turns their device on the side, I'd obviously like it to keep showing the same image:
First, here's the XAML code for the images. Note that I use "HorizontalSnapPointsType=MandatorySingle" to make sure it snaps exactly to an image, so the user can't scroll half-way between two images.
<ScrollViewer x:Name="scroll1" HorizontalScrollBarVisibility="Auto" ZoomMode="Disabled" VerticalScrollMode="Disabled" HorizontalSnapPointsType="MandatorySingle" HorizontalAlignment="Left" VerticalAlignment="Top" Background="Black"> <StackPanel x:Name="panel1" Orientation="Horizontal"> <Image Source="Assets/pic1.jpg" Stretch="Uniform"/> <Image Source="Assets/pic2.jpg" Stretch="Uniform"/> <Image Source="Assets/pic3.jpg" Stretch="Uniform"/> <Image Source="Assets/pic4.jpg" Stretch="Uniform"/> </StackPanel></ScrollViewer>
The first task is to resize the images in response to the app's size changing (including changes due to changing the orientation of the device). I'll do this in response to my page's SizeChanged event:
Sub OnSizeChanged(sender As Object, e As SizeChangedEventArgs) Handles Me.SizeChanged For Each i In panel1.Children.OfType(Of Image)() i.Width = e.NewSize.Width : i.Height = e.NewSize.Height NextEnd Sub
The second task is to make it so that, even when you resize, the ScrollViewer remains looking at the same image.
You'd think this would be easy: you could read the current value of scroll1.HorizontalOffset at the start of the SizeChanged, figure out which image the user has on-screen, resize each image, calculate what should be the new HorizontalOffset for the same image, and scroll to it.
But it's not easy. That's because the ScrollViewer has its own logic for when to scroll in response to a window-size-change event, and we have to battle against this logic...
So: you're going to have to resize the ScrollViewer manually, and you're going to have to wait until exactly the right moment to scroll to the correct new position, and you have to do it in the correct way. My solution is below.
Take care! I don't know the internals of how XAML works. This isn't an official Microsoft answer on how to tame ScrollViewer. I've just figured out this code through trial and error. Many of my earlier attempts seemed to work most of the time, but then failed sometimes. I've spent a full 30 minutes just dragging and resizing with the code below, and it hasn't failed yet.
Note: In the following code I've used "TaskCompletionSource" so I can await until after the ScrollViewer.SizeChanged event has fired. This is a common technique to turn event-based programming (hard!) into await-based programming (easier!)
Async Sub OnSizeChanged(sender As Object, e As SizeChangedEventArgs) Handles Me.SizeChanged ' I will remember which picture the user is currently viewing Dim index = Math.Round(If(e.PreviousSize.Width = 0, 0, scroll1.HorizontalOffset / e.PreviousSize.Width)) scroll1.HorizontalSnapPointsType = SnapPointsType.None ' *** If you don't disable snapping then it doesn't work
' Next, I resize all images so each one is again full-window-sized For Each i In panel1.Children.OfType(Of Image)() i.Width = e.NewSize.Width i.Height = e.NewSize.Height Next
' Now that the images have changed size, I will scroll to the same one that the user was previously viewing Dim tcs As New TaskCompletionSource(Of Object) Dim lambda As SizeChangedEventHandler = Sub(sender2, e2) tcs.TrySetResult(Nothing) AddHandler scroll1.SizeChanged, lambda scroll1.Width = e.NewSize.Width scroll1.Height = e.NewSize.Height Await tcs.Task ' *** if you don't await the ScrollViewer.SizeChanged event then it doesn't work RemoveHandler scroll1.SizeChanged, lambda scroll1.ChangeView(e.NewSize.Width * index, Nothing, Nothing, disableAnimation:=True) ' *** if "disableAnimation:=False" then it doesn't work scroll1.HorizontalSnapPointsType = SnapPointsType.MandatorySingle ' *** if this is done before scrolling then it doesn't workEnd Sub