Xamarin形成捏合和平移

时间:2016-10-21 16:05:09

标签: xamarin xamarin.forms pinchzoom

我已经单独实现了pan和pinch,它工作正常。我现在正在尝试使用捏合和平移,我发现了一些问题。这是我的代码:

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>

捏/手势添加:

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

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

Pan方法:

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;
    }
}

捏法:

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;
    }
}

如果我关闭任一手势并仅使用另一个手势,那么功能完美无缺。当我添加平移和捏合手势时会出现问题。似乎正在发生的是:

1)平底锅实际上似乎按预期工作 2)当你最初平移图像时,让我们说,将图像移动到Y中心和X中心,然后你尝试缩放,图像被设置回它的初始状态。然后,当你平移时,它会让你回到你试图缩放之前的位置(这就是为什么我说平底锅工作正常)。

根据我对调试的理解,当你放大它时,它没有考虑你目前所处的位置。因此,当您先平移,然后进行缩放时,它不会放大您所处的位置,而是放大图像的起始点。然后当你尝试从那里平移时,pan方法仍会记住你的位置,它会让你回到你试图缩放之前的位置。

希望对此有所了解。显然,我的捏合方法存在问题。我只是想(显然无法弄清楚)我需要在其中添加逻辑,并考虑到您当前所处的位置。

4 个答案:

答案 0 :(得分:9)

主要原因可能是每个人似乎都复制并使用此代码(来自dev.xamarin网站),其中包含非常复杂且非常不必要的协调计算:-)。这是不必要的,因为我们可以简单地让视图为我们做繁重的工作,使用AnchorXAnchorY属性来实现此目的。

我们可以进行双击操作以放大并恢复到原始比例。请注意,因为Xamarin无法为其Tap事件提供坐标值(实际上非常不明智的决定),我们现在只能从中心放大:

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);
    }
}

捏合处理器同样简单,根本不需要计算任何翻译。我们所要做的就是将锚点设置为捏起点,框架将完成其余的工作,缩放将围绕这一点进行。请注意,我们这里甚至还有一个额外的功能,即在缩放比例两端的过冲时弹性反弹。

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;
    }
}

平移处理程序,甚至更简单。一开始,我们计算锚点的起点,在平移过程中,我们不断更改锚点。这个锚点相对于视图区域,我们可以很容易地将它夹在0和1之间,这样就可以在极端情况下停止平移而不进行任何平移计算。

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;
    }
}

使用的常量和变量就是:

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;

答案 1 :(得分:3)

采用完全不同的处理方法。对于任何有问题的人来说,这是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 (大多数情况下你可能不需要,但有些你需要。考虑ScreenWidthScreenHeightyOffsetxOffsetcurrentScale

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;
    }
}

布局(C#中的XAML)

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;

答案 2 :(得分:0)

对我来说,它的工作方式如下所示,只是对问题中给出的代码做了一些更改,

    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;
    }
}

邮政编码

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;
            }
        }
    }

答案 3 :(得分:0)

我一直在使用Pan&zoom制作图像查看器...

我遇到了另一个变化。

我会和你分享。

首先,我们需要一个Pan / Zoom类控制器:

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;
            }
        }
    }
}

在内容页面中:

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);
        }
    }
}