6
votes

I have implemented both pan and pinch individually, and it works fine. I'm now trying to use pinch and pan together and I'm seeing some issues. Here's my code:

XAML:

<AbsoluteLayout x:Name="PinchZoomContainer">
  <controls:NavBar x:Name="NavBar" ShowPrevNext="true" ShowMenu="false" IsModal="true" />
  <controls:PanContainer  x:Name="PinchToZoomContainer">
    <Image x:Name="ImageMain" />
  </controls:PanContainer>
</AbsoluteLayout>

Pinch/Pan Gesture Add's:

var panGesture = new PanGestureRecognizer();
panGesture.PanUpdated += OnPanUpdated;
GestureRecognizers.Add(panGesture);

var pinchGesture = new PinchGestureRecognizer();
pinchGesture.PinchUpdated += OnPinchUpdated;
GestureRecognizers.Add(pinchGesture);

Pan Method:

void OnPanUpdated(object sender, PanUpdatedEventArgs e)
{
    switch (e.StatusType)
    {
        case GestureStatus.Started:
            startX = e.TotalX;
            startY = e.TotalY;
            Content.AnchorX = 0;
            Content.AnchorY = 0;

            break;
        case GestureStatus.Running:
            // Translate and ensure we don't pan beyond the wrapped user interface element bounds.
            Content.TranslationX = Math.Max(Math.Min(0, x + e.TotalX), -Math.Abs(Content.Width - App.ScreenWidth));
            Content.TranslationY = Math.Max(Math.Min(0, y + e.TotalY), -Math.Abs(Content.Height - App.ScreenHeight));
            break;

        case GestureStatus.Completed:
            // Store the translation applied during the pan
            x = Content.TranslationX;
            y = Content.TranslationY;
            break;
    }
}

Pinch Method:

void OnPinchUpdated(object sender, PinchGestureUpdatedEventArgs e)
{
    if (e.Status == GestureStatus.Started)
    {
        // Store the current scale factor applied to the wrapped user interface element,
        // and zero the components for the center point of the translate transform.
        startScale = Content.Scale;
        //ImageMain.AnchorX = 0;
        //ImageMain.AnchorY = 0;
    }
    if (e.Status == GestureStatus.Running)
    {
        // Calculate the scale factor to be applied.
        currentScale += (e.Scale - 1) * startScale;
        currentScale = Math.Max(1, currentScale);
        currentScale = Math.Min(currentScale, 2.5);
        // The ScaleOrigin is in relative coordinates to the wrapped user interface element,
        // so get the X pixel coordinate.
        double renderedX = Content.X + xOffset;
        double deltaX = renderedX / Width;
        double deltaWidth = Width / (Content.Width * startScale);
        double originX = (e.ScaleOrigin.X - deltaX) * deltaWidth;

        // The ScaleOrigin is in relative coordinates to the wrapped user interface element,
        // so get the Y pixel coordinate.
        double renderedY = Content.Y + yOffset;
        double deltaY = renderedY / Height;
        double deltaHeight = Height / (Content.Height * startScale);
        double originY = (e.ScaleOrigin.Y - deltaY) * deltaHeight;

        // Calculate the transformed element pixel coordinates.
        double targetX = xOffset - (originX * Content.Width) * (currentScale - startScale);
        double targetY = yOffset - (originY * Content.Height) * (currentScale - startScale);

        // Apply translation based on the change in origin.
        Content.TranslationX = targetX.Clamp(-Content.Width * (currentScale - 1), 0);
        Content.TranslationY = targetY.Clamp(-Content.Height * (currentScale - 1), 0);

        // Apply scale factor
        Content.Scale = currentScale;
    }
    if (e.Status == GestureStatus.Completed)
    {
        // Store the translation delta's of the wrapped user interface element.
        xOffset = Content.TranslationX;
        yOffset = Content.TranslationY;
    }
}

If I turn off either gesture and only use the other then the functionality works perfectly. The issue arises when I add the pan AND pinch gestures. What seems to be happening is this:

1) The pan actually seems to be working as expected 2) When you pan on the image initially, let's say, move the image to Y-center and X-center, and then you try to zoom, the image gets set back to it's initial state. Then, when you pan, it moves you back to where you were before you tried to zoom (which is why I say the pan is working fine).

