1
votes

I have a label with a lot of text that I want to enable pinch-to-zoom and panning gesture recognizers in. I used the recipes from here and then nested them within each other.

https://developer.xamarin.com/guides/xamarin-forms/user-interface/gestures/pinch/

https://developer.xamarin.com/guides/xamarin-forms/user-interface/gestures/pan/

Problem is, both container objects allow you to move the label completely outside of it's normal bounds anywhere within the top level page view (demonstrated in the pictures below).

Any thoughts on how to implement some limits on these? I'm sure it's just placing some limits on the math in the container code, but I haven't found the right thing to change yet.

As you can see in these images, both the pinch-to-zoom container (without panning) and the pan container (without zooming) allow you to alter the control so it goes outside it's bounds.

Initial Layout:

Initial layout

Pinch-To-Zoom only

Pinch-To-Zoom only

Panning only

Panning Only

Pinch and Pan

Pinch and Pan

The links above have the container code, but here it is:

PinchToZoomContainer.cs

public class PinchToZoomContainer : ContentView
{
    // Pinch Gesture variables
    double currentScale = 1;
    double startScale = 1;
    double xOffset = 0;
    double yOffset = 0;


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

    }



    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;
        }
        if (e.Status == GestureStatus.Completed) {
            // Store the translation delta's of the wrapped user interface element.
            xOffset = Content.TranslationX;
            yOffset = Content.TranslationY;
        }
    }

PanContainer.cs

public class PanContainer : ContentView
{
    double startX, startY;
    double x, y;

    public PanContainer ()
    {
        // Set PanGestureRecognizer.TouchPoints to control the 
        // number of touch points needed to pan
        var panGesture = new PanGestureRecognizer ();
        panGesture.PanUpdated += OnPanUpdated;
        GestureRecognizers.Add (panGesture);
    }

    void OnPanUpdated (object sender, PanUpdatedEventArgs e)
    {
        switch (e.StatusType) {

        case GestureStatus.Started:
            startX = Content.TranslationX;
            startY = Content.TranslationY;
            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));// App.ScreenWidth));
            //Content.TranslationY = Math.Max (Math.Min (0, y + e.TotalY), -Math.Abs (Content.Height - App.ScreenHeight)); //App.ScreenHeight));    
            Content.TranslationX = startX + e.TotalX;
            Content.TranslationY = startY + e.TotalY;

            break;

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

I imagine, on the PanContainer, my issue is in these lines that I had to comment out:

            //Content.TranslationX = Math.Max (Math.Min (0, x + e.TotalX), -Math.Abs (Content.Width - App.ScreenWidth));// App.ScreenWidth));
            //Content.TranslationY = Math.Max (Math.Min (0, y + e.TotalY), -Math.Abs (Content.Height - App.ScreenHeight)); //App.ScreenHeight));    

I changed these to a more simple version because I can't find App.ScreenWidth or .ScreenHeight properties.

The pinch container, however, is just as it was originally in the recipe and still goes outside the bounds.

2

2 Answers

0
votes

This answer is mostly likely very late for your needs, Chet... but, you can simply wrap the whole thing in a ScrollView (which you will appropriately locate and/or size to your needs). That should work as expected.

  <ScrollView Grid.Column="2" VerticalOptions="Start">
    <PanContainer>
      <PanContainer.Content>
        <Image x:Name="SomeImage" Aspect="AspectFit"  />
      </PanContainer.Content>
    </PanContainer>
  </ScrollView>

Cheers! Mike

0
votes

There is an IsClippedToBounds property that helped me with this issue.

For example:

<PanContainer IsClippedToBounds="true">
    <PanContainer.Content>
        <Image x:Name="SomeImage" />
    </PanContainer.Content>
</PanContainer>

To get pinch and pan, you can either wrap a pinch element in a pan element or vice versa, or you can create a single class with the functions from both the pinch and pan classes. The latter is probably better.

That alone will probably not work exactly as you expect though because the calculations in the pinch and pan functionality are not aware of each other, so if for example you pinch to zoom in then the pan functionality doesn't know that it can now pan further.