HSBogdan
HSBogdan

Reputation: 220

How to enable both scrolling and zooming using pinch in WPF?

I am struggling with making both touch events and manipulation work properly in a WPF project. I have a ScrollViewer which contains a picture and I would like to scroll both horizontally and vertically using a swipe gestures. Additionally, I would like to zoom in/out in the center of the pinch gesture. The code below achieves what I wish, but it has the following problems:

I enabled the IsManipulationEnabled and I implemented the code for zoom in/out functionality. However, I was not able to combine it with the scrolling functionality (by setting the PanningMode in the ScrollViewer only). Therefore, I created a custom control which inherits from Image control and I overwritten the OnTouchDown and OnTouchUp event handlers. Basically, what I am doing in these overwritten handlers is counting the number of touches on the screen and enabling/disabling manipulation. I also tried setting the PanningMode for the ScrollViewer, but it did not do the trick.

Below is the XAML:

<Grid>
        <ScrollViewer
            x:Name="ScrollViewerParent"
            HorizontalScrollBarVisibility="Auto"
            VerticalScrollBarVisibility="Auto"
            PanningMode="Both">
            <local:CustomImage 
                x:Name="MainImage"
                Source="{Binding Source={x:Static local:Constants.ImagePath}}"
                IsManipulationEnabled="True"
                ManipulationStarting="MainImage_ManipulationStarting"
                ManipulationDelta="MainImage_ManipulationDelta">
            </local:CustomImage>
        </ScrollViewer>
    </Grid>

Here is the code-behind:

public partial class MainWindow : Window
{
        private void MainImage_ManipulationStarting(object sender, ManipulationStartingEventArgs e)
        {
            e.ManipulationContainer = ScrollViewerParent;
            e.Handled = true;
        }

        private void MainImage_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
        {
            var matrix = MainImage.LayoutTransform.Value;

            Point? centerOfPinch = (e.ManipulationContainer as FrameworkElement)?.TranslatePoint(e.ManipulationOrigin, ScrollViewerParent);

            if (centerOfPinch == null)
            {
                return;
            }

            var deltaManipulation = e.DeltaManipulation;
            matrix.ScaleAt(deltaManipulation.Scale.X, deltaManipulation.Scale.Y, centerOfPinch.Value.X, centerOfPinch.Value.Y);
            MainImage.LayoutTransform = new MatrixTransform(matrix);

            Point? originOfManipulation = (e.ManipulationContainer as FrameworkElement)?.TranslatePoint(e.ManipulationOrigin, MainImage);

            double scrollViewerOffsetX = ScrollViewerParent.HorizontalOffset;
            double scrollViewerOffsetY = ScrollViewerParent.VerticalOffset;

            double pointMovedOnXOffset = originOfManipulation.Value.X - originOfManipulation.Value.X * deltaManipulation.Scale.X;
            double pointMovedOnYOffset = originOfManipulation.Value.Y - originOfManipulation.Value.Y * deltaManipulation.Scale.Y;

            double multiplicatorX = ScrollViewerParent.ExtentWidth / MainImage.ActualWidth;
            double multiplicatorY = ScrollViewerParent.ExtentHeight / MainImage.ActualHeight;

            ScrollViewerParent.ScrollToHorizontalOffset(scrollViewerOffsetX - pointMovedOnXOffset * multiplicatorX);
            ScrollViewerParent.ScrollToVerticalOffset(scrollViewerOffsetY - pointMovedOnYOffset * multiplicatorY);

            e.Handled = true;
        }
}

The XAML for the custom control:

<Style TargetType="{x:Type local:CustomImage}" />

Here is where I override the OnTouchDown and OnTouchUp event handlers:

 public class CustomImage : Image
    {

        private volatile int nrOfTouchPoints;
        private volatile bool isManipulationReset;
        private object mutex = new object();

        static CustomImage()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomImage), new FrameworkPropertyMetadata(typeof(CustomImage)));
        }

        protected override void OnTouchDown(TouchEventArgs e)
        {
            lock (mutex)
            {
                nrOfTouchPoints++;
                if (nrOfTouchPoints >= 2)
                {
                    IsManipulationEnabled = true;
                    isManipulationReset = false;
                }
            }
            base.OnTouchDown(e);
        }

        protected override void OnTouchUp(TouchEventArgs e)
        {
            lock (mutex)
            {
                if (!isManipulationReset)
                {
                    IsManipulationEnabled = false;
                    isManipulationReset = true;
                    nrOfTouchPoints = 0;
                }
            }
            base.OnTouchUp(e);
        }
    }

What I expect from this code is the following:

Upvotes: 3

Views: 3257

Answers (1)

