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

DylanLiuDylanLiu USUniversity ✭✭
edited August 2016 in Xamarin.Forms

I'm working on a full screen image page that supports pinch to zoom, pan to move and tap to show captions. I'm basing this on how image viewer works in apps such as Facebook and Yelp. My code is built off Xamarin examples on gesture recognizers, which can be found at https://developer.xamarin.com/guides/xamarin-forms/user-interface/gestures/

My problem is that when the image is zoomed in and I rotate the device, and then zoomed out. The image is off the center. I would really appreciate it very much if someone can help me finish this, so it supports varying device orientations.

using System;
using System.ComponentModel;
using Xamarin.Forms;

namespace TurfDoctor
{
    public class FullScreenImagePage : ContentPage
    {
        double currentScale = 1;
        double startScale = 1;
        double xOffset = 0;
        double yOffset = 0;

        double originalWidth;
        double originalHeight;

        double ScreenWidth;
        double ScreenHeight;

        PanGestureRecognizer panGesture;

        bool showEverything = false;
        StackLayout imageDescription;
        Button backButton;
        BoxView topBox;
        Image image;
        ContentView imageContainer;
        Label indexLabel;
        //Label xLabel, yLabel, transXLabel, transYLabel, widthLabel, heightLabel, scaleLabel, screenWidthLabel, screenHeightLabel;
        AbsoluteLayout absoluteLayout;

        protected override void OnAppearing ()
        {
            ShowEverything = true;
            base.OnAppearing ();
        }

        protected override bool OnBackButtonPressed ()
        {
            App.NavPage.BarTextColor = Color.Black; // turn the status bar back to black
            return base.OnBackButtonPressed ();
        }

        public bool ShowEverything
        {
            set{
                    showEverything = value;
                    backButton.IsVisible = showEverything;
                    imageDescription.IsVisible = showEverything;
                    topBox.IsVisible = showEverything;
                    indexLabel.IsVisible = showEverything;

                    if (!showEverything) {
                        // hide the status bar by turning it black
                        App.NavPage.BarTextColor = Color.Black;
                        imageContainer.GestureRecognizers.Add (panGesture);
                    } else {
                        // show the status bar by turning it white
                        App.NavPage.BarTextColor = Color.White;
                        imageContainer.GestureRecognizers.Remove (panGesture);
                    }
            }
            get{
                return showEverything;
            }
        }

