3
votes

I'm currently developing an app in Xamarin Forms with an Android background so I wanted to create an Android app first and an iOS app later.

I'm new to Xamarin Forms and I'm struggling on how to update a ProgressBar from a WebView using a custom renderer for the WebView.

In Android, you can do something like this, with the ProgressBar and WebView that are in the main_layout.xml

public class MainActivity extends Activity {

    private ProgressBar progressBar;
    private WebView webView;    

    @Override
    protected void onCreate(Bundle savedInstanceState) {     
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_layout);

        progressBar = (ProgressBar) findViewById(R.id.progress);
        webView = (AdvancedWebView) findViewById(R.id.webView);
        // webview initialisation
        webView.setWebChromeClient(new WebChromeClient(){
            @Override
            public void onProgressChanged(WebView view, int newProgress) {
                // update progressbar progress
                progressBar.setProgress(newProgress);
           }
       });
        webView.setWebViewClient(new WebViewClient(this) {
            @Override
            public void onPageFinished(WebView view, String url) {
                // hide progressbar when it's done
                progressBar.setVisibility(View.GONE);
            }
        });
    }
}

In Xamarin Forms I have this layout in MainPage.xaml in the shared project

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
x:Class="MainPage"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
NavigationPage.HasNavigationBar="False">

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="*" />
        <RowDefinition Height="*" />
        <RowDefinition Height="*" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>

    <ProgressBar
        x:Name="progress"
        Grid.Row="3"
        HorizontalOptions="FillAndExpand"
        Progress="0"
        VerticalOptions="Center" />

    <WebView
        x:Name="webview"
        Grid.Row="0"
        Grid.RowSpan="4"
        Grid.Column="0"
        HorizontalOptions="FillAndExpand"
        IsVisible="False"
        Source="https://google.com"
        VerticalOptions="FillAndExpand" />

</Grid>

public partial class App : Application
{
    public App ()
    {
        InitializeComponent();

        MainPage = new MainPage(); 
    }
}

And this custom WebView render for android in the android project

[assembly: ExportRenderer(typeof(WebView), typeof(CustomWebviewRenderer))]
namespace MyApp.Droid
{
    public class CustomWebviewRenderer: WebViewRenderer
    {
        private readonly Context context;

        public CustomWebviewRenderer(Context context) : base(context)
        {
            this.context = context;
        }

        protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
        {
            base.OnElementChanged(e);
            var formsWebView = e.NewElement as Xamarin.Forms.WebView;

            if (formsWebView != null)
            {
                var webView = Control as Android.Webkit.WebView;
                webView.SetWebViewClient(new CustomWebViewClient()); 
                webView.SetWebChromeClient(new CustomWebChromeClient());     
                webView.Settings.LoadWithOverviewMode = true;
                webView.Settings.UseWideViewPort = true;
                SetNativeControl(webView);
            }
        }

        private class ScoritoWebChromeClient : WebChromeClient
        {
            public override void OnProgressChanged(Android.Webkit.WebView view, int newProgress)
            {
                // how to update progressbar progress?
                base.OnProgressChanged(view, newProgress);               
            }
        }

        private class CustomWebViewClient : WebViewClient
        {
            public override void OnPageFinished(Android.Webkit.WebView view, string url)
            {
                // how to hide progressbar?
                base.OnPageFinished(view, url);
            }

            public override void OnPageStarted(Android.Webkit.WebView view, string url, Bitmap favicon)
            {
                base.OnPageStarted(view, url, favicon);
            }

            public override bool ShouldOverrideUrlLoading(Android.Webkit.WebView view, IWebResourceRequest request)
            {
                return base.ShouldOverrideUrlLoading(view, request);
            }
        }
    }
}

How can I update the ProgressBar that I have in my MainPage.xaml from the OnPageFinished in the CustomWebviewRenderer class to show the webpage has finished loading? Should I use the MainActivity.cs in the Android project?