From what I'm understanding from my debugging is that when you zoom it's not taking into consideration the position you are currently at. So when you pan first, and then zoom, it doesn't zoom on the position you're at but the beginning point of the image. Then when you try to pan from there, the pan method still remembers where you were, and it moves you back to where you were before you tried to zoom.

Hoping some insight on this. Obviously, there's an issue with my pinch method. I just think (obviously can't figure out) I need to add logic into it that takes into consideration where you're currently at.

4
Did you end up getting this to work?rooby
Yes I did, ended up doing something different than this to get it to work. I'll post my methods below.jdmdevdotnet
Thank you so much. I will try this out.rooby
upvote if it works for you :) also happy to help you if you have any issues.jdmdevdotnet

4 Answers

12
votes

The main reason might be that everybody seems to copy and use this code (coming from the dev.xamarin site) with its very convoluted and very unnecessary co-ordinate calculations :-). Unnecessary because we could simply ask the view to do the heavy lifting for us, using the AnchorX and AnchorY properties which serve exactly this purpose.

We can have a double tap operation to zoom in and to revert to the original scale. Note that because Xamarin fails to provide coordinate values with its Tap events (a very unwise decision, actually), we can only zoom from the center now:

private void OnTapped(object sender, EventArgs e) 
{
    if (Scale > MIN_SCALE) 
    {
        this.ScaleTo(MIN_SCALE, 250, Easing.CubicInOut);
        this.TranslateTo(0, 0, 250, Easing.CubicInOut);
    }
    else 
    {
        AnchorX = AnchorY = 0.5;
        this.ScaleTo(MAX_SCALE, 250, Easing.CubicInOut);
    }
}

The pinch handler is similarly simple, no need to calculate any translations at all. All we have to do is to set the anchors to the pinch starting point and the framework will do the rest, the scaling will occur around this point. Note that we even have an extra feature here, springy bounce-back on overshoot at both ends of the zoom scale.

private void OnPinchUpdated(object sender, PinchGestureUpdatedEventArgs e) 
{
    switch (e.Status) 
    {
        case GestureStatus.Started:
            StartScale = Scale;
            AnchorX = e.ScaleOrigin.X;
            AnchorY = e.ScaleOrigin.Y;
            break;

        case GestureStatus.Running:
            double current = Scale + (e.Scale - 1) * StartScale;
            Scale = Clamp(current, MIN_SCALE * (1 - OVERSHOOT), MAX_SCALE * (1 + OVERSHOOT));
            break;

        case GestureStatus.Completed:
            if (Scale > MAX_SCALE)
                this.ScaleTo(MAX_SCALE, 250, Easing.SpringOut);
            else if (Scale < MIN_SCALE)
                this.ScaleTo(MIN_SCALE, 250, Easing.SpringOut);
            break;
    }
}

And the panning handler, even simpler. On start, we calculate the starting point from the anchor and during panning, we keep changing the anchor. This anchor being relative to the view area, we can easily clamp it between 0 and 1 and this stops the panning at the extremes without any translation calculation at all.

private void OnPanUpdated(object sender, PanUpdatedEventArgs e) 
{
    switch (e.StatusType) 
    {
        case GestureStatus.Started:
            StartX = (1 - AnchorX) * Width;
            StartY = (1 - AnchorY) * Height;
            break;

        case GestureStatus.Running:
            AnchorX = Clamp(1 - (StartX + e.TotalX) / Width, 0, 1);
            AnchorY = Clamp(1 - (StartY + e.TotalY) / Height, 0, 1);
            break;
    }
}

The constants and variables used are just these:

private const double MIN_SCALE = 1;
private const double MAX_SCALE = 8;
private const double OVERSHOOT = 0.15;
private double StartX, StartY;
private double StartScale;
3
votes

Went with a completely different method of handling this. For anyone who is having issues, this works 100%.

OnPanUpdated

