Xamarin.forms: [Bug] UWP ScrollView scroll goes up when touch children

Created on 1 Jul 2020  路  9Comments  路  Source: xamarin/Xamarin.Forms

Description

Hi folks.

Hi folks.

I am using a scroll view in my application and after updating "Xamarin.Forms" it does not work correctly. The scroll goes up when I touch any child of the scroll view. I have attached a gif file that shows the scenario explained above.

Steps to Reproduce

  1. Clone/Download a sample
  2. Run the application
  3. Scroll down
  4. Tap on any "Red" separator

Expected Behavior

the scroll should not go up

Actual Behavior

the scroll goes up

Basic Information

  • Version with issue: 4.6.0.968
  • Last known good version: 4.5.0.530
  • IDE: Visual Studio
  • Platform Target Frameworks:

    • UWP: 16299

Screenshots

scrolling

Reproduction Link

Sample

Workaround

N/A

scrollview 5 critical regression UWP bug

All 9 comments

probably the same as
XF issue https://github.com/xamarin/Xamarin.Forms/issues/5652
MS ui xaml issue: https://github.com/microsoft/microsoft-ui-xaml/issues/597

there is a workaround which is to make your own scrollview renderer that removes the change that broke the behaviour.
I'm not sure how you mention that there's a last good XF version, as this has been broken for a long time now, I was not aware of any changes on this.

@MitchBomcanhao you are right about last good XF version. I just noticed that happens in previous versions also. Thank you for your response. I will try the renderer. Not sure if it will work... So I am not closing this issue.)))

This might be a duplicate of #11106. Please try the latest version of 4.7.0.1080 published today.

@samhouts I have updated XF version to 4.7.0.1080 and I am still getting the same issue. I have pushed the changes into the master branch in my GitHub repo.

@MitchBomcanhao could you help me to find any source of scroll view renderer? I could not find it. I have tried this but it does not work for me.

@vardansargsyan92 I'll have a look in a bit...

ok, this is what I've got

this is based on the renderer code from January 19, 2019, so might be missing stuff that newer versions include

it is essentially the same renderer but I've commented out the IDontGetFocus interface and included a copy of the Cleanup method from extensions because it is internal and can't be reached from our custom renderer.

those are the two changes I had to do to get it building and running. if you use a newer version of Xamarin forms, you might need some additional changes, eg to support the new shapes features correctly. you should be able to figure that out by looking at the history of the scrollview renderer.

using System;
using System.ComponentModel;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using ScrollBarVisibility = Xamarin.Forms.ScrollBarVisibility;
using UwpScrollBarVisibility = Windows.UI.Xaml.Controls.ScrollBarVisibility;
using Size = Xamarin.Forms.Size;
using Point = Xamarin.Forms.Point;
using Thickness = Xamarin.Forms.Thickness;
using Xamarin.Forms;
using Xamarin.Forms.Platform.UWP;

[assembly: ExportRenderer(typeof(ScrollView), typeof(YOURNAMESPACE.Renderers.ScrollViewRenderer))]
namespace YOURNAMESPACE.Renderers
{
    public class ScrollViewRenderer : ViewRenderer<ScrollView, ScrollViewer> //, IDontGetFocus
    {
        VisualElement _currentView;
        bool _checkedForRtlScroll = false;

        public ScrollViewRenderer()
        {
            AutoPackage = false;
        }

        public override SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint)
        {
            SizeRequest result = base.GetDesiredSize(widthConstraint, heightConstraint);
            result.Minimum = new Size(40, 40);
            return result;
        }

        protected override Windows.Foundation.Size ArrangeOverride(Windows.Foundation.Size finalSize)
        {
            if (Element == null)
                return finalSize;

            Element.IsInNativeLayout = true;

            Control?.Arrange(new Rect(0, 0, finalSize.Width, finalSize.Height));

            Element.IsInNativeLayout = false;

            return finalSize;
        }

        protected override void Dispose(bool disposing)
        {
            CleanUp(Element, Control);
            base.Dispose(disposing);
        }

        protected override Windows.Foundation.Size MeasureOverride(Windows.Foundation.Size availableSize)
        {
            if (Element == null)
                return new Windows.Foundation.Size(0, 0);

            double width = Math.Max(0, Element.Width);
            double height = Math.Max(0, Element.Height);
            var result = new Windows.Foundation.Size(width, height);

            Control?.Measure(result);

            return result;
        }

