9

I am working on a Xamarin.Forms PCL project that displays posts in the form of a WebView so I add clickable content such as hashtags.

The problem I am having is the WebView doesn't adjust to the size of its content. The WebView does not load an actual site I am binding the HTML to each post in a ListView using

"<html><p>" + body + "</p></html>"

I tried researching for custom renderers and followed this tutorial and ended up with the following code

For Android I used

#pragma warning disable CS0618 // Type or member is obsolete
public class CustomWebViewAndroid : WebViewRenderer
{
    static CustomWebView _xwebView = null;
    WebView _webView;            

    class ExtendedWebViewClient : Android.Webkit.WebViewClient
    {
        public override async void OnPageFinished (WebView view, string url)
        {
            if (_xwebView != null) {
                int i = 10;
                while (view.ContentHeight == 0 && i-- > 0) // wait here till content is rendered
                    await System.Threading.Tasks.Task.Delay (100);
                _xwebView.HeightRequest = view.ContentHeight;
            }
            base.OnPageFinished (view, url);
        }
    }

    protected override void OnElementChanged (ElementChangedEventArgs<Xamarin.Forms.WebView> e)
    {
        base.OnElementChanged (e); 
        _xwebView = e.NewElement as CustomWebView;
        _webView = Control;

        if (e.OldElement == null) {                
            _webView.SetWebViewClient (new ExtendedWebViewClient ());
        }         

    }       
}

And for iOS

public class CustomWebViewiOS : WebViewRenderer
{
    protected override void OnElementChanged(VisualElementChangedEventArgs e)
    {
        base.OnElementChanged(e);
        Delegate = new CustomWebViewDelegate(this);
    }
}

public class CustomWebViewDelegate : UIWebViewDelegate
{
    CustomWebViewiOS webViewRenderer;

    public CustomWebViewDelegate(CustomWebViewiOS _webViewRenderer = null)
    {
        webViewRenderer = _webViewRenderer ?? new CustomWebViewiOS();
    }

    public override async void LoadingFinished(UIWebView webView)
    {
        var wv = webViewRenderer.Element as CustomWebView;
        if (wv != null)
        {
            await System.Threading.Tasks.Task.Delay(100); // wait here till content is rendered
            wv.HeightRequest = (double)webView.ScrollView.ContentSize.Height;
        }
    }
}

And then applied it in XAML as so

<StackLayout HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
    <local:CustomWebView HeightRequest="20" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
        <local:CustomWebView.Source>
            <HtmlWebViewSource Html="{Binding HtmlContent}"/>
        </local:CustomWebView.Source>
    </local:CustomWebView>
</StackLayout>

On Android it clips off half the post's body.

Android Example

On iOS it doesn't space the posts correctly and cuts off the timestamp of the one above it. My guess is the height of the ViewCell adjusts to the content but doesn't take into consideration the other posts. Also for iOS if the text takes two lines it requires you to scroll to view the rest.

iOS Example

Dan
  • 1,100
  • 2
  • 13
  • 36

2 Answers2

7

Firstly you should set the ListView's HasUnevenRows to true, then I recommend you to use Grid to wrap your webView and remove the HeightRequest in XAML. You can refer to my XAML:

<local:MyListView x:Name="MyListView" HasUnevenRows="True">
    <local:MyListView.ItemTemplate>
        <DataTemplate>
            <ViewCell>
                <Grid>
                    <local:AutoWebView>
                        <local:AutoWebView.Source>
                            <HtmlWebViewSource Html="{Binding}"/>
                        </local:AutoWebView.Source>
                    </local:AutoWebView>
                </Grid>
            </ViewCell>
        </DataTemplate>
    </local:MyListView.ItemTemplate>
</local:MyListView>

For your Android renderer:

Do not use static identifier for _xwebView and in LoadingFinished() when we get the actual HeightRequest, refresh the ViewCell using ForceUpdateSize() like:

public class MyWebViewAndroidRenderer : WebViewRenderer
{
    protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        base.OnElementPropertyChanged(sender, e);

        Control.SetWebViewClient(new ExtendedWebViewClient(Element as AutoWebView));

    }

    class ExtendedWebViewClient : Android.Webkit.WebViewClient
    {
        AutoWebView xwebView;
        public ExtendedWebViewClient(AutoWebView webView)
        {
            xwebView = webView;
        }

        async public override void OnPageFinished(Android.Webkit.WebView view, string url)
        {
            if (xwebView != null)
            {
                int i = 10;
                while (view.ContentHeight == 0 && i-- > 0) // wait here till content is rendered
                    await System.Threading.Tasks.Task.Delay(100);
                xwebView.HeightRequest = view.ContentHeight;
                // Here use parent to find the ViewCell, you can adjust the number of parents depending on your XAML
                (xwebView.Parent.Parent as ViewCell).ForceUpdateSize();
            }

            base.OnPageFinished(view, url);
        }
    }
}

For iOS:

We also need to refresh the cell when the webView finished loading:

public override void LoadingFinished(UIWebView webView)
{
    var wv = webViewRenderer.Element as AutoWebView;
    if (wv.HeightRequest < 0)
    {
        wv.HeightRequest = (double)webView.ScrollView.ContentSize.Height;
        (wv.Parent.Parent as ViewCell).ForceUpdateSize();
    }
}
Ax1le
  • 6,563
  • 2
  • 14
  • 61
  • The Android version works perfectly except when I tried running it on Xamarin Live Player for the iOS version the height stays at 0 never showing the content. – Dan Apr 25 '18 at 06:42
  • It works well on my silde on iOS. I made a sample [here](https://github.com/landl0526/WebViewListViewDemo). Try to check this one. – Ax1le Apr 25 '18 at 13:27
  • Did you try it on Xamarin Live Player? – Dan Apr 26 '18 at 01:15
  • 2
    Xamarin Live Player is not designed for debugging, it just shows some views temporary. There're several limitations on it especially using renderer. But this will work fine on simulators or real devices. Please read this [documentation](https://learn.microsoft.com/en-us/xamarin/tools/live-player/) for more details about Xamarin Live Player and its limitations. – Ax1le Apr 26 '18 at 01:29
  • 1
    Unfortunately, this solution uses UIWebView which is meanwhile depricated by Apple – thomasgalliker Jul 02 '20 at 06:49
4

To display HTML content you can use this it works like charm for me, we don't need to use renderers to fit the content size.

<Label BackgroundColor="White" Text="{Binding description}" TextType="Html"/>
cruzier
  • 358
  • 2
  • 14
  • I like this as a solution for simple text. But if you have to display more complex HTML, or you don't know what HTML content is going to be displayed, it won't work. – pseudoabdul Feb 05 '21 at 06:03
  • 1
    @pseudoabdul, Yes for formatting text only we can use this. if contains other content it won't work,, – cruzier Feb 17 '21 at 13:39
  • This will work fine on iOS. On Android, you've got a very limited subset of HTML tags supported: https://stackoverflow.com/questions/33691287/show-in-textview-html-with-inline-styles –  Apr 28 '22 at 11:25