Forms does not currently have a mechanism to size a WebView to fit its content.
Add a bindable property SizeToContent to WebView:
static readonly BindablePropertyKey SizeToContentProperty =
BindableProperty.CreateReadOnly("SizeToContent", typeof(bool), typeof(WebView), false);
When this property is true
, the WebView should attempt to size itself to the minimum size necessary to fit the web content being displayed. Auto-sizing should take place only along unspecified dimensions; e.g., if a WidthRequest or HorizontalOptions.Fill are in play, the WebView should honor those specifications and only size itself vertically to fit the content.
This is a new property/behavior, so there should not be any backward compatibility issues.
This may require use of features in https://github.com/xamarin/Xamarin.Forms/issues/1699 in order to query the document
object for size information.
We implemented this for iOS & Android using platform APIs after javascript proved to be unreliable. Off the top of my head, js added a 50-100ms lag which caused an unsightly flash before resizing, DOM sizes were frequently a few px adrift of the rendered bitmap, and detecting changes after the initial load require polling (or a ResizeObserver polyfill.)
It turns out that observing size changes of the native webview is simple and resizing is immediate. The snippets below are from our custom renderers which implement a new webview using WKWebView
instead of UIWebView
, but I think the concept would apply to the stock WebView
renderers as well.
On iOS we used KVO to watch for changes to the webview's underlying scrollview and post changes to the element:
void ConfigureSizeChangeObserver()
{
if (Element.AutosizeHeight) {
SetupContentSizeObserver();
} else {
RemoveContentSizeObserver();
}
}
void SetupContentSizeObserver()
{
if (kvoToken != null) {
return;
}
kvoToken = Control.AddObserver("scrollView.contentSize", NSKeyValueObservingOptions.OldNew, ScrollViewContentSizeChanged);
}
void RemoveContentSizeObserver()
{
kvoToken?.Dispose();
kvoToken = null;
}
void ScrollViewContentSizeChanged(NSObservedChange nsObservedChange)
{
if (nsObservedChange.NewValue.IsEqual(nsObservedChange.OldValue)) {
return;
}
var newSize = ((NSValue)nsObservedChange.NewValue).CGSizeValue;
Element.OnContentSizeChange(new Size(newSize.Width, newSize.Height));
}
On Android we created a subclass of Android.Webkit.WebView
that raises an event if the size changes after invalidation:
public class DynamicSizeWebView : Android.Webkit.WebView
{
public EventHandler SizeChanged;
bool _observeSizeChanges;
public bool ObserveSizeChanges {
get => _observeSizeChanges;
set {
if (_observeSizeChanges != value) {
_observeSizeChanges = value;
if (_observeSizeChanges) {
OnSizeChange();
}
}
}
}
public DynamicSizeWebView(Context context) : base(context) { }
protected DynamicSizeWebView(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) { }
int _previousMesuredHeight = 0;
public override void Invalidate()
{
base.Invalidate();
if (ObserveSizeChanges) {
OnSizeChange();
}
}
void OnSizeChange()
{
var newHeight = ContentHeight;
if (newHeight > 0 && _previousMesuredHeight != newHeight) {
SizeChanged?.Invoke(this, EventArgs.Empty);
_previousMesuredHeight = newHeight;
}
}
}
We then use the custom webview like this:
protected override void OnElementChanged(ElementChangedEventArgs<EnhancedWebView> e)
{
base.OnElementChanged(e);
...
if (e.NewElement is IEnhancedWebViewController newElement) {
if (Control == null) {
var webView = new DynamicSizeWebView(Forms.Context);
webView.SetWebViewClient(new Client(this));
webView.Settings.JavaScriptEnabled = true;
webView.SizeChanged += WebView_SizeChanged;
SetNativeControl(webView);
}
...
}
}
void WebView_SizeChanged(object sender, EventArgs e)
{
Element.OnContentSizeChange(new Size(Control.MeasuredWidth, Control.ContentHeight));
}
I haven't used Xamarin.Forms.WebView
or looked at the source in a long time, but I don't think this would be hard to backport. I might be able to take a stab in the next week or two.
@michaeldwan Awesome. If there's a closer-to-native way to make it work, then that's what we'd like to use. I don't want to depend on JS for anything unless we absolutely have to.
Yeah definetly not using JS IMO. Do we have a clue if this is possible in all platforms? we should investigate .
I have code for Android, iOS, and Mac. I've never done UWP, but we could always fallback to JS if there's no better native API. I'll put a PR together for the platforms I know then investigate the others. cc @davidortinau
@michaeldwan just doing a quick checkup to see if you are still working on this. If not we can add it back to the available pool.
Bump @michaeldwan
@michaeldwan I've implemented a custom renderer for this problem based on answers on the Xamarin forums. I've noticed that the webview's height is too long compared to the content in some projects. The weird thing is that I cannot reproduce this in a seperate project. Is this a bug in Xamarin Android itself?
@michaeldwan Any progress? Thanks!
I’m sorry for the slow reply. I’m not going to be able to get this done anytime soon. If this is still open in a few months I’ll try and wrap it up.
Bump @michaeldwan
Are there any plans to add this to the roadmap?
@michaeldwan, is there any news on this? If you have an implementation and need help for testing, reviewing, etc, please let us know.
Has there been updates on it?
Please close this issue..!
@PaulsonMac close the issue? is it resolved?
@Ali-Syed, actually I wanted it to be resolved.. Sorry for the typo
Any news about this one? :)
Hi @almirvuk, I ended up using this for android. Works fine for me..!
using System;
using System.Threading.Tasks;
using Android.Content;
using Android.Webkit;
using MyProject.Controls;
using MyProject.Android.Renderers;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using WebView = Android.Webkit.WebView;
[assembly: ExportRenderer(typeof(ExtendedWebView), typeof(ExtendedWebViewRenderer))]
namespace MyProject.Android.Renderers
{
public class ExtendedWebViewRenderer : WebViewRenderer
{
public static int _webViewHeight;
static ExtendedWebView _xwebView = null;
WebView _webView;
public ExtendedWebViewRenderer(Context context) : base(context)
{
}
class ExtendedWebViewClient : WebViewClient
{
WebView _webView;
public async override void OnPageFinished(WebView view, string url)
{
try
{
_webView = view;
if (_xwebView != null)
{
view.Settings.JavaScriptEnabled = true;
await Task.Delay(100);
string result = await _xwebView.EvaluateJavaScriptAsync("(function(){return document.body.scrollHeight;})()");
_xwebView.HeightRequest = Convert.ToDouble(result);
}
base.OnPageFinished(view, url);
}
catch(Exception ex)
{
Console.WriteLine($"{ex.Message}");
}
}
}
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.WebView> e)
{
base.OnElementChanged(e);
_xwebView = e.NewElement as ExtendedWebView;
_webView = Control;
if (e.OldElement == null)
{
_webView.SetWebViewClient(new ExtendedWebViewClient());
}
}
}
}
@PaulsonMac thanks, and for ios?
You're lucky, I worked hard for this many days.. Here you go..!
using System;
using Foundation;
using MyProject.Controls;
using MyProject.iOS.Renderers;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(ExtendedWebView), typeof(ExtendedWebViewRenderer))]
namespace MyProject.iOS.Renderers
{
class ExtendedWebViewRenderer : WebViewRenderer
{
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
try
{
base.OnElementChanged(e);
if (NativeView != null)
{
var webView = (UIWebView)NativeView;
webView.Opaque = false;
webView.BackgroundColor = UIColor.Clear;
}
Delegate = new ExtendedUIWebViewDelegate(this);
}
catch(Exception ex)
{
Console.WriteLine("Error at ExtendedWebViewRenderer OnElementChanged: " + ex.Message);
}
}
}
class ExtendedUIWebViewDelegate : UIWebViewDelegate
{
ExtendedWebViewRenderer webViewRenderer;
public ExtendedUIWebViewDelegate(ExtendedWebViewRenderer _webViewRenderer = null)
{
webViewRenderer = _webViewRenderer ?? new ExtendedWebViewRenderer();
}
public override async void LoadingFinished(UIWebView webView)
{
try
{
var _webView = webViewRenderer.Element as ExtendedWebView;
if (_webView != null)
{
await System.Threading.Tasks.Task.Delay(100); // wait here till content is rendered
_webView.HeightRequest = (double)webView.ScrollView.ContentSize.Height;
}
}
catch(Exception ex)
{
Console.WriteLine("Error at ExtendedWebViewRenderer LoadingFinished: " + ex.Message);
}
}
}
}
You must set heightrequest to some value like 40 in XAML for iOS alone..
@almirvuk , did those codes work..?
Yes, thank you! It works properly for dynamic height issue.
Glad to know that.. Thanks!
Thanks for posting the code guys. UWP would be great too - will try and give it a shot.
Here's where I landed. Know very little about UWP so please feel free to suggest improvements, but this seems to work fairly well. Note that in my case I'm passing an HTML string from my custom control to render/"navigate" to, if you're going to an actual page just use Control.Navigate(Uri source) instead.
public class ExtendedWebViewRenderer : ViewRenderer<ExtendedWebView, Windows.UI.Xaml.Controls.WebView>
{
protected override void OnElementChanged(ElementChangedEventArgs<ExtendedWebView> e)
{
try
{
base.OnElementChanged(e);
if (e.OldElement != null && Control != null)
{
Control.NavigationCompleted -= OnWebViewNavigationCompleted;
}
if (e.NewElement != null)
{
if (Control == null)
{
SetNativeControl(new Windows.UI.Xaml.Controls.WebView());
}
Control.NavigationCompleted += OnWebViewNavigationCompleted;
}
}
catch (Exception ex)
{
Console.WriteLine("Error at ExtendedWebViewRenderer OnElementChanged: " + ex.Message);
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (Element is ExtendedWebView element && e.PropertyName.Equals(nameof(ExtendedWebView.Html)) && !string.IsNullOrWhiteSpace(element.Html))
Control.NavigateToString(element.Html);
}
private async void OnWebViewNavigationCompleted(WebView sender, WebViewNavigationCompletedEventArgs args)
{
if (!args.IsSuccess)
return;
var heightString = await Control.InvokeScriptAsync("eval", new[] {"document.body.scrollHeight.toString()" });
if (int.TryParse(heightString, out int height))
{
Element.HeightRequest = height;
}
var widthString = await Control.InvokeScriptAsync("eval", new[] {"document.body.scrollWidth.toString()" });
if (int.TryParse(widthString, out int width))
{
Element.WidthRequest = width;
}
}
}
Nice, I am not using UWP but I am sure this will be helpful for other devs. Thank you!
It does not work for me, leave a blank space after the content of the webview, could you help me?
Glad to know that.. Thanks!
help me!! :(
KVO doesn't work. If the web page resizes its height so that it's smaller than the UIWebView frame height, then you won't be notified of this change. I'm surprised Apple does not support this feature out of the box, and so we have to find a hacky solution that doesn't work.
I think the scope of this enhancement needs to be discussed. We could use JavaScript to get height changes, but this will only work as long as JS works. In my case, I'd need change notification for HTML that I supply as opposed to a remote URL, so JS is more likely to work for me. I'm assuming this is true for most people as well. We could still provide a JS implementation and document its reliability accordingly.
That said, Xamarin should treat this as a high priority item. If CollectionView scrolling does not improve for me (#7152), using a WebView to load complex attributed labels is a better choice when constructing cells instead of Label/Span work. Of course, there is also #4527.
@almirvuk @PaulsonMac any idea how to update the iOS code to use WkWebViewRenderer instead? UIWebView will be obsolete soon as Apple will begin rejecting apps that use it.
My current attempt is not working:
public class ExtendedWebViewRenderer : WkWebViewRenderer
{
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
try
{
base.OnElementChanged(e);
if (NativeView != null)
{
var webView = (WKWebView)NativeView;
webView.Opaque = false;
webView.BackgroundColor = UIColor.Clear;
}
NavigationDelegate = new ExtendedNavigationDelegate(this);
}
catch (Exception ex)
{
Console.WriteLine("Error at ExtendedWebViewRenderer OnElementChanged: " + ex.Message);
}
}
}
public class ExtendedNavigationDelegate : WKNavigationDelegate
{
private readonly ExtendedWebViewRenderer webViewRenderer;
public ExtendedNavigationDelegate(ExtendedWebViewRenderer webViewRenderer = null)
{
this.webViewRenderer = webViewRenderer ?? new ExtendedWebViewRenderer();
}
public override async void DidFinishNavigation(WKWebView webView, WKNavigation navigation)
{
try
{
var extWebView = webViewRenderer.Element as ExtendedWebView;
if (extWebView != null)
{
await System.Threading.Tasks.Task.Delay(100); // wait here till content is rendered
extWebView.HeightRequest = (double)webView.ScrollView.ContentSize.Height;
webView.ScrollView.FlashScrollIndicators(); // no direct way to ALWAYS show the scrollbar, so just going to flash it upon load to show user it is there and scrollable
}
}
catch (Exception ex)
{
Console.WriteLine("Error at ExtendedWebViewRenderer LoadingFinished: " + ex.Message);
}
}
}
Hi @mzhukovs, maybe you should follow this thread: https://github.com/xamarin/Xamarin.Forms/issues/7323
@almirvuk thanks but that has nothing to do with sizing the WKWebView to autofit its content, unless I am missing something?
Sorry I was thinking that you asked about WKWebView support regarding that Apple publishing issue notice.
@PaulsonMac this solution does not work on iOS (anymore), on Xamarin.Forms 4.2.0.709249, WebView with dynamic content height is not spreading in height, on XF 3.6 it still works like a charm.
:( - if set default height in code, then it work
Is there any update for the iOS implementation? I am currently stuck with the same resize to contents issue migrating away from UIWebView
@Predatorie Xamarin Forms implemented HTML text display with Label in recent release. You just need to apply TextType = Html.
Please refer: https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/text/label#display-html
Could be a different solution than what discussed in this thread.
For UWP, I started with mzhukovs' solution above, but it wasn't loading some of the local assets (e.g. a style sheet file) into the page due to not knowing the base URL. So my renderer inherits from Xamarin.Forms.Platform.UWP.WebViewRenderer and simply does the size update upon NavigationCompleted.
I guess I will include the whole class for completeness.
ExtendedWebView (in portable project) looks like this:
public class ExtendedWebView : WebView { }
(XAML)
<controls:ExtendedWebView x:Name="wvApplication" WidthRequest="-1" HeightRequest="-1"
VerticalOptions="StartAndExpand" HorizontalOptions="FillAndExpand">
<WebView.Source>
<HtmlWebViewSource Html="{Binding ApplicationHtml}" />
</WebView.Source>
</controls:ExtendedWebView>
[assembly: ExportRenderer(typeof(ExtendedWebView), typeof(ExtendedWebViewRenderer))]
namespace MyNamespace
{
public class ExtendedWebViewRenderer : Xamarin.Forms.Platform.UWP.WebViewRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
{
try
{
base.OnElementChanged(e);
if (e.OldElement != null && Control != null)
{
Control.NavigationCompleted -= Control_NavigationCompleted;
}
if (e.NewElement != null && Control != null)
{
Control.NavigationCompleted += Control_NavigationCompleted;
}
} catch (Exception ex)
{
Console.WriteLine($"Error at ExtendedWebViewRenderer {nameof(OnElementChanged)}: " + ex.Message);
}
}
private async void Control_NavigationCompleted(Windows.UI.Xaml.Controls.WebView sender, Windows.UI.Xaml.Controls.WebViewNavigationCompletedEventArgs args)
{
if (args.IsSuccess)
{
await UpdateSize();
}
}
private async Task UpdateSize()
{
try
{
var heightString = await Control.InvokeScriptAsync("eval", new[] { "document.body.scrollHeight.toString()" });
if (int.TryParse(heightString, out int height))
{
Element.HeightRequest = height;
}
var widthString = await Control.InvokeScriptAsync("eval", new[] { "document.body.scrollWidth.toString()" });
if (int.TryParse(widthString, out int width))
{
Element.WidthRequest = width;
}
} catch (Exception ex)
{
Console.WriteLine($"Error at ExtendedWebViewRenderer {nameof(UpdateSize)}: " + ex.Message);
}
}
}
}
Two years later, this still remains an issue in Xamarin Forms.
Did anyone found a way to implement this on iOS with WkWebViewRenderer? @mzhukovs :) ?
I have tried with CustomWKNavigationDelegate
in WkWebViewRenderer
and delegate methods but the DidFinishNavigation
(...) is never called. I am using "local", Html data as a source for WebView
... Any guidance for this? Thanks!
This is what i have and it works pretty good.
Hi @eevahr, I am using similar approach but DidFinishNavigation is never called with local html as a source for a webview. Do you use an url with webview or?
I get the html from an api and set the source manually. So not local but no url is loaded either. I'm guessing that you are familiar with custom renderers and what not but are you sure you're using the renderer? :)
@eevahr Same as me... Yes, because I get breakpoints hit in Renderer and Custom delegate ctors. :)
i want this too!
Hi guys! Has anyone tried this solution before - https://stackoverflow.com/a/56304192/10329199 in WKWebView to dynamically size content...??
@PaulsonMac I have some kind of weird bug... the DidFinishNavigation is never triggered with local HTML content. :(
Ultimately we had success using a hybrid approach. For iOS we are using the HTML feature of the Label component. It automatically adjusts its height properly and accepts a wide range of HTML tags. It works great. Not so much for Android it has a far more limited set of accepted HTML and does not adjust the height well for the content. In this case we instead use the dynamic height custom renderer for the webview. So far this has worked for us.
Is there any update on when this is going to be implemented?
source example in android?
Hi @almirvuk, I ended up using this for android. Works fine for me..!
using System; using System.Threading.Tasks; using Android.Content; using Android.Webkit; using MyProject.Controls; using MyProject.Android.Renderers; using Xamarin.Forms; using Xamarin.Forms.Platform.Android; using WebView = Android.Webkit.WebView; [assembly: ExportRenderer(typeof(ExtendedWebView), typeof(ExtendedWebViewRenderer))] namespace MyProject.Android.Renderers { public class ExtendedWebViewRenderer : WebViewRenderer { public static int _webViewHeight; static ExtendedWebView _xwebView = null; WebView _webView; public ExtendedWebViewRenderer(Context context) : base(context) { } class ExtendedWebViewClient : WebViewClient { WebView _webView; public async override void OnPageFinished(WebView view, string url) { try { _webView = view; if (_xwebView != null) { view.Settings.JavaScriptEnabled = true; await Task.Delay(100); string result = await _xwebView.EvaluateJavaScriptAsync("(function(){return document.body.scrollHeight;})()"); _xwebView.HeightRequest = Convert.ToDouble(result); } base.OnPageFinished(view, url); } catch(Exception ex) { Console.WriteLine($"{ex.Message}"); } } } protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.WebView> e) { base.OnElementChanged(e); _xwebView = e.NewElement as ExtendedWebView; _webView = Control; if (e.OldElement == null) { _webView.SetWebViewClient(new ExtendedWebViewClient()); } } } }
I used this code, but my html content is not display, i have debug this code, but there are no exact url pass in url string, pls tell me.
Is there any working solution for WkWebview?
I need this to be implemented before going live in a few days. Any solutions please?
@PaulsonMac ... if you look at my comment we have successfully used a hybrid approach. In the Xamarin Forms code based on the platform we will either use the built in HTML capabilities of the Label component or, for Android, use the custom control solution as mentioned above.
Thanks for your response. But since we have images in the HTML, I think we can't proceed with the Xamarin Label HTML text type.
@almirvuk , DidFinishNavigation is not getting hit for me as well. Can you please let me know how you resolved this?
Thank you JESUS! Got it working in iOS!
[assembly: ExportRenderer(typeof(ExtendedWebView), typeof(ExtendedWebViewRenderer))]
namespace Project.iOS.Renderers
{
class ExtendedWebViewRenderer : WkWebViewRenderer
{
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
try
{
base.OnElementChanged(e);
NavigationDelegate = new ExtendedWKWebViewDelegate(this);
// For fixing white flash issue in webview on dark themes/backgrounds and disable webview's scrolling
if (NativeView != null)
{
var webView = (WKWebView)NativeView;
webView.Opaque = false;
webView.BackgroundColor = UIColor.Clear;
webView.ScrollView.ScrollEnabled = false;
}
}
catch (Exception ex)
{
Console.WriteLine("Error at ExtendedWebViewRenderer OnElementChanged: " + ex.Message);
}
}
}
class ExtendedWKWebViewDelegate : WKNavigationDelegate
{
ExtendedWebViewRenderer webViewRenderer;
public ExtendedWKWebViewDelegate(ExtendedWebViewRenderer _webViewRenderer)
{
webViewRenderer = _webViewRenderer ?? new ExtendedWebViewRenderer();
}
public override async void DidFinishNavigation(WKWebView webView, WKNavigation navigation)
{
var wv = webViewRenderer.Element as ExtendedWebView;
if (wv != null && webView != null)
{
await System.Threading.Tasks.Task.Delay(100); // wait here till content is rendered
if (webView.ScrollView != null && webView.ScrollView.ContentSize != null)
{
wv.HeightRequest = (double)webView.ScrollView.ContentSize.Height;
}
}
}
Previously I had white screen in webview area and also dynamic height was not working. But now this works fine!
Most helpful comment
Hi @almirvuk, I ended up using this for android. Works fine for me..!