Is it possible to stop the first Entry getting Focus in a ScrollView in Xamarin Forms

心已入冬 提交于 2020-08-23 12:18:31

问题


In my application I have the following situation:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:conv="clr-namespace:NumericTextBoxTest.Converters;assembly=NumericTextBoxTest"
             xmlns:numericTextBox="clr-namespace:Syncfusion.SfNumericTextBox.XForms;assembly=Syncfusion.SfNumericTextBox.XForms"
             x:Class="NumericTextBoxTest.MainPage">
  <ScrollView>
    <StackLayout>    
      <Entry/>
      <Entry/>
      <Entry/>
      <Entry/>
      <Entry/>
      <Entry/>
      <Entry/>
      <Entry/>
      <Entry/>
      <Entry/>
    </StackLayout>

  </ScrollView>
</ContentPage>

Now If I click the blank space at the bottom (below the entries) i.e. the ScrollView the first Entry in the ScrollView will gain focus.

Very annoying If I am changing the value on the first Entry and trying to unfocus that Entry to set the value.

Is it possible to stop this behavior?


回答1:


Now If I click the blank space at the bottom (below the entries) i.e. the ScrollView the first Entry in the ScrollView will gain focus.

In UWP it is by design that when the StackLayout gets tapped the system will search element for-each in the StackLayout until the first one which can be focused on. As a workaround to solve this issue, you can place an invisible buton in the top of StackLayout.

<ScrollView>
        <StackLayout>
            <Button HeightRequest="0" WidthRequest="1" />
            <Entry  />
            ....
            <Entry />
        </StackLayout>
    </ScrollView>

The button will be focused on when StackLayout was tapped. The Entry will not be focused




回答2:


In the end I actually ended up overriding the default ScrollViewRenderer as mentioned in this comment:

https://github.com/microsoft/microsoft-ui-xaml/issues/597#issuecomment-513804526

My ScrollViewRenderer on UWP looked like this:

using Xamarin.Forms;
using Xamarin.Forms.Platform.UWP;
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 FieldStrikeMove.Forms.CustomControls;

//https://github.com/microsoft/microsoft-ui-xaml/issues/597

//https://github.com/xamarin/Xamarin.Forms/blob/f17fac7b9e2225b1bfe9e94909d2b954106f8f1f/Xamarin.Forms.Platform.UAP/ScrollViewRenderer.cs
//07/01/20

[assembly: ExportRenderer(typeof(ExtendedScrollView), typeof(MyApp.UWP.CustomRenderers.Controls.ScrollViewRenderer))]
[assembly: ExportRenderer(typeof(ScrollView), typeof(MyApp.UWP.CustomRenderers.Controls.ScrollViewRenderer))]
namespace MApp.UWP.CustomRenderers.Controls
{
    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 (Element != null)
                Element.PropertyChanged -= OnContentElementPropertyChanged;

            if (ContainerElement != null)
                ContainerElement.LayoutUpdated -= SetInitialRtlPosition;

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



回答3:


If I click the blank space at the bottom (below the entries)

you can stretch your stacklayout and then it will receive focus instead scrollview.

And you can also set backgroundcolor for stacklayout to make sure that stacklayout is stretched



来源:https://stackoverflow.com/questions/42231163/is-it-possible-to-stop-the-first-entry-getting-focus-in-a-scrollview-in-xamarin

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!