Can someone point me in the right direction on how to solve this?

2
Your whole page is going to be a webview? if yes don't you think its a better option to just use a dependency service that has a callback?FreakyAli
Yes, but I don't know if that's a better option. I dont have that knowledge yet. Also looking through the basics of dependency service, I don't see how it has to be implemented in my custom rendererDenny
To be very honest I am curious, why exactly are you creating a custom renderer?FreakyAli
To use methods that are not available in the webview provided by xamarinDenny
Based on your questions, I'm thinking this is not as straightforward in Xamarin Forms as in native Android. So this is never done before or I'm doing something horribly wrong.Denny

2 Answers

0
votes

Update:

Create a custom WebView class in your PCL:

 public class CustomWebView: WebView
 {   
    public static readonly BindableProperty ActionProperty = BindableProperty.Create(propertyName: nameof(Action), returnType: typeof(Action),
        declaringType: typeof(CustomWebView),
        defaultValue: null,
        defaultBindingMode: BindingMode.OneWay);

    public void InvokeAction()
    {
        if (Action == null || data == null)
        {
            return;
        }
        Action.Invoke();
    }

    public Action Action
    {
        get { return (Action)GetValue(ActionProperty); }
        set { SetValue(ActionProperty, value); }
    }

 }

Use this CustomWebView with its namespace like this:

<namespace:CustomWebView  x:Name="webview"
    Grid.Row="0"
    Grid.RowSpan="4"
    Grid.Column="0"
    HorizontalOptions="FillAndExpand"
    IsVisible="False"
    Source="https://google.com"
    VerticalOptions="FillAndExpand"/>  

But if you still insist to use your renderer what you can do is update the progress bar using the element property and get a callback or change a property something like below:

[assembly: ExportRenderer(typeof(CustomWebView), typeof(CustomWebviewRenderer))]
 namespace MyApp.Droid
{
public class CustomWebviewRenderer: WebViewRenderer
{
    private readonly Context context;  

    public CustomWebviewRenderer(Context context) : base(context)
    {
        this.context = context;
    }

    protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
    {
        base.OnElementChanged(e);
        var formsWebView = e.NewElement as Xamarin.Forms.WebView;

        if (formsWebView != null)
        {
            var webView = Control as Android.Webkit.WebView;
            webView.SetWebViewClient(new CustomWebViewClient()); 
            webView.SetWebChromeClient(new CustomWebChromeClient());     
            webView.Settings.LoadWithOverviewMode = true;
            webView.Settings.UseWideViewPort = true;
            SetNativeControl(webView);
        }
    }

    private class ScoritoWebChromeClient : WebChromeClient
    {
        public override void OnProgressChanged(Android.Webkit.WebView view, int newProgress)
        {
            // how to update progressbar progress?
            base.OnProgressChanged(view, newProgress);               
        }
    }

    private class CustomWebViewClient : WebViewClient
    {
        private _webView;
        public CustomWebViewClient (Xamarin.Forms.WebView webView) 
        {
          _webView=webView;
        }
        public override void OnPageFinished(Android.Webkit.WebView view, string url)
        {
            // how to hide progressbar?
            _webView.InvokeAction();
            base.OnPageFinished(view, url);
        }

        public override void OnPageStarted(Android.Webkit.WebView view, string url, Bitmap favicon)
        {
            base.OnPageStarted(view, url, favicon);
        }

        public override bool ShouldOverrideUrlLoading(Android.Webkit.WebView view, IWebResourceRequest request)
        {
            return base.ShouldOverrideUrlLoading(view, request);
        }
    }
  }
}   

Note that this is just an example and can be changed as per your requirements.

In case it doesn't work or you have queries revert!

0
votes

So after 10 days I have finally found a solution to this by using bindableproperties...

So I've added a CustomWebView in the PCL

public class CustomWebView : WebView
{
    public static readonly BindableProperty PageProgressProperty = BindableProperty.Create(
        nameof(PageProgress),
        typeof(int),
        typeof(CustomWebView),
        default(int));

