Xamarin WebView on Android showing multiple instances - android

I have created a Hybrid App using Xamarin.Forms. It uses multiple WebViews (XLabs HybridWebViews). I use the same app on iOS and it works fine, while on Android it gradually slows down. When I do a chrome://inspect on the device it shows multiple WebViews for the same page, seems more like the WebViews which were hidden are still in the memory and are slowing down the application.
How can I destroy the hidden WebView instances on Android?
Following is a screenshot of the chrome://inspect on the App:

Generally you should:
set your WebViews instances to null.
(optionaly) force garbage collection with GC.Collect();
If it's not enough make a custom renderer of your WebView and add this:
protected override void Dispose(bool disposing)
{
if (disposing && Element != null)
{
if (Control != null)
{
Control.StopLoading();
Control.ClearHistory();
Control.ClearCache(false);
Control.LoadUrl("about:blank");
Control.FreeMemory();
Control.PauseTimers();
}
}
base.Dispose(disposing);
}
It should solve your issues.

Related

Always hide keyboard for Xamarin Forms Android Entry Control (Barcode Scanner input)

I'm new to Xamarin.Forms and mobile app development, so patience & kindness is appreciated! Am building a barcode scanner app with Xamarin.Forms PCL, trying to use MVVM. The scanner is an EXTERNAL bluetooth device (so can't use ZXing).
This project has a fixed requirement to use the scanner as a keyboard-type input and for the user to be able to quickly swap out one bluetooth device for another brand (so no device-specific APIs can be used). A second requirement is for the user to never be allowed to type anything directly into the Entry control. Input should come from the scanner and only the scanner, so therefore we don't ever want the keyboard showing on the scanning page.
There are other pages that have Entry controls where the user WILL need access to the keyboard, and the scanner should be able to stay connected to bluetooth even when a non-scanning screen is displayed. Therefore, I need a reliable way to set the soft keyboard to never be displayed on the scanning page (there is only one input control on this page, and it's intended for scanner use only), but to allow the keyboard to be accessed on other pages.
When on the scanning page, we want focus to always be set on the scanner's Entry control, so when the control gets a Completed event, we do stuff with the value received, then clear out the control and re-set focus on it to prepare for the next scan.
I have been stumbling around writing custom controls and android renderers, and with setting up dependencies (preferred), both with partial success. Either way, there's a timing issue related to how soon focus is set on the control. If there's not enough of a delay before focus is set, the soft keyboard stays visible. In the code sample provided, I added a short sleep delay, which mostly works to keep the keyboard hidden. However, the keyboard still "flashes" on the screen briefly with each scan, which looks terrible. Would really prefer a solution that is less hacky and ugly.
Is there a good, simple way to remove the soft keyboard entirely for a page, while still allowing an input control to receive focus, so that a scanned barcode can be received? And/or any other suggestions that will allow me to still meet the requirements?
(PS: the scanning page does not currently use MVVM binding. Just trying to get the keyboard to go away first, then will work on binding.)
Below is one way I tried to solve it. There were others as well. NOTE: Ultimately I went with a completely different approach which I'll post as an answer.
The custom control (in PCL):
using Xamarin.Forms;
namespace MyPCL.Views
{
//See ScanEntryRenderer in the Android project.
public class ScanEntryControl : Entry
{
public ScanEntryControl() { }
}
}
The Xaml page (notice InputTransparent = "True" on the custom control. This is so the user cannot directly enter input on the android device. All input must come from the bluetooth scanner).
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MyPCL.Views"
x:Class="MyPCL.Views.ScanTestPage"
Title="Scan Test Page" >
<ContentPage.Content>
<StackLayout>
<Label Text="Scanner Test" />
<local:ScanEntryControl x:Name="BarcodeEntry"
Completed="BarcodeEntryCompleted"
InputTransparent="True"/>
<Label x:Name="ResultLabel" />
</StackLayout>
</ContentPage.Content>
</ContentPage>
The code behind for the form:
using System;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace MyPCL.Views
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class ScanTestPage : ContentPage
{
public ScanTestPage()
{
InitializeComponent();
BarcodeEntry.Focus();
}
protected override void OnAppearing()
{
base.OnAppearing();
BarcodeEntry.Focus();
}
private void BarcodeEntryCompleted(object sender, EventArgs e)
{
if (!string.IsNullOrWhiteSpace(BarcodeEntry.Text))
{
ResultLabel.Text = "You entered: " + BarcodeEntry.Text;
BarcodeEntry.Text = string.Empty;
}
BarcodeEntry.Focus();
}
}
}
The Android renderer:
using Android.Content;
using Xamarin.Forms;
using MyPCL.Views;
using MyPCL.Droid;
using Xamarin.Forms.Platform.Android;
using Android.Views.InputMethods;
[assembly: ExportRenderer(typeof(ScanEntryControl), typeof(ScanEntryRenderer))]
namespace MyPCL.Droid
{
public class ScanEntryRenderer : EntryRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
{
base.OnElementChanged(e);
if (e.NewElement != null)
{
((ScanEntryControl)e.NewElement).PropertyChanging += OnPropertyChanging;
}
if (e.OldElement != null)
{
((ScanEntryControl)e.OldElement).PropertyChanging -= OnPropertyChanging;
}
// Disable the Keyboard on Focus
this.Control.ShowSoftInputOnFocus = false;
}
private void OnPropertyChanging(object sender, PropertyChangingEventArgs propertyChangingEventArgs)
{
// Check if the view is about to get Focus
if (propertyChangingEventArgs.PropertyName == VisualElement.IsFocusedProperty.PropertyName)
{
// Dismiss the Keyboard
InputMethodManager imm = (InputMethodManager)this.Context.GetSystemService(Context.InputMethodService);
imm.HideSoftInputFromWindow(this.Control.WindowToken, 0);
}
}
}
}
I have been stumbling around writing custom controls and android renderers, and with setting up dependencies (preferred), both with partial success.
You can use EditText.ShowSoftInputOnFocus to achieve it in your scanning page, then the keyboard will not appear when your entry gets the focus:
using Android.Content;
using Android.Views.InputMethods;
using Edi;
using Edi.Droid;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
[assembly: ExportRenderer(typeof(ScanEntryControl), typeof(ScanEntryRenderer))]
namespace Edi.Droid
{
public class ScanEntryRenderer : EntryRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
{
base.OnElementChanged(e);
if (e.NewElement != null)
{
((ScanEntryControl)e.NewElement).PropertyChanging += OnPropertyChanging;
}
if (e.OldElement != null)
{
((ScanEntryControl)e.OldElement).PropertyChanging -= OnPropertyChanging;
}
// Disable the Keyboard on Focus
this.Control.ShowSoftInputOnFocus = false;
}
private void OnPropertyChanging(object sender, PropertyChangingEventArgs propertyChangingEventArgs)
{
// Check if the view is about to get Focus
if (propertyChangingEventArgs.PropertyName == VisualElement.IsFocusedProperty.PropertyName)
{
// incase if the focus was moved from another Entry
// Forcefully dismiss the Keyboard
InputMethodManager imm = (InputMethodManager)this.Context.GetSystemService(Context.InputMethodService);
imm.HideSoftInputFromWindow(this.Control.WindowToken, 0);
}
}
}
}
In other pages you can still use Entry, so the keyboard will be appear.
UPDATE:
ScanEntryControl class in PCL:
using Xamarin.Forms;
namespace Edi
{
public class ScanEntryControl : Entry
{
}
}
.xaml file:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Edi"
x:Class="Edi.MainPage">
<ContentPage.Content>
<StackLayout>
<local:ScanEntryControl Text="ScanEntryControl"/>
<Entry Text="Entry"/>
</StackLayout>
</ContentPage.Content>
</ContentPage>
This answer does not solve the original issue directly, in the sense that it does not involve an Entry control. However, it was the only thing that worked for me, and ended up being a more elegant solution:
The Bluetooth scanner was in HID mode (Human Interface Device) by default, meaning the only way it could interact with the app was by imitating key presses, thereby necessitating an Entry (EditText) control, or similar. I switched the scanner to SPP mode (Serial Port Profile) and adapted the code from this page (see also the GitHub repo here, and for more info on HID vs SPP see this document).
The resulting code activates the scanner and then "listens" for input. When input is received, it is displayed in a Label rather than an Entry control.
There were other problems with the Entry control that I didn't mention prior: often it would add a repeat character to the front of the barcode and/or chop off one or more characters from the end. The SPP solution solved all that as well. If anyone wants the code I came up with, let me know. It will take some work to put together in a generic example, so not posting it at the moment.
I was facing the same problem. I had found one sample over in the Xamarin forums that IMHO contained the key solution:
You must override Focus() and must not call the base method. This gives you full control over the virtual keyboard. In all other solutions I have seen the virtual keyboard appears sometimes.
Of course your custom Entry needs methods to show/hide the keyboard. You would call them in your OnFocus() method. My sample control (see below) also has a bindable property that allows you to show the virtual keyboard automatically on Focus. So you may decide for every field if the keyboard should appear automatically or not.
In addition I have included another object that informs you if the virtual keyboard is currently visible and its size in case you need to size your layout accordingly.
Since this is quite a common question in several different forums I have decided to create a sample control and a small app to show the features.
In addition I wrote a detailed Readme that explains all crucial points of the implementation.
You will find it here: https://github.com/UweReisewitz/XamarinAndroidEntry
protected override void OnAppearing()
{
base.OnAppearing();
txtLotID.Focus();
}
private void OnLoad()
{
Init();
swScanMode.IsToggled = Global.IsScannable;
txtLotID.EnableKeyboard = !Global.IsScannable;
txtLotID.OnEntryScanned += BtnSearch_Clicked;
Device.StartTimer(TimeSpan.FromSeconds(1), () =>
{
if (txtLotID.IsReadOnly)
{
txtLotID.Text = "";
**txtLotID.IsReadOnly = false;**
txtLotID.GetFocus();
}
return true;
});
}

