Height of a WebView

MichaelRumplerMichaelRumpler ATMember ✭✭✭✭

I have a StackLayout with a WebView followed by some other controls. The WebView should be as high as its contents.

Unfortunately the WebView doesn't occupy any space at all unless I set its HeightRequest.

For Android I found this SO question which uses the ViewTreeObserver. I tried that in a custom renderer and I do get a height at some point in time, but when I tried to set the Layout of the WebView, then the controls below it will not move down. It I call ForceLayout() on the StackLayout, then the method is called recursively (even though I removed the PreDraw handler already).

private void ViewTreeObserver_PreDraw(object sender, ViewTreeObserver.PreDrawEventArgs e)
{
    var height = Control.ContentHeight;
    if(height > 0 && height != Element.Height && Element.HeightRequest <= 0)
    {
        Control.ViewTreeObserver.PreDraw -= ViewTreeObserver_PreDraw;
        Element.Layout(new Rectangle(Element.X, Element.Y, Element.Width, height));
        ((StackLayout)Element.Parent).ForceLayout();
    }
}

How can I set the Height of the WebView to be as high as its content on Android and iOS?

Best Answers

  • MichaelRumplerMichaelRumpler ATMember ✭✭✭✭
    Accepted Answer

    There were several problems with this approach.

    1) Control.ViewTreeObserver always returns a different object

    Because of that my Control.ViewTreeObserver.PreDraw -= ViewTreeObserver_PreDraw couldn't work. The PreDraw event seems to be useless.
    I worked around that with the following code:

        // initialization in the renderer
        if(Element.HeightRequest < 0)
        {
            //Control.ViewTreeObserver.PreDraw += ViewTreeObserver_PreDraw;
            Control.ViewTreeObserver.AddOnPreDrawListener(new PreDrawListener(this));
        }
    
    private class PreDrawListener : Java.Lang.Object, ViewTreeObserver.IOnPreDrawListener
    {
        WeakReference<CustomWebViewRenderer> _renderer;
    
        public PreDrawListener(CustomWebViewRenderer renderer)
        {
            _renderer = new WeakReference<CustomWebViewRenderer>(renderer);
        }
    
        public bool OnPreDraw()
        {
            if(_renderer.TryGetTarget(out var renderer))
            {
                var Control = renderer.Control;
                var Element = renderer.Element;
    
                var height = Control.ContentHeight;
                if (height > 0 && height != Element.Height && Element.HeightRequest < 0)
                {
                    Control.ViewTreeObserver.RemoveOnPreDrawListener(this);
                    renderer.SetHeight(height);
                }
            }
            return false;
        }
    }
    

    2) The Element.Layout and StackLayout.ForceLayout methods didn't work as expected.

    I needed to call InvalidateMeasure and override GetDesiredSize.

    int minHeight = 0;
    
    private void SetHeight(int height)
    {
        minHeight = (int)Context.ToPixels(height);
        ((IVisualElementController)Element).InvalidateMeasure(InvalidationTrigger.MeasureChanged);
    }
    
    public override SizeRequest GetDesiredSize(int widthConstraint, int heightConstraint)
    {
        var sizeRequest = base.GetDesiredSize(widthConstraint, heightConstraint);
        sizeRequest.Request = new Size(sizeRequest.Request.Width, Math.Max(sizeRequest.Request.Height, minHeight));
        sizeRequest.Minimum = new Size(sizeRequest.Minimum.Width, Math.Max(sizeRequest.Minimum.Height, minHeight));
        return sizeRequest;
    }
    

    Tomorrow I will work on iOS.

  • MichaelRumplerMichaelRumpler ATMember ✭✭✭✭
    Accepted Answer

    And here is the code for iOS:

    private async void RaiseNavigated(WebNavigatedEventArgs args)
    {
        Element.RaiseNavigated(args);
        UpdateCanGoBackForward();
    
        if (args.Result == WebNavigationResult.Success && Element.HeightRequest < 0)
        {
            // set height
            var height = await GetContentHeight();
            if (height > 0 && height != Element.Height)
            {
                minHeight = height;
    
                ((IVisualElementController)Element).InvalidateMeasure(InvalidationTrigger.MeasureChanged);
            }
        }
    }
    
    private async Task<int> GetContentHeight()
    {
        // document.height = null, window.innerHeight = 0, document.body.scrollHeight finally works
        var obj = await Control.EvaluateJavaScriptAsync("document.height || window.innerHeight || document.body.scrollHeight");
        if(obj != null)
        {
            if (int.TryParse(obj?.ToString(), out int height))
                return height;
            //UI.Log($"height={height}");
        }
    
        return -1;
    }
    
    public override SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint)
    {
        var sizeRequest = base.GetDesiredSize(widthConstraint, heightConstraint);
        if (minHeight > 0)
        {
            // if we set the height constraint, then we also disable scrolling
            Control.ScrollView.Bounces = false;
            Control.ScrollView.ScrollEnabled = false;
    
            sizeRequest.Request = new Size(sizeRequest.Request.Width, Math.Max(sizeRequest.Request.Height, minHeight));
            sizeRequest.Minimum = new Size(sizeRequest.Minimum.Width, Math.Max(sizeRequest.Minimum.Height, minHeight));
        }
        return sizeRequest;
    }
    

    (how to call/use RaiseNavigated is a question for a different thread)

