Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Auto resize WebView to fit content

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

like image 646
Dan Avatar asked Apr 23 '18 04:04

Dan


2 Answers

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();
    }
}
like image 181
Anonymous Avatar answered Sep 17 '22 20:09

Anonymous


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"/>
like image 29
cruzier Avatar answered Sep 19 '22 20:09

cruzier