        public FullScreenImagePage (String ImageName, string DescriptionText, int index, int count)
        {
            NavigationPage.SetHasNavigationBar (this, false);

            image = new Image {
                HorizontalOptions = LayoutOptions.CenterAndExpand,
                VerticalOptions = LayoutOptions.CenterAndExpand,
                Aspect = Aspect.AspectFill, 
                Source = ImageName
            };

            imageContainer = new ContentView {
                Content = image
            };

            var tapGesture = new TapGestureRecognizer ();
            tapGesture.Tapped += OnTapped;
            imageContainer.GestureRecognizers.Add (tapGesture);

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

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

            absoluteLayout = new AbsoluteLayout {
                BackgroundColor = MyAppStyle.blackColor,
            };

            var label = new Label { 
                Text = DescriptionText, 
                TextColor = MyAppStyle.whiteColor, 
                FontAttributes = FontAttributes.Bold,
                FontSize = Device.GetNamedSize(NamedSize.Small, typeof(Label))
            };
            var separator = new BoxView() { HeightRequest = 1, BackgroundColor = MyAppStyle.whiteColor};

            imageDescription = new StackLayout {
                Padding = new Thickness(20),
                HorizontalOptions = LayoutOptions.Fill,
                Orientation = StackOrientation.Vertical,
                Children = { label, separator}
            };

            backButton = new Button { Text = "Back", WidthRequest = 80, HeightRequest = 40, TextColor = MyAppStyle.whiteColor, FontAttributes = FontAttributes.Bold };
            backButton.Clicked += (object sender, EventArgs e) => { OnBackButtonPressed(); Navigation.PopAsync(); };

            indexLabel = new Label {
                Text = (index + 1).ToString () + " of " + count.ToString (),
                TextColor = MyAppStyle.whiteColor,
                FontAttributes = FontAttributes.Bold,
                HorizontalTextAlignment = TextAlignment.Center
            };

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

            AbsoluteLayout.SetLayoutFlags (imageDescription, AbsoluteLayoutFlags.PositionProportional | AbsoluteLayoutFlags.WidthProportional);
            AbsoluteLayout.SetLayoutBounds (imageDescription, new Rectangle(0f, 1f, 1f, AbsoluteLayout.AutoSize));
            absoluteLayout.Children.Add(imageDescription);

            topBox = new BoxView { Color = MyAppStyle.blackColor, Opacity = 0.5 };
            AbsoluteLayout.SetLayoutFlags (topBox, AbsoluteLayoutFlags.WidthProportional);
            AbsoluteLayout.SetLayoutBounds (topBox, new Rectangle(0f, 0f, 1f, 50f));
            absoluteLayout.Children.Add (topBox);

            AbsoluteLayout.SetLayoutFlags (backButton, AbsoluteLayoutFlags.None);
            AbsoluteLayout.SetLayoutBounds (backButton, new Rectangle(0f, 10f, AbsoluteLayout.AutoSize, AbsoluteLayout.AutoSize));
            absoluteLayout.Children.Add (backButton);

            AbsoluteLayout.SetLayoutFlags (indexLabel, AbsoluteLayoutFlags.XProportional);
            AbsoluteLayout.SetLayoutBounds (indexLabel, new Rectangle(.5f, 20f, AbsoluteLayout.AutoSize, AbsoluteLayout.AutoSize));
            absoluteLayout.Children.Add (indexLabel);

            Content = absoluteLayout;
        }

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

            if (ScreenWidth != width || ScreenHeight != height) {

                absoluteLayout.ForceLayout();

                originalWidth = imageContainer.Content.Width /  imageContainer.Content.Scale;
                originalHeight = imageContainer.Content.Height / imageContainer.Content.Scale;

                ScreenWidth = width;
                ScreenHeight = height;

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

                currentScale = imageContainer.Content.Scale;
            }
        }

        void OnTapped(object sender, EventArgs e)
        {
            ShowEverything = !ShowEverything;
        }

        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 = Math.Max (Math.Min (0, xOffset), -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 = Math.Max (Math.Min ((originalHeight - ScreenHeight)/2, yOffset), -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;
            }
        }

        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 = Math.Max (1, currentScale);
                currentScale = Math.Min (currentScale, 5);

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

                if (currentScale == 1)
                    ShowEverything = true;
                else
                    ShowEverything = false;