Best practice on enable/disable webview in Android

Would like to ask for some advice on what's best way to implement on enabling and disabling the web view on Android?
I have this app wherein it can open urls within (by using web views) which then popups up and covers 80% of the UI, when the user navigates to another page of the app it should hide/close the web view but can be re-opened again when needed.
Here's a snippet of the code
private WebViewInterface webViewInterface = new WebViewInterface() {
#Override
public void onOpenURL(String url) {
navBrowserWV.setVisibility(View.VISIBLE);
navBrowserWV.getSettings().setJavaScriptEnabled(true);
navBrowserWV.setWebViewClient(new WebViewClient() {
#Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error){
handler.proceed();
}
});
navBrowserWV.loadUrl(url);
}
};
then this is how I close it
private void closeWebView() {
Log.d(LOGTAG, "closing webview...");
// Destroy WebView if it exists
if (this.navBrowserWV != null) {
this.navBrowserWV.stopLoading();
this.navBrowserWV.loadUrl("about:blank");
this.navBrowserWV.clearHistory();
this.navBrowserWV.clearCache(true);
this.navBrowserWV.pauseTimers();
this.navBrowserWV.setVisibility(View.GONE);
}
}
The problem with this implementation is that it displays the page properly at first but when I close it and then open the web view again with a url, it does not load the page anymore just a white background (no errors on the logger btw).
Would like to ask for help on how to resolve this one. Thanks
Finally, after a long time of digging up answers on the internet I found this old post regarding killing Android webview [link].
There's no real way to kill the WebView (it will always run in your process and you can't do anything about it ATM). So you only have to tell the WebView to load a bogus page, for me I did:
this.navBrowserWV.loadUrl("about:blank");
And it works now!
Quote from the website
4.1 .getSettings().setJavaScriptEnabled() Inhalt
Well, you would think that disabling JavaScript in the WebView’s settings would have an instant effect, in short: it does not. Only after reloading the page would there be no javascript any more, and this is not nice by design, since the page is still more or less well rendered.
4.2 .stopLoading() Inhalt
Since XHR „loads“ something from another server, you might think that calling „webview.stopLoading()“ would have an effect. In short: it does not. Works only on ressources contained within the HTML-file. Pity, is it not… Well, maybe not, since there is no „startLoading()“ method to resume XHR after resuming the activity anyway.
4.3 .destroy() Inhalt
As a last resort one might think about „destroy()“ing that thing, and true enough, the WebView itself is not accessible after that. Its threads however continue to exist as zombies somewhere in the vast RAM space and also continue to send XHR requests…
4.4 .pauseTimers() / resumeTimers() Inhalt
In short: Nope, does not work. I even don’t know what these methods are good for if not for controlling JavaScript timers. There aren’t any in plain HTML, AFAIK.
Update: When it comes to timers only, these functions seem to work on 2.3.5 and upwards, however, when there is no timer active at the time of calling the function, all in vain. With my use case: When pausing the app while there is an XHR active (instead of the running timer that schedules the next XHR call), nothing happens and the next timer continues unhindered.
I hope this will help someone who has the same problem as mine.