    public int PageProgress
    {
        get => (int)GetValue(PageProgressProperty);
        set => SetValue(PageProgressProperty, value);
    }      
}

Also a CustomProgressBar in the PCL

public class CustomProgressBar : ProgressBar
{
    public CustomProgressBar()
    {
    }

    public static readonly BindableProperty CurrentProgressProperty =
    BindableProperty.Create(
        nameof(CurrentProgress),
        typeof(int),
        typeof(CustomProgressBar),
        default(int),
        propertyChanged: CustomProgressPropertyChanged);

    private static void CustomProgressPropertyChanged(BindableObject sender, object oldValue, object newValue)
    {
        CustomProgressBar thisProgressBar = (CustomProgressBar)sender;
        double percent = Convert.ToDouble(newValue) / 100;
        thisProgressBar.ProgressTo(percent, 100, Easing.Linear);
    }

    public int CurrentProgress
    {
        get => (int)GetValue(CurrentProgressProperty);
        set => SetValue(CurrentProgressProperty, value);
    }
}

And now I can update the progress from my CustomWebviewRenderer as follows

public class CustomWebviewRenderer : WebViewRenderer
{
    private readonly Context context;

    public CustomWebviewRenderer(Context context) : base(context)
    {
        this.context = context;
    }

    protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
    {
        base.OnElementChanged(e);
        var formsWebView = e.NewElement as WebView;

        if (e.OldElement == null)
        {
            var x = Control;
            var webView = Control as Android.Webkit.WebView;
            webView.SetWebViewClient(new CustomWebViewClient(Element));
            webView.SetWebChromeClient(new CustomWebChromeClient(Element));
            webView.Settings.LoadWithOverviewMode = true;
            SetNativeControl(webView);
        }
    }

    private class CustomWebViewClient : WebViewClient
    {
        private readonly WebView formsWebView;

        public CustomWebViewClient(WebView webView)
        {
            formsWebView = webView;
        }

        public override void OnReceivedError(Android.Webkit.WebView view, IWebResourceRequest request, WebResourceError error)
        {
            base.OnReceivedError(view, request, error);
        }

        public override void OnPageFinished(Android.Webkit.WebView view, string url)
        {
            base.OnPageFinished(view, url);
        }

        public override void OnPageStarted(Android.Webkit.WebView view, string url, Bitmap favicon)
        {
            base.OnPageStarted(view, url, favicon);
        }

        public override bool ShouldOverrideUrlLoading(Android.Webkit.WebView view, IWebResourceRequest request)
        {
            return base.ShouldOverrideUrlLoading(view, request);
        }
    }

    private class CustomWebChromeClient : WebChromeClient
    {
        private WebView element;

        public CustomWebChromeClient(WebView element)
        {
            this.element = element;
        }

        public override void OnProgressChanged(Android.Webkit.WebView view, int newProgress)
        {
            base.OnProgressChanged(view, newProgress);
            element.SetValueFromRenderer(CustomWebView.PageProgressProperty, newProgress);
        }          
    }
}

And finally in the .xaml views these properties: BindingContext and CurrentProgress. The BindingContext is set to the webview, and the CurrentProgress is a custom property bound to PageProgress which is a property that lives in the CustomWebView

    <local:CustomProgressBar
        x:Name="progress"
        Grid.Row="3"
        BindingContext="{x:Reference webview}"
        CurrentProgress="{Binding Path=PageProgress}"
        HorizontalOptions="FillAndExpand"
        ProgressColor="#FFB800"
        VerticalOptions="Center" />

    <local:CustomWebView
        x:Name="webview"
        Grid.Row="0"
        Grid.RowSpan="4"
        Grid.Column="0"
        HorizontalOptions="FillAndExpand"
        IsVisible="False"
        VerticalOptions="FillAndExpand" />

So yeah, took a while to figure this out...