windows-runtimewindows-phone-8.1cropscrollviewerfileopenpicker

Problems Scrolling and Zooming an Image in an WP8.1 App


I'm struggling with the ScrollViewer in my Windows Phone 8.1 (WinRT) app. Basically, what I'm trying to achieve is to retrieve an image using FileOpenPicker, crop the image to a fixed ratio (square) format while letting the user select the part of the image and the zoom level, and then use that image in my app. Perfect would be functionality as in the "People" app where you can add an image to a contact, but I would settle for less if I could somehow get it to work without the ScrollView acting too erratically.

Here is one of the variations I tried:

<ScrollViewer x:Name="SelectedImageScrollViewer"
    ZoomMode="Enabled"
    HorizontalScrollBarVisibility="Auto"
    VerticalScrollBarVisibility="Auto"
    Height="300"
    Width="300" >
    <Image x:Name="SelectedImage"
        Source="{Binding SelectedImage}"
        MinHeight="300"
        MinWidth="300" />
</ScrollViewer>

and in code-behind (in the constructor):

if (SelectedImage.ActualHeight > SelectedImage.ActualWidth) {
    SelectedImage.Width = SelectedImageScrollViewer.ViewportWidth;
}
else {
    SelectedImage.Height = SelectedImageScrollViewer.ViewportHeight;
}

Like I said, this isn't really working, and there are several problems with it:

  1. ScrollViews have this kind of "rubber band" overscroll functionality built in. While I can agree on platform uniformity, here it isn't helpful, and the mentioned "People" app doesn't have that either.
  2. When the user zooms beyond the MaxZoomLevel, zooming doesn't just stop, but the image drifts away and snaps back after releasing - not a good user experience.
  3. The image can be made smaller than the cropping frame. It should not be possible to reduce the zoom level to the point where the image is not filling the viewport.
  4. The ScrollView does not show the center of the image.

How can I fix those issues, and what would be the best approach for cropping and scaling the image? Would be nice if this was available as part of the SDK as it was in Silverlight (photo chooser).


Solution

  • The following solution provides a reasonably good user experience. Regarding the list of problems:

    1. Apparently cannot be solved using the basic ScrollViewer.
    2. Increasing MaxZoomFactor to something large enough makes it unlikely that the user sees the issue.
    3. After setting the image's smaller dimension to the cropping frame size, a MinZoomFactor of 1 ensures that the image always fills the frame.
    4. The ScrollView's offsets can be set in code behind.

    Setting IsScrollInertiaEnabled and IsZoomInertiaEnabled to false removes some of the erratic behavior in scrolling an zooming. Image width and height are set in SelectedImage_SizeChanged because the initial actual dimensions are not available in the constructor (before the page is rendered).

    <ScrollViewer Grid.Row="1"
                  x:Name="SelectedImageScrollViewer"
                  ZoomMode="Enabled"
                  IsScrollInertiaEnabled="False"
                  IsZoomInertiaEnabled="False"
                  HorizontalScrollBarVisibility="Auto"
                  VerticalScrollBarVisibility="Auto"
                  Height="300"
                  Width="300"
                  MinZoomFactor="1.0"
                  MaxZoomFactor="10.0">
        <Image x:Name="SelectedImage"
               Source="{Binding SelectedImage}"
               HorizontalAlignment="Center"
               SizeChanged="SelectedImage_SizeChanged" />
    </ScrollViewer>
    

    and

    private void SelectedImage_SizeChanged(object sender, SizeChangedEventArgs e) {
        // Here the proportions of the image are known and the initial size can be set
        // to fill the cropping frame depending on the orientation of the image.
    
        if (!_imageProportionsSet) {
            if (SelectedImage.ActualWidth != 0) {
    
                double actualHeight = SelectedImage.ActualHeight;
                double actualWidth = SelectedImage.ActualWidth;
                double viewPortWidth = SelectedImageScrollViewer.ViewportWidth;
                double viewPortHeight = SelectedImageScrollViewer.ViewportHeight;
    
                if (actualHeight > actualWidth) {
                    SelectedImage.Width = viewPortWidth;
                    double yOffset = (actualHeight - actualWidth) * viewPortWidth / actualHeight;
                    SelectedImageScrollViewer.ChangeView(0, yOffset, 1);
                }
                else {
                    SelectedImage.Height = viewPortHeight;
                    double xOffset = (actualWidth - actualHeight) * viewPortHeight / actualWidth;
                    SelectedImageScrollViewer.ChangeView(xOffset, 0, 1);
                }
    
                // Do this only once.
                _imageProportionsSet = true;
            }
        }
    }
    

    This is workable. If you find any issues with this please don't hesitate to comment or to provide an improved answer.