Can I combine these two apps (native and hybrid) into one app?

I have an app fully developped with Android Studio. I need to create a second app and this one I would like to do it with phonegapp. These two apps are related one to each other... so ideally I would like to combine them both in the same app... By combining I do not mean they need to work together. I would like them to be downloadad together (as the same app) and be able to go from one to the other withinn the app... so they can be completely two separate apps, but where the user would think it is only one app. I hope I am making myself well understood, The hybrid app uses the microphone with the Cordova plugin. At one time I thorugh about placing it with an iframe, but we would lose the microphone which invokes speech recognition Google native in Android, to convert it to text and pass it to edit text.
So my questions is:
Can I combine these two apps (native and hybrid) into one app?
Would the microphone with speech recognition to convert to text work well in both cases?
Yes, it was a bit of a pain to set up, but I have it working. I am not using PhoneGap, but I am using Construct along with the Cordova plugin, so it should be a similar setup.
First I took all of the web files generated by Cordova and placed them in the assets directory (if you do not have an assets directory you can just create one in the main directory). Then I used a WebView to display the content:
public class CordovaActivity extends AppCompatActivity {
private WebView mWebView;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_cordova);
mWebView = (WebView) findViewById(R.id.content);
WebSettings settings = mWebView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setDomStorageEnabled(true);
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
settings.setAllowUniversalAccessFromFileURLs(true);
}
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
settings.setMediaPlaybackRequiresUserGesture(false);
}
mWebView.loadUrl("file:///android_asset/www/index.html");
}
#Override
public void onResume() {
super.onResume();
if(mWebView != null) {
mWebView.resumeTimers();
mWebView.onResume();
}
}
#Override
public void onPause() {
super.onPause();
if(mWebView != null) {
mWebView.pauseTimers();
mWebView.onPause();
}
}
}
The setJavaScriptEnabled(), setDomStorageEnabled() and setAllowUniversalAccessFromFileURLs() settings all had to be set to true or the content would not load. The last setting, setMediaPlaybackRequiresUserGesture(), was required to allow audio to play.
It will be a little more work to get the Cordova content to interact with the rest of the app though; you will have to use a JavaScriptInterface in order to do so.
1) Nothing is stopping you from having two apps that act as one (aside from the downloading part). They can "speak" to one another using dedicated Intents and share data using ContentProviders. You will have to take care of the case where one app is installed and the other isn't.
2) I don't see any particular issue with the the microphone in this scenario.
However, if I were you, I'd opt for using a single app if that is possible. I am not sure what are the phonegapp limitations for this kind of implementation though.