                // 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 / ScreenWidth;
                double deltaWidth = 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 / ScreenHeight;
                double deltaHeight = 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 = Math.Max (Math.Min (0, xOffset), -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 = Math.Max (Math.Min ((originalHeight - ScreenHeight)/2, yOffset), -Math.Abs(originalHeight*currentScale - ScreenHeight - (originalHeight - ScreenHeight)/2));

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

Best Answers

«1

Answers

  • DylanLiuDylanLiu USUniversity ✭✭

    The code that handles device orientation change goes inside the following function according Xamarin documentation
    https://developer.xamarin.com/guides/xamarin-forms/user-interface/layouts/device-orientation/

            protected override void OnSizeAllocated(double width, double height)
            {
                base.OnSizeAllocated(width, height); //must be called
    
                if (ScreenWidth != width || ScreenHeight != height) {
    
            // handle orientation change
            }
        }
    
  • DylanLiuDylanLiu USUniversity ✭✭
    edited August 2016

    My problem is that when the image is zoomed in and I rotate the device, and then zoomed out. The image is off the center. It looks like this:

  • DylanLiuDylanLiu USUniversity ✭✭

    Here is the screenshot image:

  • JordanMaxJordanMax USMember ✭✭

    Any updates? Did you solve this?

  • DylanLiuDylanLiu USUniversity ✭✭

    @JordanMax said:
    Any updates? Did you solve this?

    No. This thread sort of died.

  • JordanMaxJordanMax USMember ✭✭

    @DylanLiu said:

    @JordanMax said:
    Any updates? Did you solve this?

    No. This thread sort of died.

    Damn, so did you disable landscape mode on this page for you? Or what was your resolution?

  • JordanMaxJordanMax USMember ✭✭

    Bit of an odd issue, when you look at the translationx and y for the imagecontainer, it's not correct. But simply correcting it to 0 doesn't fix the issue. Very odd. At first, I found this happens when you zoom (and even if you zoom back out) and then rotate. So let's say you zoom in, and then zoom back out to 0. It would still do it, I fixed that. What I still did not fix is when you zoom in and rotate, it positions the image not where it's supposed to. If I find a fix I'll let you know.

  • JordanMaxJordanMax USMember ✭✭

    Wow, as I typed that, I was able to fix it by this:

    imageContainer.Content.TranslateTo(0, 0, 0, Easing.Linear);

    in the OnSizeAllocated method call.

    The only other issue is that on landscape mode the bounds are not correct, even when I debug with a color for background, the background color takes up the acceptable bounds, but the pan bounds doesn't seem correct. Again, if I find a solution I'll let you know.

  • DylanLiuDylanLiu USUniversity ✭✭

    @JordanMax said:

    @DylanLiu said:

    @JordanMax said:
    Any updates? Did you solve this?

    No. This thread sort of died.

    Damn, so did you disable landscape mode on this page for you? Or what was your resolution?

    I'm not sure how to disable device rotation on a single page. So I disabled it for the whole app.

  • DylanLiuDylanLiu USUniversity ✭✭

    @JordanMax said:
    Wow, as I typed that, I was able to fix it by this:

    imageContainer.Content.TranslateTo(0, 0, 0, Easing.Linear);

    in the OnSizeAllocated method call.

    The only other issue is that on landscape mode the bounds are not correct, even when I debug with a color for background, the background color takes up the acceptable bounds, but the pan bounds doesn't seem correct. Again, if I find a solution I'll let you know.

    Can you post the entire OnSizedAllocated Method? I'm not sure it's working for me

  • JordanMaxJordanMax USMember ✭✭

    @DylanLiu said:

    @JordanMax said:
    Wow, as I typed that, I was able to fix it by this:

    imageContainer.Content.TranslateTo(0, 0, 0, Easing.Linear);

    in the OnSizeAllocated method call.

    The only other issue is that on landscape mode the bounds are not correct, even when I debug with a color for background, the background color takes up the acceptable bounds, but the pan bounds doesn't seem correct. Again, if I find a solution I'll let you know.

    Can you post the entire OnSizedAllocated Method? I'm not sure it's working for me

    Sure, so basically just resetting everything. (some code you don't really need to mind)

            protected override void OnSizeAllocated(double width, double height)
            {            
                base.OnSizeAllocated(width, height); //must be called
    
                if (width != -1 &&  (ScreenWidth != width || ScreenHeight != height))
                {
                    imageContainer.Content.TranslateTo(0, 0, 0, Easing.Linear);
                    imageContainer.Content.Scale = 1;
                    BackToStory.WidthRequest = width;
                    //reset imageContainer 
                    imageContainer.Content = null;
                    absoluteLayout.Children.Remove(imageContainer);
    
                    imageContainer.Content = ImageMain;
                    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);
                    //end reset imageContainer
                    originalWidth = initialLoad ? ImageWidth / 320 : imageContainer.Content.Width / imageContainer.Content.Scale;
    
                    var normalizedHeight = ImageHeight / (ImageWidth / 320);
    
                    originalHeight = initialLoad ? normalizedHeight : (imageContainer.Content.Height / imageContainer.Content.Scale);
    
                    ScreenWidth = width;
                    ScreenHeight = height;
    
                    xOffset = imageContainer.Content.TranslationX;
                    yOffset = imageContainer.Content.TranslationY;
    
                    currentScale = imageContainer.Content.Scale;
    
                    if (initialLoad)
                        initialLoad = false;
                }
    
  • JordanMaxJordanMax USMember ✭✭

    @DylanLiu If an image isn't the width of the screen, I'm seeing some weird behavior. Any thoughts?

  • MabroukMabrouk USMember ✭✭✭

    Hi @DylanLiu ,

    In your pinch update event, try dont initialize the AnchorX and AnchorY :

    Replace this :
    s.Content.AnchorX = 0; s.Content.AnchorY = 0;
    by this :
    //s.Content.AnchorX = 0; //s.Content.AnchorY = 0;

    I hope that helps,
    Mabrouk.

  • DylanLiuDylanLiu USUniversity ✭✭

    @Mabrouk said:
    Hi @DylanLiu ,

    In your pinch update event, try dont initialize the AnchorX and AnchorY :

    Replace this :
    s.Content.AnchorX = 0; s.Content.AnchorY = 0;
    by this :
    //s.Content.AnchorX = 0; //s.Content.AnchorY = 0;

    I hope that helps,
    Mabrouk.

    I tried that one before. Commenting out zeriong AnchorX and AnchorY breaks the pinch to zoom feature. I also tried zeroing the AnchorX and AnchorY in the OnSizedAllocated method, it made it worse.

  • JordanMaxJordanMax USMember ✭✭

    I've tried it all as well. Probably spent days trying out various things. Nothing works besides resetting the image.

  • EmanueleSabettaEmanueleSabetta ITBeta ✭✭✭

    This is a very useful component. You should open a bug on https://bugzilla.xamarin.com , so that this can be addressed asap.

  • JordanMaxJordanMax USMember ✭✭

    @EmanueleSabetta said:
    This is a very useful component. You should open a bug on https://bugzilla.xamarin.com , so that this can be addressed asap.

    Lol asap? More like whenever they feel like doing it. I get your point though ;)

  • DylanLiuDylanLiu USUniversity ✭✭
    edited November 2016

    @EmanueleSabetta
    @JordanMax

    I too spent days working on this ... and finally decided to give up and lock the screen orientation. I posted my code in hope somebody can fix it 100%.

    Like I said my code was based on Xamarin documentation on gesture recognizer.

    I added the bouncing off the edges stuff to make it behave more like facebook and yelp.

  • JordanMaxJordanMax USMember ✭✭

    @DylanLiu said:
    @EmanueleSabetta
    @JordanMax

    I too spent days working on this ... and finally decided to give up and lock the screen orientation. I posted my code in hope somebody can fix it 100%.

    Like I said my code was based on Xamarin documentation on gesture recognizer.

    I added the bouncing off the edges stuff to make it behave more like facebook and yelp.

    Well I did give you a way to fix it. If you reset the image it works perfectly. There's no performance hit either, it just readjusts the image. I've attached my OnSizeAllocated method above in case you missed it. The only problem right now that I see (except the reset workaround as I just mentioned) is that if the image isn't the full width of the screen, the zoom/pinch doesn't work as expected. Any insight on that?

  • RogerSchmidlinRogerSchmidlin CHUniversity ✭✭✭

    I got the same problem. Unfortunately, I couldn't get it to work like @JordanMax . It works fine as long as I work on the same picture. A simple tap is reseting my picture to its original state. But if I load a new picture it gets offset to the top left corner. I also have the issue with turning the screen.
    I also posted an issue on the github where the original Xamarin example is. But no response yet from Xamarin.
    Have you guys had any more luck?

  • JordanMaxJordanMax USMember ✭✭

    @RogerSchmidlin said:
    I got the same problem. Unfortunately, I couldn't get it to work like @JordanMax . It works fine as long as I work on the same picture. A simple tap is reseting my picture to its original state. But if I load a new picture it gets offset to the top left corner. I also have the issue with turning the screen.
    I also posted an issue on the github where the original Xamarin example is. But no response yet from Xamarin.
    Have you guys had any more luck?

    It will work if you follow what I did. What did you try? If you just reset everything on the image, it worked for me. We ended up doing away with landscape, not because of this but a completely unrelated reason. So I can't give you any more insight for I may have deleted the code. But I did post it above. Let me know what you tried.

  • RogerSchmidlinRogerSchmidlin CHUniversity ✭✭✭

    I found out what my problem was. In the Xamarin example the AnchorX and Y are reset to 0. But they should be 0.5. My pictures appear now the way they should :smile: I still have to figure out the rotation problem.

  • RogerSchmidlinRogerSchmidlin CHUniversity ✭✭✭

    And I just found a solution to the screen rotation problem. I am resetting the content properties in OnPropertyChanging rather than OnSizeAllocated. That does the trick for me.

    protected override void OnPropertyChanging(string propertyName = null) { base.OnPropertyChanging(propertyName); if (propertyName == "Width") { ResetView(); } }

  • RogerSchmidlinRogerSchmidlin CHUniversity ✭✭✭

    I did some more modifications and produced a class derived from Image that I can use in the XAML file directly. I combined pinch and pan in one class. Rotation works as well as a reset on tapping once on the picture. One thing that I need to improve is the detection when the panning moves out of scope. The calculation there is not correct yet. Maybe someone can fix that for me :smile:
    `using System;
    using System.Diagnostics;
    using Xamarin.Forms;

    namespace LiveGantt.Helpers
    {
    public class PinchPanImage : Image
    {
    double _currentScale;
    double _startPinchScale;
    double _lastTransX;
    double _lastTransY;

        /// <summary>
        /// constructor
        /// </summary>
        public PinchPanImage()
        {
            var pinchGesture = new PinchGestureRecognizer();
            pinchGesture.PinchUpdated += OnPinchUpdated;
            GestureRecognizers.Add(pinchGesture);
    
            var panGesture = new PanGestureRecognizer();
            panGesture.PanUpdated += OnPanUpdated;
            GestureRecognizers.Add(panGesture);
    
            var tapGesture = new TapGestureRecognizer();
            tapGesture.Tapped += OnTapped;
            tapGesture.NumberOfTapsRequired = 1;
            GestureRecognizers.Add(tapGesture);
    
            ResetView();
        }
    
        /// <summary>
        /// resets the picture to its origin scale and position
        /// </summary>
        void ResetView()
        {
            TranslationY = 0;
            TranslationX = 0;
            Scale = 1;
            AnchorX = 0.5;
            AnchorY = 0.5;
            _currentScale = 1;
            _lastTransX = 0;
            _lastTransY = 0;
        }
    
        /// <summary>
        /// gets triggered before the new picture gets drawn
        /// </summary>
        /// <returns>The measure.</returns>
        /// <param name="widthConstraint">Width constraint.</param>
        /// <param name="heightConstraint">Height constraint.</param>
        protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
        {
            ResetView();
            return base.OnMeasure(widthConstraint, heightConstraint);
        }
    
        /// <summary>
        /// user tapped to reset view
        /// </summary>
        /// <param name="sender">Sender.</param>
        /// <param name="e">E.</param>
        void OnTapped(object sender, EventArgs e)
        {
            ResetView();
        }
    
        /// <summary>
        /// moves the picture on pan gesture
        /// </summary>
        /// <param name="sender">Sender.</param>
        /// <param name="e">E.</param>
        void OnPanUpdated(object sender, PanUpdatedEventArgs e)
        {
            switch (e.StatusType)
            {
                case GestureStatus.Running:
                    // Translate and ensure we don't pan beyond the wrapped user interface element bounds.
                    this.TranslationX =
                        Math.Max(Math.Min(0, _lastTransX + e.TotalX), -Math.Abs(this.Width * _currentScale - this.Width));
                    this.TranslationY =
                               Math.Max(Math.Min(0, _lastTransY + e.TotalY), -Math.Abs(this.Height * _currentScale - this.Height));
                    Debug.WriteLine("TransX={0}, transY={1}", this.TranslationX, this.TranslationY);
                    break;
    
                case GestureStatus.Completed:
                    // Store the translation applied during the pan
                    _lastTransX = this.TranslationX;
                    _lastTransY = this.TranslationY;
                    break;
            }
        }
    
        /// <summary>
        /// pinche zooms the picture on gesture
        /// </summary>
        /// <param name="sender">Sender.</param>
        /// <param name="e">E.</param>
        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.
                _startPinchScale = this.Scale;
                this.AnchorX = 0;
                this.AnchorY = 0;
            }
            if (e.Status == GestureStatus.Running)
            {
                // Calculate the scale factor to be applied.
                _currentScale += (e.Scale - 1) * _startPinchScale;
                _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 = this.X + _lastTransX;
                double deltaX = renderedX / Width;
                double deltaWidth = Width / (this.Width * _startPinchScale);
                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 = this.Y + _lastTransY;
                double deltaY = renderedY / Height;
                double deltaHeight = Height / (this.Height * _startPinchScale);
                double originY = (e.ScaleOrigin.Y - deltaY) * deltaHeight;
    
                // Calculate the transformed element pixel coordinates.
                double targetX = _lastTransX - (originX * this.Width) * (_currentScale - _startPinchScale);
                double targetY = _lastTransY - (originY * this.Height) * (_currentScale - _startPinchScale);
    
                // Apply translation based on the change in origin.
                this.TranslationX = targetX.Clamp(-this.Width * (_currentScale - 1), 0);
                this.TranslationY = targetY.Clamp(-this.Height * (_currentScale - 1), 0);
    
                // Apply scale factor.
                this.Scale = _currentScale;
            }
            if (e.Status == GestureStatus.Completed)
            {
                // Store the translation delta's of the wrapped user interface element.
                _lastTransX = this.TranslationX;
                _lastTransY = this.TranslationY;
            }
        }
    }
    

    }

    <?xml version="1.0" encoding="utf-8"?>




    `

  • pradeepArutlapradeepArutla USUniversity ✭✭

    In iOS device, if device is rotated by 90 degrees(orientation changes), then the image bounds are getting changed and image is getting displayed in one of the corners. Someone please help me in fixing the orientation issues in iOS device. I am trying to work on orientation issues using protected override void OnSizeAllocated(double width, double height)
    {
    } but facing difficulties in calcuations of setting the image bounds properly when orientation of the device changes.

    Note: In Android devices, it is working as expected.

  • PatrickSpainPatrickSpain USMember ✭✭

    @djgtram said:
    Spoke a bit too soon. :-) The corrected panning function:

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