        void CleanUp(ScrollView scrollView, ScrollViewer scrollViewer)
        {
            if (scrollView != null)
                scrollView.ScrollToRequested -= OnScrollToRequested;

            if (scrollViewer != null)
            {
                scrollViewer.ViewChanged -= OnViewChanged;
                if (scrollViewer.Content is FrameworkElement element)
                {
                    element.LayoutUpdated -= SetInitialRtlPosition;
                }
            }

            if (_currentView != null)
                _currentView.Cleanup();
        }

        protected override void OnElementChanged(ElementChangedEventArgs<ScrollView> e)
        {
            base.OnElementChanged(e);
            CleanUp(e.OldElement, Control);

            if (e.NewElement != null)
            {
                if (Control == null)
                {
                    SetNativeControl(new ScrollViewer
                    {
                        HorizontalScrollBarVisibility = ScrollBarVisibilityToUwp(e.NewElement.HorizontalScrollBarVisibility),
                        VerticalScrollBarVisibility = ScrollBarVisibilityToUwp(e.NewElement.VerticalScrollBarVisibility)
                    });

                    Control.ViewChanged += OnViewChanged;
                }

                Element.ScrollToRequested += OnScrollToRequested;

                UpdateOrientation();

                UpdateContent();
            }
        }

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            if (e.PropertyName == "Content")
                UpdateContent();
            else if (e.PropertyName == Layout.PaddingProperty.PropertyName)
                UpdateContentMargins();
            else if (e.PropertyName == ScrollView.OrientationProperty.PropertyName)
                UpdateOrientation();
            else if (e.PropertyName == ScrollView.VerticalScrollBarVisibilityProperty.PropertyName)
                UpdateVerticalScrollBarVisibility();
            else if (e.PropertyName == ScrollView.HorizontalScrollBarVisibilityProperty.PropertyName)
                UpdateHorizontalScrollBarVisibility();
        }

        protected void OnContentElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == View.MarginProperty.PropertyName)
                UpdateContentMargins();
        }

        void UpdateContent()
        {
            if (_currentView != null)
                _currentView.Cleanup();

            if (Control?.Content is FrameworkElement oldElement)
            {
                oldElement.LayoutUpdated -= SetInitialRtlPosition;

                if (oldElement is IVisualElementRenderer oldRenderer
                    && oldRenderer.Element is View oldContentView)
                    oldContentView.PropertyChanged -= OnContentElementPropertyChanged;
            }

            _currentView = Element.Content;

            IVisualElementRenderer renderer = null;
            if (_currentView != null)
                renderer = _currentView.GetOrCreateRenderer();

            Control.Content = renderer != null ? renderer.ContainerElement : null;

            UpdateContentMargins();
            if (renderer?.Element != null)
                renderer.Element.PropertyChanged += OnContentElementPropertyChanged;

            if (renderer?.ContainerElement != null)
                renderer.ContainerElement.LayoutUpdated += SetInitialRtlPosition;
        }

        async void OnScrollToRequested(object sender, ScrollToRequestedEventArgs e)
        {
            ClearRtlScrollCheck();

            // Adding items into the view while scrolling to the end can cause it to fail, as
            // the items have not actually been laid out and return incorrect scroll position
            // values. The ScrollViewRenderer for Android does something similar by waiting up
            // to 10ms for layout to occur.
            int cycle = 0;
            while (Element != null && !Element.IsInNativeLayout)
            {
                await Task.Delay(TimeSpan.FromMilliseconds(1));
                cycle++;

                if (cycle >= 10)
                    break;
            }

            if (Element == null)
                return;

            double x = e.ScrollX, y = e.ScrollY;

            ScrollToMode mode = e.Mode;
            if (mode == ScrollToMode.Element)
            {
                Point pos = Element.GetScrollPositionForElement((VisualElement)e.Element, e.Position);
                x = pos.X;
                y = pos.Y;
                mode = ScrollToMode.Position;
            }

            if (mode == ScrollToMode.Position)
            {
                Control.ChangeView(x, y, null, !e.ShouldAnimate);
            }
            Element.SendScrollFinished();
        }

        void SetInitialRtlPosition(object sender, object e)
        {
            if (Control == null) return;

            if (Control.ActualWidth <= 0 || _checkedForRtlScroll || Control.Content == null)
                return;

            if (Element is IVisualElementController controller && controller.EffectiveFlowDirection.IsLeftToRight())
            {
                ClearRtlScrollCheck();
                return;
            }

            var element = (Control.Content as FrameworkElement);
            if (element.ActualWidth == Control.ActualWidth)
                return;

            ClearRtlScrollCheck();
            Control.ChangeView(element.ActualWidth, 0, null, true);
        }

        void ClearRtlScrollCheck()
        {
            _checkedForRtlScroll = true;
            if (Control.Content is FrameworkElement element)
                element.LayoutUpdated -= SetInitialRtlPosition;
        }

        void OnViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
        {
            ClearRtlScrollCheck();
            Element.SetScrolledPosition(Control.HorizontalOffset, Control.VerticalOffset);

            if (!e.IsIntermediate)
                Element.SendScrollFinished();
        }

        Windows.UI.Xaml.Thickness AddMargin(Thickness original, double left, double top, double right, double bottom)
        {
            return new Windows.UI.Xaml.Thickness(original.Left + left, original.Top + top, original.Right + right, original.Bottom + bottom);
        }

        // UAP ScrollView forces Content origin to be the same as the ScrollView origin.
        // This prevents Forms layout from emulating Padding and Margin by offsetting the origin. 
        // So we must actually set the UAP Margin property instead of emulating it with an origin offset. 
        // Not only that, but in UAP Padding and Margin are aliases with
        // the former living on the parent and the latter on the child. 
        // So that's why the UAP Margin is set to the sum of the Forms Padding and Forms Margin.
        void UpdateContentMargins()
        {
            if (!(Control.Content is FrameworkElement element
                && element is IVisualElementRenderer renderer
                && renderer.Element is View contentView))
                return;

            var margin = contentView.Margin;
            var padding = Element.Padding;
            switch (Element.Orientation)
            {
                case ScrollOrientation.Horizontal:
                    // need to add left/right margins
                    element.Margin = AddMargin(margin, padding.Left, 0, padding.Right, 0);
                    break;
                case ScrollOrientation.Vertical:
                    // need to add top/bottom margins
                    element.Margin = AddMargin(margin, 0, padding.Top, 0, padding.Bottom);
                    break;
                case ScrollOrientation.Both:
                    // need to add all margins
                    element.Margin = AddMargin(margin, padding.Left, padding.Top, padding.Right, padding.Bottom);
                    break;
            }
        }

        void UpdateOrientation()
        {
            //Only update the horizontal scroll bar visibility if the user has not set a desired state.
            if (Element.HorizontalScrollBarVisibility != ScrollBarVisibility.Default)
                return;

            var orientation = Element.Orientation;
            if (orientation == ScrollOrientation.Horizontal || orientation == ScrollOrientation.Both)
            {
                Control.HorizontalScrollBarVisibility = UwpScrollBarVisibility.Auto;
            }
            else
            {
                Control.HorizontalScrollBarVisibility = UwpScrollBarVisibility.Disabled;
            }
        }

        UwpScrollBarVisibility ScrollBarVisibilityToUwp(ScrollBarVisibility visibility)
        {
            switch (visibility)
            {
                case ScrollBarVisibility.Always:
                    return UwpScrollBarVisibility.Visible;
                case ScrollBarVisibility.Default:
                    return UwpScrollBarVisibility.Auto;
                case ScrollBarVisibility.Never:
                    return UwpScrollBarVisibility.Hidden;
                default:
                    return UwpScrollBarVisibility.Auto;
            }
        }

        void UpdateVerticalScrollBarVisibility()
        {
            Control.VerticalScrollBarVisibility = ScrollBarVisibilityToUwp(Element.VerticalScrollBarVisibility);
        }

        void UpdateHorizontalScrollBarVisibility()
        {
            var horizontalVisibility = Element.HorizontalScrollBarVisibility;

            if (horizontalVisibility == ScrollBarVisibility.Default)
            {
                UpdateOrientation();
                return;
            }

            var orientation = Element.Orientation;
            if (orientation == ScrollOrientation.Horizontal || orientation == ScrollOrientation.Both)
                Control.HorizontalScrollBarVisibility = ScrollBarVisibilityToUwp(horizontalVisibility);
        }
    }

    public static class Extensions
    {
        // here to replace unreachable internal xamarin forms class
        internal static void Cleanup(this VisualElement self)
        {
            if (self == null)
                throw new ArgumentNullException("self");

            IVisualElementRenderer renderer = Platform.GetRenderer(self);

            foreach (Element element in self.Descendants())
            {
                var visual = element as VisualElement;
                if (visual == null)
                    continue;

                IVisualElementRenderer childRenderer = Platform.GetRenderer(visual);
                if (childRenderer != null)
                {
                    childRenderer.Dispose();
                    Platform.SetRenderer(visual, null);
                }
            }

            if (renderer != null)
            {
                renderer.Dispose();
                Platform.SetRenderer(self, null);
            }
        }
    }
}

@MitchBomcanhao Thank you a lot))). I appreciate your help. I will try it.

Thanks this is a duplicate of #5652

Was this page helpful?
0 / 5 - 0 ratings