Full Screen Image Viewer (with Pinch to Zoom, Pan to Move, Tap to show captions) for Xamarin forms.

2»

Answers

  • CarLoOSXCarLoOSX USMember ✭✭

    The best solution, before this one I was trying like Xamarin.forms examples... But this is exactly what I need. Thank you so much!!!!

    @djgtram said:
    @Roger, as I looked all over the web, all solutions seemed to be a bit too complicated and still lacking in functionality. Actually, all can be traced back to some original on the dev.xamarin forum, including the overcomplicated calculations. Instead of those, we can simply use the AnchorX/AnchorY properties. This is what I came up with in the end:

    public class ZoomImage : Image {
      private const double MIN_SCALE = 1;
      private const double MAX_SCALE = 4;
      private const double OVERSHOOT = 0.15;
      private double StartScale;
      private double LastX, LastY;
    
      public ZoomImage() {
        var pinch = new PinchGestureRecognizer();
        pinch.PinchUpdated += OnPinchUpdated;
        GestureRecognizers.Add(pinch);
    
        var pan = new PanGestureRecognizer();
        pan.PanUpdated += OnPanUpdated;
        GestureRecognizers.Add(pan);
    
        var tap = new TapGestureRecognizer { NumberOfTapsRequired = 2 };
        tap.Tapped += OnTapped;
        GestureRecognizers.Add(tap);
    
        Scale = MIN_SCALE;
        TranslationX = TranslationY = 0;
        AnchorX = AnchorY = 0;
      }
    
      protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint) {
        Scale = MIN_SCALE;
        TranslationX = TranslationY = 0;
        AnchorX = AnchorY = 0;
        return base.OnMeasure(widthConstraint, heightConstraint);
      }
    
      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; //TODO tapped position
          this.ScaleTo(MAX_SCALE, 250, Easing.CubicInOut);
        }
      }
    
      private void OnPanUpdated(object sender, PanUpdatedEventArgs e) {
        if (Scale > MIN_SCALE)
          switch (e.StatusType) {
            case GestureStatus.Started:
              LastX = TranslationX;
              LastY = TranslationY;
              break;
            case GestureStatus.Running:
              TranslationX = Clamp(LastX + e.TotalX * Scale, -Width / 2, Width / 2);
              TranslationY = Clamp(LastY + e.TotalY * Scale, -Height / 2, Height / 2);
              break;
          }
      }
    
      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;
        }
      }
    
      private T Clamp<T>(T value, T minimum, T maximum) where T : IComparable {
        if (value.CompareTo(minimum) < 0)
          return minimum;
        else if (value.CompareTo(maximum) > 0)
          return maximum;
        else
          return value;
      }
    }
    

    Extra functionality: zoom overshoot with springy bounce-back. Double tap in both directions: to jump to full zoom and restore the original scale. Unfortunately, some poor soul at Xamarin decided not to send co-ordinate info with the Tapped event (I can't fathom what they were thinking), so it jumps to full zoom centered now.

    And the problems: I don't know if this is something universal or it only happens on my device but while zooming in from scale=1 works all right, pinching to zoom out at larger scales results in wildly out-of-tune, alternating e.Scale values coming in from Xamarin (something like alternating 0.7, 1.3, 0.6, 1.4 and so on, in quick succession, making the image flicker). It would be nice to know if somebody else can see this, too.

    Apart from this, this code seems to work all right.

  • CarLoOSXCarLoOSX USMember ✭✭

    @MarkAnsley said:
    Is anyone else having performance issues on android with this? IOS zooms smooth, but Android is very choppy.

    In iOS works perfect , but not in android, the problem is when you try to zoom out

  • CarLoOSXCarLoOSX USMember ✭✭

    Okay Im working with a content view inside a ViewPager, I have corrected the bad behaviour on Android mentioned in other comments. My solution has been combining 2 solutions... The Xamarin Oficial examples like https://developer.xamarin.com/guides/xamarin-forms/application-fundamentals/gestures/pinch/ and @djgtram solution.

    I enable and disable the pan gesture depending on the scale of the image or contentpage because of the CarouselView plugin, but if you don't want this you should create like the other instances in the constructor.

    Android and iOS works nice, but I have 2 "problems":

    Ontap -> It should scale from the touched pointed, but instead is scaling from 0.5
    OnPan -> the same as Ontap it should start moving from the touched point but it doesn't and also The commented code on that method works but I don't know whats the expected behaviour so its up to you.

    This solution should works in a Image too, but if it doen't try to make a custom control (content view with an image).

            public ViewPagerItemPage()
            {
                InitializeComponent();
    
                PinchGestureRecognizer pinch = new PinchGestureRecognizer();
                pinch.PinchUpdated += OnPinchUpdated;
                GestureRecognizers.Add(pinch);
    
                pan = new PanGestureRecognizer();
                pan.PanUpdated += OnPanUpdated;
    
                TapGestureRecognizer tap = new TapGestureRecognizer { NumberOfTapsRequired = 2 };
                tap.Tapped += OnTapped;
                GestureRecognizers.Add(tap);
    
                Content.AnchorX = Content.AnchorY = 0;
            }
    
            void RestoreScaleValues()
            {
                Content.ScaleTo(MIN_SCALE, 250, Easing.CubicInOut);
                Content.TranslateTo(0.5, 0.5, 250, Easing.CubicInOut);
    
                CurrentScale = 1;
    
                Content.TranslationX = 0.5;
                Content.TranslationY = 0.5;
    
                xOffset = Content.TranslationX;
                yOffset = Content.TranslationY;
            }
    
            private void OnTapped(object sender, EventArgs e)
            {
                if (Content.Scale > MIN_SCALE)
                {
                    RestoreScaleValues();
                    if (GestureRecognizers.Contains(pan))
                    {
                        Device.BeginInvokeOnMainThread(() =>
                        {
                            GestureRecognizers.Remove(pan);
    
                        });
                    }
                               //Enable Swipe of the carouselview or view pager here
                }
                else
                {
                    Content.AnchorX = Content.AnchorY = 0.5; //TODO tapped position
                    Content.ScaleTo(MAX_SCALE, 250, Easing.CubicInOut);
                    if (!GestureRecognizers.Contains(pan))
                    {
                        Device.BeginInvokeOnMainThread(() =>
                        {
                            GestureRecognizers.Add(pan);
    
                        });
                    }
                               //Disable Swipe of the carouselview or view pager here
                }
            }
    
            private void OnPanUpdated(object sender, PanUpdatedEventArgs e)
            {
                switch (e.StatusType)
                {
                    case GestureStatus.Started:
                        StartX = (1 - x) * Width;
                        StartY = (1 - y) * Height;
                        break;
                    case GestureStatus.Running:
                        x = Clamp(1 - (StartX + e.TotalX) / Width, 0, 1);
                        y = Clamp(1 - (StartY + e.TotalY) / Height, 0, 1);
    
                        Content.AnchorX = x;
                        Content.AnchorY = y;
    
                        Content.TranslationX = Content.AnchorX;
                        Content.TranslationY = Content.AnchorY;
                        break;
                }
    
                /*  switch (e.StatusType)
                  {
                      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));
                          Content.TranslationY =
                                     Math.Max(Math.Min(0, y + e.TotalY), -Math.Abs(Content.Height));
                          break;
    
                      case GestureStatus.Completed:
                          // Store the translation applied during the pan
                          x = Content.TranslationX;
                          y = Content.TranslationY;
                          break;
                  }
      */
            }
    
            private T Clamp<T>(T value, T minimum, T maximum) where T : IComparable
            {
                if (value.CompareTo(minimum) < 0)
                    return minimum;
                else if (value.CompareTo(maximum) > 0)
                    return maximum;
                else
                    return value;
            }
    
            void OnPinchUpdated(object sender, PinchGestureUpdatedEventArgs e)
            {
                if (e.Status == GestureStatus.Started)
                {
    
                               //Disable Swipe of the carouselview or view pager here
                    // 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;
                    if (!GestureRecognizers.Contains(pan))
                    {
                        Device.BeginInvokeOnMainThread(() =>
                        {
                            GestureRecognizers.Add(pan);
    
                        });
                    }
                    if (Content.Scale == MIN_SCALE)
                    {
                        if (GestureRecognizers.Contains(pan))
                        {
                            Device.BeginInvokeOnMainThread(() =>
                            {
                                GestureRecognizers.Remove(pan);
    
                            });
                        }
                               //Enable Swipe of the carouselview or view pager here
                    }
    
                }
            }
    

    If anyone has an idea to make better the tap and pan (my two "problems") please post the answers.

    Sorry about my English :smiley:

2»
Sign In or Register to comment.