    This uses the same anchor now as the zoom.

    I implemented your solution and it actually works great. One thing though, if an image is not the width of the screen, I'm seeing weird behavior when zoomed in and panning on landscape mode. Portrait it's fine, can't seem to understand why. Any thoughts?

  • @djgtram, I see the same wild behavior on the e.Scale values. I updated your code to account for this by throwing out any value that varies by more than a certain percentage from the previous value I got. Smoothes out the zooming quite a bit.

    private void OnPinchUpdated(object sender, PinchGestureUpdatedEventArgs e) { switch (e.Status) { case GestureStatus.Started: LastScale = e.Scale; StartScale = Scale; AnchorX = e.ScaleOrigin.X; AnchorY = e.ScaleOrigin.Y; break; case GestureStatus.Running: if (e.Scale < 0 || Math.Abs(LastScale - e.Scale) > (LastScale * 1.3) - LastScale) { // e.Scale sometimes returns wildly different values from one update to the next. This causes flickering // By removing values that are too far off, we smooth that out. return; } LastScale = e.Scale; var 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; } }

  • DylanLiuDylanLiu USUniversity ✭✭

    Just FYI, I put the an array of FullScreenImagePage in a carouselPage. And there was problem of swiping through images while the it is zoomed out. Here is the fix:

            public bool ShowEverything
            {
                set{
                if (showEverything == value)
                    return;
    
                        showEverything = value;
                        backButton.IsVisible = showEverything;
                        imageDescription.IsVisible = showEverything;
                        topBox.IsVisible = showEverything;
                        indexLabel.IsVisible = showEverything;
    
                        if (!showEverything) {
                            // hide the status bar by turning it black
                            App.NavPage.BarTextColor = Color.Black;
                            imageContainer.GestureRecognizers.Add (panGesture);
                        } else {
                            // show the status bar by turning it white
                            App.NavPage.BarTextColor = Color.White;
                            imageContainer.GestureRecognizers.Remove (panGesture);
                        }
                }
                get{
                    return showEverything;
                }
            }
    
  • DylanLiuDylanLiu USUniversity ✭✭

    @djgtram @DaveRobinder.3226 the weird flickering while zooming that you are referring to, is it happening on android or iOS?

    I'm experiencing it on android. And it actually makes the image completely disappear if I don't move my fingers very slowsly

  • DonMesserli.1843DonMesserli.1843 USMember ✭✭

    @djgtram

    I am using your ZoomImage as the second row in a two row grid with the first row being a segmented control. The image initially displays properly. I don't mind that it covers up the segmented control when zoomed out.

    When the user changes the segment, I change the Source of the ZoomImage and the image ends up centered on 0,0 of the screen.

    Any ideas?

  • JeroendeKokJeroendeKok USMember

    When i'm using the ZoomImage solution of @djgtram the image positions itself in the top left corner and not completely visible i'm adding the image to an empty contentPage

    Can anyone help me out ?

  • MarkAnsleyMarkAnsley USMember

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

  • GregCadmesGregCadmes USUniversity ✭✭

    With regards to the "choppy" visual behavior on Android, I may have some clues as to why this could be happening.
    Firstly, in the Xamarin.Forms.Platform.Android.ImageRenderer, anytime the Source property is changed, the renderer sets the Source = null prior to the new value its expecting to receive. Setting Source = null causes the flicker. I don't want the Android.ImageRenderer to set the Source to null just because the value (bytes) are changing.

    For reference, here is the bug that will probably NEVER get fixed by Xamarin Microsoft.
    https://bugzilla.xamarin.com/show_bug.cgi?id=36600

    I resolved this heinous issue using a custom renderer. I override the OnElementPropertyChanged and handle when the Source property is changing. Obviously I override OnElementChanged as well. In that override I create an instance of the Android.Widget.ImageView using the SetNativeControl API.

    If you happen to see flickering (or choppy) behavior, try this and let me know if it worked for you or not.
    The use-case for my application is a Xamarin.Forms.Image downloading a .png file (95KB) from a site every 1.5 seconds. Flicker would ensue, but the 'NoFlickerImageViewRenderer' saved me.
    By the way, iOS does not have this problem.

    public class NoFlickerImageViewRenderer : ImageRenderer
    {
        protected async override void OnElementChanged(ElementChangedEventArgs<Image> e)
        {
            base.OnElementChanged(e);
    
            if (Control == null)
            {
                SetNativeControl(new ImageView(Context));
    
    
                if (Element != null)
                    await TryUpdateBitmap();
            }
        }
        protected async override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            try
            {
                if (e.PropertyName == Image.SourceProperty.PropertyName)
                {
                    if (Element == null || Element.Source == null)
                        base.OnElementPropertyChanged(sender, e);
                    else
                        await TryUpdateBitmap();
                }
                else
                    base.OnElementPropertyChanged(sender, e);
            }
            catch (Exception x)
            {
                System.Diagnostics.Debug.WriteLine(x.Message);
            }
        }
        async Task TryUpdateBitmap()
        {
            // By default we'll just catch and log any exceptions thrown by UpdateBitmap so they don't bring down
            // the application; a custom renderer can override this method and handle exceptions from
            // UpdateBitmap differently if it wants to
    
            try
            {
                ((IImageController)Element)?.SetIsLoading(true);
                var bmp = await AndroidImageHelper.GetBitmapFromImageSourceAsync(Element.Source, Context);
                Control.SetScaleType(ImageView.ScaleType.CenterCrop);
                Control.SetImageBitmap(bmp);
            }
            catch (Exception x)
            {
                System.Diagnostics.Debug.WriteLine(x.Message);
            }
            finally
            {
                ((IImageController)Element)?.SetIsLoading(false);
            }
        }
    }
    
    class AndroidImageHelper
    {
        private static IImageSourceHandler GetHandler(ImageSource source)
        {
            IImageSourceHandler returnValue = null;
            if (source is UriImageSource)
            {
                returnValue = new ImageLoaderSourceHandler();
            }
            else if (source is FileImageSource)
            {
                returnValue = new FileImageSourceHandler();
            }
            else if (source is StreamImageSource)
            {
                returnValue = new StreamImagesourceHandler();
            }
            return returnValue;
        }
        public static async Task<Bitmap> GetBitmapFromImageSourceAsync(ImageSource source, Context context)
        {
            var handler = GetHandler(source);
            var returnValue = (Bitmap)null;
    
            returnValue = await handler.LoadImageAsync(source, context);
    
            return returnValue;
        }
    }
    
  • DanielLDanielL PLInsider ✭✭✭✭

    You can also replace Image with CachedImage from FFImageLoading (it doesn't have that issue too). It will also auto-cancel all current image loading tasks if the source changing refresh rate is high.

  • @JeroendeKok said:
    When i'm using the ZoomImage solution of @djgtram the image positions itself in the top left corner and not completely visible i'm adding the image to an empty contentPage

    Can anyone help me out ?

    I am having the same problem as well on iOS.

  • @JeroendeKok said:
    When i'm using the ZoomImage solution of @djgtram the image positions itself in the top left corner and not completely visible i'm adding the image to an empty contentPage

    Can anyone help me out ?

    I fixed it by setting the preset AnchorX and AnchorY in the constructor to 0.5 insted of 0 as they suggested.

    `
    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, LastScale, StartX, StartY;

        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.5;
        }
    
        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) {
            switch (e.StatusType) {
                case GestureStatus.Started: {
                    StartX = (1 - AnchorX) * Width;
                    StartY = (1 - AnchorY) * Height;
                    break;
                }
                case GestureStatus.Running: {
                    AnchorX = (1 - (StartX + e.TotalX) / Width).Clamp(0, 1);
                    AnchorY = (1 - (StartY + e.TotalY) / Height).Clamp(0, 1);
                    break;
                }
            }
        }
        private void OnPinchUpdated(object sender, PinchGestureUpdatedEventArgs e) {
            switch (e.Status) {
                case GestureStatus.Started: {
                    LastScale = e.Scale;
                    StartScale = Scale;
                    AnchorX = e.ScaleOrigin.X;
                    AnchorY = e.ScaleOrigin.Y;
                    break;
                }
                case GestureStatus.Running: {
                    if (e.Scale < 0 || Math.Abs(LastScale - e.Scale) > (LastScale * 1.3) - LastScale) {
                        // e.Scale sometimes returns wildly different values from one update to the next. This causes flickering                    
                        // By removing values that are too far off, we smooth that out.                  
                        return;
                    }
                    LastScale = e.Scale;
                    var current = Scale + (e.Scale - 1) * StartScale; Scale = current.Clamp(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 also, if anyone is having problem of Pan Gesture in iOS with the MasterDetailsPage menu gesture, try setting the IsGestureEnabled property of your MasterDetailsPage page.

    IsGestureEnabled = false

    I had noticed that in Android Emulator, the panning is not correct. I haven't tried it on an actual Android device yet.
    On the other hand, the panning on iOS Simulator works correctly.

    The only problem i am having right now with the double tap to zoom the image is it did not capture the touched position as the anchor. Everyone on the Internet i found claimed that the GestureRecognizers does not provide you the touch positions unless you make a custom renderer, but I m not sure how to get the touch positions in the renderer.

  • eyalkapaheyalkapah USMember
    edited September 17

    Hi GregCadmes,

    How can I use this control ?
    If I put it in the PCL it requires me to add android platform dlls etc

  • DylanLiuDylanLiu USUniversity ✭✭

    @MeteorsoftWindows If you want to get the touch position through a custom renderer, I recommend that you take a look at an example of a paint app. Like this one: https://github.com/MitchMilam/Drawit

«1
Sign In or Register to comment.