Answers

  • MichaelRumplerMichaelRumpler ATMember ✭✭✭✭
    Accepted Answer

    There were several problems with this approach.

    1) Control.ViewTreeObserver always returns a different object

    Because of that my Control.ViewTreeObserver.PreDraw -= ViewTreeObserver_PreDraw couldn't work. The PreDraw event seems to be useless.
    I worked around that with the following code:

        // initialization in the renderer
        if(Element.HeightRequest < 0)
        {
            //Control.ViewTreeObserver.PreDraw += ViewTreeObserver_PreDraw;
            Control.ViewTreeObserver.AddOnPreDrawListener(new PreDrawListener(this));
        }
    
    private class PreDrawListener : Java.Lang.Object, ViewTreeObserver.IOnPreDrawListener
    {
        WeakReference<CustomWebViewRenderer> _renderer;
    
        public PreDrawListener(CustomWebViewRenderer renderer)
        {
            _renderer = new WeakReference<CustomWebViewRenderer>(renderer);
        }
    
        public bool OnPreDraw()
        {
            if(_renderer.TryGetTarget(out var renderer))
            {
                var Control = renderer.Control;
                var Element = renderer.Element;
    
                var height = Control.ContentHeight;
                if (height > 0 && height != Element.Height && Element.HeightRequest < 0)
                {
                    Control.ViewTreeObserver.RemoveOnPreDrawListener(this);
                    renderer.SetHeight(height);
                }
            }
            return false;
        }
    }
    

    2) The Element.Layout and StackLayout.ForceLayout methods didn't work as expected.

    I needed to call InvalidateMeasure and override GetDesiredSize.

    int minHeight = 0;
    
    private void SetHeight(int height)
    {
        minHeight = (int)Context.ToPixels(height);
        ((IVisualElementController)Element).InvalidateMeasure(InvalidationTrigger.MeasureChanged);
    }
    
    public override SizeRequest GetDesiredSize(int widthConstraint, int heightConstraint)
    {
        var sizeRequest = base.GetDesiredSize(widthConstraint, heightConstraint);
        sizeRequest.Request = new Size(sizeRequest.Request.Width, Math.Max(sizeRequest.Request.Height, minHeight));
        sizeRequest.Minimum = new Size(sizeRequest.Minimum.Width, Math.Max(sizeRequest.Minimum.Height, minHeight));
        return sizeRequest;
    }
    

    Tomorrow I will work on iOS.

  • MichaelRumplerMichaelRumpler ATMember ✭✭✭✭
    Accepted Answer

    And here is the code for iOS:

    private async void RaiseNavigated(WebNavigatedEventArgs args)
    {
        Element.RaiseNavigated(args);
        UpdateCanGoBackForward();
    
        if (args.Result == WebNavigationResult.Success && Element.HeightRequest < 0)
        {
            // set height
            var height = await GetContentHeight();
            if (height > 0 && height != Element.Height)
            {
                minHeight = height;
    
                ((IVisualElementController)Element).InvalidateMeasure(InvalidationTrigger.MeasureChanged);
            }
        }
    }
    
    private async Task<int> GetContentHeight()
    {
        // document.height = null, window.innerHeight = 0, document.body.scrollHeight finally works
        var obj = await Control.EvaluateJavaScriptAsync("document.height || window.innerHeight || document.body.scrollHeight");
        if(obj != null)
        {
            if (int.TryParse(obj?.ToString(), out int height))
                return height;
            //UI.Log($"height={height}");
        }
    
        return -1;
    }
    
    public override SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint)
    {
        var sizeRequest = base.GetDesiredSize(widthConstraint, heightConstraint);
        if (minHeight > 0)
        {
            // if we set the height constraint, then we also disable scrolling
            Control.ScrollView.Bounces = false;
            Control.ScrollView.ScrollEnabled = false;
    
            sizeRequest.Request = new Size(sizeRequest.Request.Width, Math.Max(sizeRequest.Request.Height, minHeight));
            sizeRequest.Minimum = new Size(sizeRequest.Minimum.Width, Math.Max(sizeRequest.Minimum.Height, minHeight));
        }
        return sizeRequest;
    }
    

    (how to call/use RaiseNavigated is a question for a different thread)

  • AdamZucchiAdamZucchi USUniversity, Developer Group Leader ✭✭
    edited August 22

    @MichaelRumpler - I am attempting to get a Xamarin.Forms WebView to also expand its height to 100% based on the height of its content (I don't want to set a HeightRequest) and came across your post.

    I am starting down this path on iOS first, but one issue I am running into is with the override you use "GetDesiredSize". I'm getting an error in my iOS WebView custom renderer class that this method cannot be overridden. How did you accomplish this?

    Thanks!

  • MichaelRumplerMichaelRumpler ATMember ✭✭✭✭

    @AdamZucchi

    Ah, yes, I see. Apparently Xamarin forgot to make X.F.Platform.iOS.WebViewRenderer.GetDesiredSize virtual like they did in VisualElementRenderer.
    I'm afraid you'll have to file a bug for that and hope that they'll change it soon. A PR from you might accelerate things.

    I don't have that problem because I don't inherit from Xamarin's WebViewRenderer, but ViewRenderer<CustomWebView, WKWebView> instead. There the method is virtual.

  • AdamZucchiAdamZucchi USUniversity, Developer Group Leader ✭✭

    @MichaelRumpler Ahhhh, so that's where I went wrong! Thanks, I'll look into using ViewRenderer instead.

    I was hoping that I could get this WebView height behavior for free without a custom renderer by using a Grid as described in Xamarin's WebView documentation under the "Layout" section, but unfortunately that is only to get the WebView to render without setting a HeightRequest, it does not make the WebView assume 100% height of its content :(

    Thanks again for the reply and posting your findings above!

  • KnotsKnots USMember
    edited October 16

    @MichaelRumpler
    Nice going! I found some other ways to determine the height dynamically, but this is the first I've seen using Android's pre-draw renderer.
    I'm wondering if it's just on my side, but when I rotate the device, it throws a NullReferenceException. Does your app handle the orientation changes well?

    Thanks in advance.

  • KnotsKnots USMember

    It turns out the null pointer was not the issue here.

    When changing the orientation, the height of the control is increased if required, but never decreased. Do you know any way to properly recalculate it when the orientation changed?

    I've tried to add the PreDrawListener again in the 'OnConfigurationChanged', but that doesn't help. First setting the requested height to a negative number doesn't work either. All other height-related properties are read-only (get-only).

    So while the code-snippet above works fast, it leaves a - sort of - margin at the bottom of the webview when going from portrait to landscape. (On a phone.)

    Regards.

  • MichaelRumplerMichaelRumpler ATMember ✭✭✭✭

    @Knots You're right, it doesn't work when you rotate the phone. I didn't notice that before. When I open the page in landscape and then rotate to portrait, it also doesn't increase the height. That's a bigger issue because the bottom of the html is not visible.

    One other change I did after posting the code here is in the OnPreDraw method:

            public bool OnPreDraw()
            {
                if (_renderer.TryGetTarget(out var renderer))
                {
                    var Control = renderer.Control;
                    var Element = renderer.Element;
    
                    if (Element != null && Control != null)
                    {
                        var height = Control.ContentHeight;
                        if (height > 0)
                        {
                            Control.ViewTreeObserver.RemoveOnPreDrawListener(this);
    
                            if (height != Element.Height && Element.HeightRequest < 0)
                                renderer.SetHeight(height);
                        }
                    }
                }
                return false;
            }
    

    I didn't remove the PreDrawListener in every case before. But this doesn't fix the problem with the rotation.

Sign In or Register to comment.