void OnPanUpdated(object sender, PanUpdatedEventArgs e)
{
    var s = (ContentView)sender;

    // do not allow pan if the image is in its intial size
    if (currentScale == 1)
        return;

    switch (e.StatusType)
    {
        case GestureStatus.Running:
            double xTrans = xOffset + e.TotalX, yTrans = yOffset + e.TotalY;
            // do not allow verical scorlling unless the image size is bigger than the screen
            s.Content.TranslateTo(xTrans, yTrans, 0, Easing.Linear);
            break;

        case GestureStatus.Completed:
            // Store the translation applied during the pan
            xOffset = s.Content.TranslationX;
            yOffset = s.Content.TranslationY;

            // center the image if the width of the image is smaller than the screen width
            if (originalWidth * currentScale < ScreenWidth && ScreenWidth > ScreenHeight)
                xOffset = (ScreenWidth - originalWidth * currentScale) / 2 - s.Content.X;
            else
                xOffset = System.Math.Max(System.Math.Min(0, xOffset), -System.Math.Abs(originalWidth * currentScale - ScreenWidth));

            // center the image if the height of the image is smaller than the screen height
            if (originalHeight * currentScale < ScreenHeight && ScreenHeight > ScreenWidth)
                yOffset = (ScreenHeight - originalHeight * currentScale) / 2 - s.Content.Y;
            else
                //yOffset = System.Math.Max(System.Math.Min((originalHeight - (ScreenHeight)) / 2, yOffset), -System.Math.Abs((originalHeight * currentScale - ScreenHeight - (originalHeight - ScreenHeight) / 2)) + (NavBar.Height + App.StatusBarHeight));
                yOffset = System.Math.Max(System.Math.Min((originalHeight - (ScreenHeight)) / 2, yOffset), -System.Math.Abs((originalHeight * currentScale - ScreenHeight - (originalHeight - ScreenHeight) / 2)));

            // bounce the image back to inside the bounds
            s.Content.TranslateTo(xOffset, yOffset, 500, Easing.BounceOut);
            break;
    }
}

OnPinchUpdated

void OnPinchUpdated(object sender, PinchGestureUpdatedEventArgs e)
{
    var s = (ContentView)sender;

    if (e.Status == GestureStatus.Started)
    {
        // Store the current scale factor applied to the wrapped user interface element,
        // and zero the components for the center point of the translate transform.
        startScale = s.Content.Scale;

        s.Content.AnchorX = 0;
        s.Content.AnchorY = 0;
    }
    if (e.Status == GestureStatus.Running)
    {

        // Calculate the scale factor to be applied.
        currentScale += (e.Scale - 1) * startScale;
        currentScale = System.Math.Max(1, currentScale);
        currentScale = System.Math.Min(currentScale, 5);

        //scaleLabel.Text = "Scale: " + currentScale.ToString ();

        // The ScaleOrigin is in relative coordinates to the wrapped user interface element,
        // so get the X pixel coordinate.
        double renderedX = s.Content.X + xOffset;
        double deltaX = renderedX / App.ScreenWidth;
        double deltaWidth = App.ScreenWidth / (s.Content.Width * startScale);
        double originX = (e.ScaleOrigin.X - deltaX) * deltaWidth;

        // The ScaleOrigin is in relative coordinates to the wrapped user interface element,
        // so get the Y pixel coordinate.
        double renderedY = s.Content.Y + yOffset;

        double deltaY = renderedY / App.ScreenHeight;
        double deltaHeight = App.ScreenHeight / (s.Content.Height * startScale);
        double originY = (e.ScaleOrigin.Y - deltaY) * deltaHeight;

        // Calculate the transformed element pixel coordinates.
        double targetX = xOffset - (originX * s.Content.Width) * (currentScale - startScale);
        double targetY = yOffset - (originY * s.Content.Height) * (currentScale - startScale);

        // Apply translation based on the change in origin.
        var transX = targetX.Clamp(-s.Content.Width * (currentScale - 1), 0);
        var transY = targetY.Clamp(-s.Content.Height * (currentScale - 1), 0);


        s.Content.TranslateTo(transX, transY, 0, Easing.Linear);
        // Apply scale factor.
        s.Content.Scale = currentScale;
    }
    if (e.Status == GestureStatus.Completed)
    {
        // Store the translation applied during the pan
        xOffset = s.Content.TranslationX;
        yOffset = s.Content.TranslationY;

        // center the image if the width of the image is smaller than the screen width
        if (originalWidth * currentScale < ScreenWidth && ScreenWidth > ScreenHeight)
            xOffset = (ScreenWidth - originalWidth * currentScale) / 2 - s.Content.X;
        else
            xOffset = System.Math.Max(System.Math.Min(0, xOffset), -System.Math.Abs(originalWidth * currentScale - ScreenWidth));

        // center the image if the height of the image is smaller than the screen height
        if (originalHeight * currentScale < ScreenHeight && ScreenHeight > ScreenWidth)
            yOffset = (ScreenHeight - originalHeight * currentScale) / 2 - s.Content.Y;
        else
            yOffset = System.Math.Max(System.Math.Min((originalHeight - ScreenHeight) / 2, yOffset), -System.Math.Abs(originalHeight * currentScale - ScreenHeight - (originalHeight - ScreenHeight) / 2));

        // bounce the image back to inside the bounds
        s.Content.TranslateTo(xOffset, yOffset, 500, Easing.BounceOut);
    }
}