Why Xamarin.Forms WebView increases Android app storage data size?

I have a Xamarin.Forms app with a feature using WebView. It opens a simple web page.
Every time I open a webpage in it, app's storage data size increases. I check it on Android/Settings/App info/Storage. My app size is ~15 MB, but after opening some pages, it goes to 50 MB and so on. What is it doing? Caching? Or...? How can I disable it?
I assume that it is the cache of the WebView. Unfortunately, the Cache of the Forms WebView is not configurable. So you have to write a custom renderer and disable it on the native view. Add this class to your Android Project.
[assembly: ExportRenderer(typeof(WebView), typeof(NoChacheWebViewRenderer))]
namespace MyApp.Droid.Renderer
{
public class NoChacheWebViewRenderer : WebViewRenderer
{
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
if (Control == null) return;
Control.ClearCache(true);
Control.Settings.SetAppCacheEnabled(false);
Control.Settings.CacheMode = CacheModes.NoCache;
}
}
}

Disable Long Press of ListView with custom renderer

I'm attempting to disable the "long press" animation that seems to come by default on Android devices using Xamarin Forms. I have a custom renderer written (that properly works, I can change the background, etc.. and the changes are resounded in the listview). Here is the core of my renderer:
protected override void OnElementChanged(ElementChangedEventArgs<ListView> e)
{
base.OnElementChanged(e);
if (Control != null) {
Control.LongClickable = false;
}
}
I have also tried Control.SetOnLongClickListener (null). Neither of the above controls seem to affect the long press behavior of the listview. Any suggestions would be greatly appreciated :)

Categories

Resources