HSBogdan
HSBogdan

Reputation: 220

Fortunately, I managed to find the perfect solution. Therefore, I am going to post the answer in the case that someone is working on a similar problem and needs some help.

What I did:

  1. Got rid of the custom control as it was not necessary;
  2. Create a field which counts the number of the touch points;
  3. Implemented the TouchDown event handler, which increases the number of touch points by 1 (this method is called each time there is a touch down gesture on the device);
  4. Implemented the TouchUp event handler, which decreases the number of touch points by 1 (this method is called each time there is a touch up gesture on the device);
  5. In the Image_ManipulationDelta event handler, I check the number of touch points:
    • if the number of touch points < 2, then the translation value is added to the current offset of the scrollbars, thus achieving scrolling;
    • otherwise, the center of the pinch is calculated and a scale gesture is applied.

Here is the full XAML:

 <Grid
        x:Name="GridParent">
        <ScrollViewer
            x:Name="ScrollViewerParent"
            HorizontalScrollBarVisibility="Auto"
            VerticalScrollBarVisibility="Auto"
            PanningMode="Both">
            <Image
                x:Name="MainImage"
                Source="{Binding Source={x:Static local:Constants.ImagePath}}"
                IsManipulationEnabled="True"
                TouchDown="MainImage_TouchDown"
                TouchUp="MainImage_TouchUp"
                ManipulationDelta="Image_ManipulationDelta"
                ManipulationStarting="Image_ManipulationStarting"/>
        </ScrollViewer>
    </Grid>

Here is the entire code discussed above:

    public partial class MainWindow : Window
    {

        private volatile int nrOfTouchPoints;
        private object mutex = new object();

        public MainWindow()
        {
            InitializeComponent();
            DataContext = this;
        }

        private void Image_ManipulationStarting(object sender, ManipulationStartingEventArgs e)
        {
            e.ManipulationContainer = ScrollViewerParent;
            e.Handled = true;
        }

        private void Image_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
        {
            int nrOfPoints = 0;

            lock (mutex)
            {
                nrOfPoints = nrOfTouchPoints;
            }

            if (nrOfPoints >= 2)
            {
                DataLogger.LogActionDescription($"Executed {nameof(Image_ManipulationDelta)}");

                var matrix = MainImage.LayoutTransform.Value;

                Point? centerOfPinch = (e.ManipulationContainer as FrameworkElement)?.TranslatePoint(e.ManipulationOrigin, ScrollViewerParent);

                if (centerOfPinch == null)
                {
                    return;
                }

                var deltaManipulation = e.DeltaManipulation;
                matrix.ScaleAt(deltaManipulation.Scale.X, deltaManipulation.Scale.Y, centerOfPinch.Value.X, centerOfPinch.Value.Y);
                MainImage.LayoutTransform = new MatrixTransform(matrix);

                Point? originOfManipulation = (e.ManipulationContainer as FrameworkElement)?.TranslatePoint(e.ManipulationOrigin, MainImage);

                double scrollViewerOffsetX = ScrollViewerParent.HorizontalOffset;
                double scrollViewerOffsetY = ScrollViewerParent.VerticalOffset;

                double pointMovedOnXOffset = originOfManipulation.Value.X - originOfManipulation.Value.X * deltaManipulation.Scale.X;
                double pointMovedOnYOffset = originOfManipulation.Value.Y - originOfManipulation.Value.Y * deltaManipulation.Scale.Y;

                double multiplicatorX = ScrollViewerParent.ExtentWidth / MainImage.ActualWidth;
                double multiplicatorY = ScrollViewerParent.ExtentHeight / MainImage.ActualHeight;

                ScrollViewerParent.ScrollToHorizontalOffset(scrollViewerOffsetX - pointMovedOnXOffset * multiplicatorX);
                ScrollViewerParent.ScrollToVerticalOffset(scrollViewerOffsetY - pointMovedOnYOffset * multiplicatorY);

                e.Handled = true;
            }
            else
            {
                ScrollViewerParent.ScrollToHorizontalOffset(ScrollViewerParent.HorizontalOffset - e.DeltaManipulation.Translation.X);
                ScrollViewerParent.ScrollToVerticalOffset(ScrollViewerParent.VerticalOffset - e.DeltaManipulation.Translation.Y);
            }
        }

        private void MainImage_TouchDown(object sender, TouchEventArgs e)
        {
            lock (mutex)
            {
                nrOfTouchPoints++;
            }
        }

        private void MainImage_TouchUp(object sender, TouchEventArgs e)
        {
            lock (mutex)
            {
                nrOfTouchPoints--;
            }
        }
    }
}

Upvotes: 4

Related Questions