OnSizeAllocated (most of this you probably dont need, but some you do. consider ScreenWidth, ScreenHeight, yOffset, xOffset, currentScale)

protected override void OnSizeAllocated(double width, double height)
{            
    base.OnSizeAllocated(width, height); //must be called

    if (width != -1 &&  (ScreenWidth != width || ScreenHeight != height))
    {
        ResetLayout(width, height);

        originalWidth = initialLoad ?
            ImageWidth >= 960 ?
               App.ScreenWidth > 320 
                    ? 768 
                    : 320 
                :  ImageWidth / 3
            : imageContainer.Content.Width / imageContainer.Content.Scale;

        var normalizedHeight = ImageWidth >= 960 ?
                App.ScreenWidth > 320 ? ImageHeight / (ImageWidth / 768) 
                : ImageHeight / (ImageWidth / 320) 
            : ImageHeight / 3;

        originalHeight = initialLoad ? 
            normalizedHeight : (imageContainer.Content.Height / imageContainer.Content.Scale);

        ScreenWidth = width;
        ScreenHeight = height;

        xOffset = imageContainer.TranslationX;
        yOffset = imageContainer.TranslationY;

        currentScale = imageContainer.Scale;

        if (initialLoad)
            initialLoad = false;
    }
}

Layout (XAML in C#)

ImageMain = new Image
{
    HorizontalOptions = LayoutOptions.CenterAndExpand,
    VerticalOptions = LayoutOptions.CenterAndExpand,
    Aspect = Aspect.AspectFill,
    Source = ImageMainSource
};

imageContainer = new ContentView
{
    Content = ImageMain,
    BackgroundColor = Xamarin.Forms.Color.Black,
    WidthRequest = App.ScreenWidth - 250
};

var panGesture = new PanGestureRecognizer();
panGesture.PanUpdated += OnPanUpdated;
imageContainer.GestureRecognizers.Add(panGesture);

var pinchGesture = new PinchGestureRecognizer();
pinchGesture.PinchUpdated += OnPinchUpdated;
imageContainer.GestureRecognizers.Add(pinchGesture);

double smallImageHeight = ImageHeight / (ImageWidth / 320);

absoluteLayout = new AbsoluteLayout
{
    HeightRequest = App.ScreenHeight,
    BackgroundColor = Xamarin.Forms.Color.Black,
};

AbsoluteLayout.SetLayoutFlags(imageContainer, AbsoluteLayoutFlags.All);
AbsoluteLayout.SetLayoutBounds(imageContainer, new Rectangle(0f, 0f, AbsoluteLayout.AutoSize, AbsoluteLayout.AutoSize));
absoluteLayout.Children.Add(imageContainer, new Rectangle(0, 0, 1, 1), AbsoluteLayoutFlags.All);
Content = absoluteLayout;
1
votes

I've been working on a Image viewer with pan&zoom...

I reached another variation.

I'll share with you.

First, we need a Pan/Zoom class controller:

using System;
using Xamarin.Forms;

namespace Project.Util
{
    public class PanZoom
    {
        bool pitching = false;
        bool panning = false;

        bool collectFirst = false;

        double xOffset = 0;
        double yOffset = 0;

        //scale processing...
        double scaleMin;
        double scaleMax;
        double scale;

        double _xScaleOrigin;
        double _yScaleOrigin;

        double panTotalX;
        double panTotalY;

        ContentPage contentPage;
        View Content;
        public void Setup(ContentPage cp, View content)
        {
            contentPage = cp;
            Content = content;

            PinchGestureRecognizer pinchGesture = new PinchGestureRecognizer();
            pinchGesture.PinchUpdated += PinchUpdated;
            contentPage.Content.GestureRecognizers.Add(pinchGesture);

            var panGesture = new PanGestureRecognizer();
            panGesture.PanUpdated += OnPanUpdated;
            contentPage.Content.GestureRecognizers.Add(panGesture);

            contentPage.SizeChanged += (sender, e) => { layoutElements(); };
        }

        public void layoutElements()
        {
            if (contentPage.Width <= 0 || contentPage.Height <= 0 || Content.WidthRequest <= 0 || Content.HeightRequest <= 0)
                return;

            xOffset = 0;
            yOffset = 0;

            double pageW = contentPage.Width;
            double pageH = contentPage.Height;

            double w_s = pageW / Content.WidthRequest;
            double h_s = pageH / Content.HeightRequest;
            if (w_s < h_s)
                scaleMin = w_s;
            else
                scaleMin = h_s;
            scaleMax = scaleMin * 3.0;

            scale = scaleMin;

            double w = Content.WidthRequest * scale;
            double h = Content.HeightRequest * scale;
            double x = pageW / 2.0 - w / 2.0 + xOffset;
            double y = pageH / 2.0 - h / 2.0 + yOffset;

            AbsoluteLayout.SetLayoutBounds(Content, new Rectangle(x, y, w, h));
        }

        void fixPosition(
            ref double x, ref double y, ref double w, ref double h,
            bool setoffset
            )
        {
            double pageW = contentPage.Width;
            double pageH = contentPage.Height;


            if (w <= pageW)
            {
                double new_x = pageW / 2.0 - w / 2.0;
                if (setoffset)
                    xOffset = new_x - (pageW / 2.0 - w / 2.0);
                x = new_x;
            } else
            {
                if (x > 0)
                {
                    double new_x = 0;
                    if (setoffset)
                        xOffset = new_x - (pageW / 2.0 - w / 2.0);
                    x = new_x;
                }
                if (x < (pageW - w))
                {
                    double new_x = (pageW - w);
                    if (setoffset)
                        xOffset = new_x - (pageW / 2.0 - w / 2.0);
                    x = new_x;
                }
            }

            if (h <= pageH)
            {
                double new_y = pageH / 2.0 - h / 2.0;
                if (setoffset)
                    yOffset = new_y - (pageH / 2.0 - h / 2.0);
                y = new_y;
            }
            else
            {
                if (y > 0)
                {
                    double new_y = 0;
                    if (setoffset)
                        yOffset = new_y - (pageH / 2.0 - h / 2.0);
                    y = new_y;
                }
                if (y < (pageH - h))
                {
                    double new_y = (pageH - h);
                    if (setoffset)
                        yOffset = new_y - (pageH / 2.0 - h / 2.0);
                    y = new_y;
                }
            }
        }

        private void PinchUpdated(object sender, PinchGestureUpdatedEventArgs e)
        {
            if (sender != contentPage.Content)
                return;

            switch (e.Status)
            {
                case GestureStatus.Started:
                    {
                        pitching = true;
                        collectFirst = true;

                        double pageW = contentPage.Width;
                        double pageH = contentPage.Height;

                        _xScaleOrigin = e.ScaleOrigin.X * pageW;
                        _yScaleOrigin = e.ScaleOrigin.Y * pageH;
                    }
                    break;
                case GestureStatus.Running:
                    if (pitching)
                    {
                        double targetScale = scale * e.Scale;
                        targetScale = Math.Min(Math.Max(scaleMin, targetScale), scaleMax);

                        double scaleDelta = targetScale / scale;

                        double pageW = contentPage.Width;
                        double pageH = contentPage.Height;

                        double w_old = Content.WidthRequest * scale;
                        double h_old = Content.HeightRequest * scale;
                        double x_old = pageW / 2.0 - w_old / 2.0 + xOffset;
                        double y_old = pageH / 2.0 - h_old / 2.0 + yOffset;

                        scale = targetScale;

                        //new w and h
                        double w = Content.WidthRequest * scale;
                        double h = Content.HeightRequest * scale;

                        //transform x old and y old 
                        //   to get new scaled position over a pivot
                        double _x = (x_old - _xScaleOrigin) * scaleDelta + _xScaleOrigin;
                        double _y = (y_old - _yScaleOrigin) * scaleDelta + _yScaleOrigin;

                        //fix offset to be equal to _x and _y
                        double x = pageW / 2.0 - w / 2.0 + xOffset;
                        double y = pageH / 2.0 - h / 2.0 + yOffset;
                        xOffset += _x - x;
                        yOffset += _y - y;
                        x = pageW / 2.0 - w / 2.0 + xOffset;
                        y = pageH / 2.0 - h / 2.0 + yOffset;

                        fixPosition(ref x, ref y, ref w, ref h, true);

                        AbsoluteLayout.SetLayoutBounds(Content, new Rectangle(x, y, w, h));
                    }
                    break;
                case GestureStatus.Completed:
                    pitching = false;
                    break;
            }
        }

        public void OnPanUpdated(object sender, PanUpdatedEventArgs e)
        {
            if (sender != contentPage.Content)
                return;

            switch (e.StatusType)
            {
                case GestureStatus.Started:
                    {
                        panning = true;
                        panTotalX = e.TotalX;
                        panTotalY = e.TotalY;
                        collectFirst = true;
                    }
                    break;
                case GestureStatus.Running:
                    if (panning)
                    {
                        if (collectFirst)
                        {
                            collectFirst = false;
                            panTotalX = e.TotalX;
                            panTotalY = e.TotalY;
                        }

                        double pageW = contentPage.Width;
                        double pageH = contentPage.Height;

                        double deltaX = e.TotalX - panTotalX;
                        double deltaY = e.TotalY - panTotalY;

                        panTotalX = e.TotalX;
                        panTotalY = e.TotalY;

                        xOffset += deltaX;
                        yOffset += deltaY;

                        double w = Content.WidthRequest * scale;
                        double h = Content.HeightRequest * scale;
                        double x = pageW / 2.0 - w / 2.0 + xOffset;
                        double y = pageH / 2.0 - h / 2.0 + yOffset;

                        fixPosition(ref x, ref y, ref w, ref h, true);

                        AbsoluteLayout.SetLayoutBounds(Content, new Rectangle(x, y, w, h));
                    }
                    break;
                case GestureStatus.Completed:
                    panning = false;
                    break;
            }
        }
    }
}

In the content page:

using System;
using FFImageLoading.Forms;
using Xamarin.Forms;
using Project.Util;

namespace Project.ContentPages
{
    public class ContentPage_ImageViewer : ContentPage
    {
        AbsoluteLayout al = null;
        CachedImage image = null;
        PanZoom panZoom;

        public ContentPage_ImageViewer(string imageURL)
        {
            MasterDetailPage mdp = Application.Current.MainPage as MasterDetailPage;
            mdp.IsGestureEnabled = false;
            NavigationPage.SetHasBackButton(this, true);

            Title = "";

            image = new CachedImage()
            {
                HorizontalOptions = LayoutOptions.FillAndExpand,
                VerticalOptions = LayoutOptions.FillAndExpand,
                Aspect = Aspect.Fill,
                LoadingPlaceholder = "placeholder_320x322.png",
                ErrorPlaceholder = "placeholder_320x322.png",
                Source = imageURL,
                RetryCount = 3,
                DownsampleToViewSize = false,
                IsVisible = false,
                FadeAnimationEnabled = false
            };

            image.Success += delegate (object sender, CachedImageEvents.SuccessEventArgs e)
            {
                Device.BeginInvokeOnMainThread(() =>
                {
                    image.WidthRequest = e.ImageInformation.OriginalWidth;
                    image.HeightRequest = e.ImageInformation.OriginalHeight;
                    image.IsVisible = true;

                    for(int i = al.Children.Count-1; i >= 0; i--)
                    {
                        if (al.Children[i] is ActivityIndicator)
                            al.Children.RemoveAt(i);
                    }

                    panZoom.layoutElements();
                });
            };

            ActivityIndicator ai = new ActivityIndicator()
            {
                IsRunning = true,
                Scale = (Device.RuntimePlatform == Device.Android) ? 0.25 : 1.0,
                VerticalOptions = LayoutOptions.Fill,
                HorizontalOptions = LayoutOptions.Fill,
                Color = Color.White
            };

            Content = (al = new AbsoluteLayout()
            {
                VerticalOptions = LayoutOptions.Fill,
                HorizontalOptions = LayoutOptions.Fill,
                BackgroundColor = Color.Black,
                Children =
                {
                    image,
                    ai
                } 
            });

            AbsoluteLayout.SetLayoutFlags(image, AbsoluteLayoutFlags.None);
            AbsoluteLayout.SetLayoutBounds(ai, new Rectangle(0, 0, 1, 1));
            AbsoluteLayout.SetLayoutFlags(ai, AbsoluteLayoutFlags.All);

            panZoom = new PanZoom();
            panZoom.Setup(this, image);
        }
    }
}
-1
votes

For me it worked like below, just did some changes in the code given in the question,

    void OnPinchUpdated(object sender, PinchGestureUpdatedEventArgs e)
     {


if (e.Status == GestureStatus.Started)
            {
                // Store the current scale factor applied to the wrapped user interface element,


     // and zero the components for the center point of the translate transform.
        startScale = Content.Scale;
        Content.AnchorX = 0;
        Content.AnchorY = 0;
    }
    if (e.Status == GestureStatus.Running)
    {
        // Calculate the scale factor to be applied.
        currentScale += (e.Scale - 1) * startScale;
        currentScale = Math.Max(1, currentScale);

        // The ScaleOrigin is in relative coordinates to the wrapped user 
        interface element,
        // so get the X pixel coordinate.
        double renderedX = Content.X + xOffset;
        double deltaX = renderedX / Width;
        double deltaWidth = Width / (Content.Width * startScale);
        double originX = (e.ScaleOrigin.X - deltaX) * deltaWidth;

        // The ScaleOrigin is in relative coordinates to the wrapped user 
        interface element,
        // so get the Y pixel coordinate.
        double renderedY = Content.Y + yOffset;
        double deltaY = renderedY / Height;
        double deltaHeight = Height / (Content.Height * startScale);
        double originY = (e.ScaleOrigin.Y - deltaY) * deltaHeight;

        // Calculate the transformed element pixel coordinates.
        double targetX = xOffset - (originX * Content.Width) * (currentScale - 
        startScale);
        double targetY = yOffset - (originY * Content.Height) * (currentScale - 
        startScale);

        // Apply translation based on the change in origin.
        Content.TranslationX = targetX.Clamp(-Content.Width * (currentScale - 1), 0);
        Content.TranslationY = targetY.Clamp(-Content.Height * (currentScale - 1), 0);

        // Apply scale factor.
        Content.Scale = currentScale;
        width = Content.Width * currentScale;
        height = Content.Height * currentScale;

    }

    if (e.Status == GestureStatus.Completed)
    {
        // Store the translation delta's of the wrapped user interface element.
        xOffset = Content.TranslationX;
        yOffset = Content.TranslationY;
        x = Content.TranslationX;
        y = Content.TranslationY;
    }
}

Pan Code

void OnPanUpdated(object sender, PanUpdatedEventArgs e)
    {
        if (!width.Equals(Content.Width) && !height.Equals(Content.Height))
        {
            switch (e.StatusType)
            {
                case GestureStatus.Started:
                    startX = Content.TranslationX;
                    startY = Content.TranslationY;
                    break;
                case GestureStatus.Running:
                    if (!width.Equals(0))
                    {
                        Content.TranslationX = Math.Max(Math.Min(0, x + e.TotalX), -Math.Abs(Content.Width - width));// App.ScreenWidth));
                    }
                    if (!height.Equals(0))
                    {
                        Content.TranslationY = Math.Max(Math.Min(0, y + e.TotalY), -Math.Abs(Content.Height - height)); //App.ScreenHeight));    
                    }
                    break;
                case GestureStatus.Completed:
                    // Store the translation applied during the pan
                    x = Content.TranslationX;
                    y = Content.TranslationY;
                    xOffset = Content.TranslationX;
                    yOffset = Content.TranslationY;
                    break;
            }
        }
    }