8

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
    Commented Dec 7, 2016 at 12:49
  • Yes I did, ended up doing something different than this to get it to work. I'll post my methods below. Commented Dec 7, 2016 at 15:37
  • Thank you so much. I will try this out.
    – rooby
    Commented Dec 7, 2016 at 21:01
  • upvote if it works for you :) also happy to help you if you have any issues. Commented Dec 7, 2016 at 21:02

4 Answers 4

13

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;
8
  • Didnt try this but you dont seem to be taking in account bounds for pan. If the image is not the width/height of the screen, I imagine this code would have a plethora of issues. Commented Jan 19, 2017 at 19:36
  • The system does it for you. The Anchor values are normalized to the 0..1 range, you don't need more than that.
    – Gábor
    Commented Jan 20, 2017 at 9:19
  • Hmm.. interesting. Just out of curiosity, how would the layout look? Commented Jan 20, 2017 at 17:20
  • Im my particular case, there was no specific layout. I just inherited a ZoomImagefrom Image, added the handlers and wherever I used it, there were pinch and pan and everything.
    – Gábor
    Commented Jan 20, 2017 at 21:30
  • I have my image in a stacklayout and your solution doesn't seem to work with what I have going. But I do appreciate your feedback. Commented Jan 20, 2017 at 21:34
3

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;
4
  • 2
    There seems to be some missing related code. For example, ImageWidth & ImageHeight are undefined, along with the ResetLayout() function. Relevant xaml may also be useful to avoid making assumptions about what the imageContainer is.
    – rooby
    Commented Dec 8, 2016 at 14:50
  • Well of course ImageHeight/Width is undefined for you. You need to get those values and fill them. I've added the C# layout code (didn't do it in XAML) for you. You can easily convert to XAML if need be. Commented Dec 8, 2016 at 16:53
  • @jdmdevdotnet Could you post the code for the ResetLayout method? The code seems solid but I'm having some odd bounds detection problems and I think its probably covered in that method. Thx Commented Jul 19, 2017 at 15:55
  • Unfortunately I no longer work for the company that I did this code for and do not have access to it :(. I should have posted it, but in essence is just sets the width and height passed through OnSizeAllocated. Commented Jul 19, 2017 at 17:01
1

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

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;
            }
        }
    }
1
  • very good code, it's working like a charm. just a question: do you know how it's possible to limit the value of the zoom? for example if I want to limite the zoom to 3x, how can I achieve that? Commented Jan 19, 2022 at 16:29

Not the answer you're looking for? Browse other questions tagged or ask your own question.