How to update custom WebView height request when content is shrunk - android

The problem occurs on Android.
I have implemented a custom renderer for a WebView to get the capability of resizing the height request base on its content.
I took this from a xamarin forum post.
[assembly: ExportRenderer(typeof(AutoHeightWebView), typeof(AutoHeightWebViewRenderer))]
namespace MyProject.Droid.Renderers
{
public class AutoHeightWebViewRenderer : WebViewRenderer
{
public AutoHeightWebViewRenderer(Context context): base(context) {}
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.WebView> e)
{
base.OnElementChanged(e);
if (e.NewElement is AutoHeightWebView webViewControl)
{
if (e.OldElement == null)
{
Control.SetWebViewClient(new ExtendedWebViewClient(webViewControl));
}
}
}
class ExtendedWebViewClient : Android.Webkit.WebViewClient
{
private readonly AutoHeightWebView _control;
public ExtendedWebViewClient(AutoHeightWebView control)
{
_control = control;
}
public override async void OnPageFinished(Android.Webkit.WebView view, string url)
{
if (_control != null)
{
int i = 10;
while (view.ContentHeight == 0 && i-- > 0) // wait here till content is rendered
{
await System.Threading.Tasks.Task.Delay(100);
}
_control.HeightRequest = view.ContentHeight;
}
base.OnPageFinished(view, url);
}
}
}
}
Based on a certain logic, I change the source of the WebView and use the custom renderer to resize the view.
This works when the size is increased but not when the content size is smaller than the one before...
The be clearer, if I set the source of the WebView to a html file that is 200px height and change it to a html file that is 1000px, it works fine and I can see all the content. BUT, if I try to go back to my 200px html file, I get a 800px blank space underneath since the content doesn't change on the view.ContentHeight and keep the value of 1000px.
I followed this issue/thread and didn't find a solution to resolve this problem : https://github.com/xamarin/Xamarin.Forms/issues/1711
I have seen a lot of topics on Android saying that we need to recreate the webview. Is there any other solution?

I found a way to do it based on this thread.
I first tried with a JS command that returns the body height : document.body.scrollHeight.
The problem was the same, it always returned the largest size, but never decreased the size.
As the JS command have no problem to increase the size, I had to set the height to 0, set a 100ms delay (arbitrary) and then get the height of the HTML with the JS command above and set the HeightRequest of the view.
With this, the HTML body height will not decrease... It will always start from 0 and increase to the HTML body size.
Final result :
public class AutoHeightWebViewRenderer : WebViewRenderer
{
static AutoHeightWebView _xwebView = null;
public AutoHeightWebViewRenderer(Android.Content.Context context) : base(context)
{
}
class DynamicSizeWebViewClient : WebViewClient
{
public async override void OnPageFinished(Android.Webkit.WebView view, string url)
{
try
{
if (_xwebView != null)
{
view.Settings.JavaScriptEnabled = true;
view.Settings.DomStorageEnabled = true;
_xwebView.HeightRequest = 0d;
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 AutoHeightWebView;
if (e.OldElement == null)
{
Control.SetWebViewClient(new DynamicSizeWebViewClient());
}
}
}

Related

Xamarin Forms - Take photograph without any user interaction

I have a requirement to take a photograph of a user in Xamarin Forms without them having to press the shutter button. For example, when the app launches it should show a preview and count down from 5 seconds (to give the user chance to get in position) then take a picture automatically.
I have tried the Xamarin Media Plugin library however this stackoverflow post and this GitHub issue state that this feature is not a supported.
I have seen a number of dead discussions such as this with people asking similar questions without resoltion.
I tried the LeadTools AutoCapture sample but this only seems to work for documents/text and not people (unless I am missing something??).
I am now working my way through the Camera2Basic sample which is quite old and only targets Android via android.hardware.camera2.
Are there any samples out there (or 3rd party libraries) that can acheive this requirement? Ideally I would like it to be cross platform (iOS and Android) but currently the main focus is Android.
You can create the Custom View Renderer on Android to achieve that.
And based on this offical sample is more convenient, just modify code as follow can achieve your wants.
This official sample can preview camera view in Xamarin Forms App, we just need to add a Timer to call the Frame from Camera after 5 seconds.The modified Renderer code as follow:
public class CameraPreviewRenderer : ViewRenderer<CustomRenderer.CameraPreview, CustomRenderer.Droid.CameraPreview>, Camera.IPreviewCallback
{
CameraPreview cameraPreview;
byte[] tmpData;
public CameraPreviewRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<CustomRenderer.CameraPreview> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
// Unsubscribe
cameraPreview.Click -= OnCameraPreviewClicked;
}
if (e.NewElement != null)
{
if (Control == null)
{
cameraPreview = new CameraPreview(Context);
SetNativeControl(cameraPreview);
}
Control.Preview = Camera.Open((int)e.NewElement.Camera);
// Subscribe
cameraPreview.Click += OnCameraPreviewClicked;
}
}
protected override void OnAttachedToWindow()
{
base.OnAttachedToWindow();
// call the timer method to get the current frame.
Device.StartTimer(new TimeSpan(0, 0, 5), () =>
{
// do something every 5 seconds
Device.BeginInvokeOnMainThread(() =>
{
Console.WriteLine("get data"+tmpData);
// using MessagingCenter to pass data to forms
MessagingCenter.Send<object, byte[]>(this, "CameraData", tmpData);
cameraPreview.Preview.StopPreview();
cameraPreview.IsPreviewing = false;
// interact with UI elements
});
return false; // runs again, or false to stop
});
}
void OnCameraPreviewClicked(object sender, EventArgs e)
{
if (cameraPreview.IsPreviewing)
{
cameraPreview.Preview.StopPreview();
cameraPreview.IsPreviewing = false;
}
else
{
cameraPreview.Preview.SetPreviewCallback(this);
cameraPreview.Preview.StartPreview();
cameraPreview.IsPreviewing = true;
}
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
Control.Preview.Release();
}
base.Dispose(disposing);
}
// get frame all the time
public void OnPreviewFrame(byte[] data, Camera camera)
{
tmpData = data;
}
}
Now, Xamarin Forms can receive the data from MessagingCenter:
MessagingCenter.Subscribe<object, byte[]>(this, "CameraData", async (sender, arg) =>
{
MemoryStream stream = new MemoryStream(arg);
if (stream != null)
{
//image is defined in Xaml
image.Source = ImageSource.FromStream(() => stream);
}
});
image is defined in XAML: <Image x:Name="image" WidthRequest="200" HeightRequest="200"/>

How do you remove the underline of a text in Xamarin Editor Control on the Android Platform?

Based on the research that I've done, the suggestion was to create a renderer in Android and set the background to transparent but it did not work.
Here's the code.
public class CustomEditorRenderer : EditorRenderer
{
protected override void OnElementChanged (ElementChangedEventArgs<Xamarin.Forms.Editor> e)
{
base.OnElementChanged(e);
if (Control == null)
return;
Control.Background = new ColorDrawable(ClaimAllGraphics.Color.Transparent);
}
}
How do I remove the bottom line in an editor?
Secondly, how do I ensure I still have the enter / return keypad on the keyboard?
This is important as I have a multiline editor, so users should be able to press the enter / return keypad and move to the next line.
Remove the underline of a text in Xamarin Editor Control on the Android Platform?
Change ClaimAllGraphics.Color.Transparent to null:
public class CustomEditorRenderer : EditorRenderer
{
public CustomEditorRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.Editor> e)
{
base.OnElementChanged(e);
if (Control == null)
return;
Control.Background = null;
//Or Control.Background = new ColorDrawable(Android.Graphics.Color.Transparent);
}
}
Effect.

How to set ContentPage orientation in Xamarin.Forms

I'm using Xamarin.Forms to create a cross platform application, all of my ContentPages are situated within the PCL.
I'm looking for a way to set and lock the orientation of a single ContentPage to Landscape, preferably without having to create another activity in each of the platform specific projects.
Since my ContentPage.Content is set to a ScrollView, I've tried setting the ScrollOrientation to Horizontal, however this did not work.
I've also tried using a RelativeLayout, but I can't see an Orientation property on this.
public class PlanningBoardView : ContentPage //Container Class.
{
public PlanningBoardView()
{
scroller = new ScrollView ();
Board = new PlanningBoard();
scroller.Orientation = ScrollOrientation.Horizontal;
scroller.WidthRequest = Board.BoardWidth;
scroller.Content = Board;
Content = scroller;
}
}
The last thing I tried was using Xamarin Studio's version of Intellisense and the Xamarin Forms API Doc's to look through the different Layouts available to me, none of which had a Orientation property.
I fear the only way to do this is by creating a second platform specific Activity just for this one ContentPage and setting the orientation to landscape.
Although this method would work, it makes the Navigation between screens a lot more complex.
This is currently being tested in Android.
Hate to say this but this can only be done using custom renderers or a platform-specific code
In android, you can set the RequestedOrientation property of the MainActivity to ScreenOrientation.Landscape.
In iOS, you can override GetSupportedInterfaceOrientations in the AppDelegate class to return one of the UIInterfaceOrientationMask values when Xamarin.Forms.Application.Current.MainPage is the ContentPage that you are intereted in.
Android
[assembly: Xamarin.Forms.ExportRenderer(typeof(MyCustomContentPage), typeof(CustomContentPageRenderer))]
public class CustomContentPageRenderer : Xamarin.Forms.Platform.Android.PageRenderer
{
private ScreenOrientation _previousOrientation = ScreenOrientation.Unspecified;
protected override void OnWindowVisibilityChanged(ViewStates visibility)
{
base.OnWindowVisibilityChanged(visibility);
var activity = (Activity)Context;
if (visibility == ViewStates.Gone)
{
// Revert to previous orientation
activity.RequestedOrientation = _previousOrientation == ScreenOrientation.Unspecified ? ScreenOrientation.Portrait : _previousOrientation;
}
else if (visibility == ViewStates.Visible)
{
if (_previousOrientation == ScreenOrientation.Unspecified)
{
_previousOrientation = activity.RequestedOrientation;
}
activity.RequestedOrientation = ScreenOrientation.Landscape;
}
}
}
iOS
public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
{
public override UIInterfaceOrientationMask GetSupportedInterfaceOrientations(UIApplication application, UIWindow forWindow)
{
if (Xamarin.Forms.Application.Current == null || Xamarin.Forms.Application.Current.MainPage == null)
{
return UIInterfaceOrientationMask.Portrait;
}
var mainPage = Xamarin.Forms.Application.Current.MainPage;
if (mainPage is MyCustomContentPage ||
(mainPage is NavigationPage && ((NavigationPage)mainPage).CurrentPage is MyCustomContentPage) ||
(mainPage.Navigation != null && mainPage.Navigation.ModalStack.LastOrDefault() is MyCustomContentPage))
{
return UIInterfaceOrientationMask.Landscape;
}
return UIInterfaceOrientationMask.Portrait;
}
}
This can also be done by sending the message from Form project to host project using MessagingCenter class. without using the custom renderer or dependency service as follows,
public partial class ThirdPage : ContentPage
{
protected override void OnAppearing()
{
base.OnAppearing();
MessagingCenter.Send(this, "allowLandScapePortrait");
}
//during page close setting back to portrait
protected override void OnDisappearing()
{
base.OnDisappearing();
MessagingCenter.Send(this, "preventLandScape");
}
}
Change in mainactivity to receive the message and set RequestedOrientation
[Activity(Label = "Main", ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation,ScreenOrientation = ScreenOrientation.Portrait)]
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsApplicationActivity
{
//allowing the device to change the screen orientation based on the rotation
MessagingCenter.Subscribe<ThirdPage>(this, "allowLandScapePortrait", sender =>
{
RequestedOrientation = ScreenOrientation.Unspecified;
});
//during page close setting back to portrait
MessagingCenter.Subscribe<ThirdPage>(this, "preventLandScape", sender =>
{
RequestedOrientation = ScreenOrientation.Portrait;
});
}
Check for more in my blog post : http://www.appliedcodelog.com/2017/05/force-landscape-or-portrait-for-single.html
If you are also running into issue on Android where device rotation returns you back to prompt for user email, you can follow up progress of fixes for both ADAL and MSAL here:
https://github.com/AzureAD/azure-activedirectory-library-for-dotnet/issues/1622 https://github.com/xamarin/xamarin-android/issues/3326
Dealdiane's code works well for me with minor change:
protected override void OnWindowVisibilityChanged(ViewStates visibility) {
base.OnWindowVisibilityChanged( visibility );
IRotationLock page = Element as IRotationLock;
if ( page == null )
return;
var activity = (Activity) Context;
if ( visibility == ViewStates.Gone ) {
// Revert to previous orientation
activity.RequestedOrientation = _previousOrientation;
} else if ( visibility == ViewStates.Visible ) {
if ( _previousOrientation == ScreenOrientation.Unspecified ) {
_previousOrientation = activity.RequestedOrientation;
}
switch ( page.AllowRotation() ) {
case RotationLock.Landscape:
activity.RequestedOrientation = ScreenOrientation.SensorLandscape;
break;
case RotationLock.Portrait:
activity.RequestedOrientation = ScreenOrientation.SensorPortrait;
break;
}
}
}

Issue with WebView.EvaluateJavaScript in Android Xamarin

I am using the following code for injecting Java Script in to my Android Web view
WebView
webView = FindViewById<WebView> (Resource.Id.learningWebView);
if (null != webView) {
webView.Settings.JavaScriptEnabled = true;
webView.Settings.SetSupportZoom (true);
webView.SetWebViewClient (new CustomWebViewClient ());
}
WebView Client implementation
public class CustomWebViewClient : WebViewClient
{
public override bool ShouldOverrideUrlLoading (WebView view, string url)
{
view.LoadUrl (url);
return true;
}
public override void OnPageStarted (WebView view, string url, Android.Graphics.Bitmap favicon)
{
}
public override void OnPageFinished (WebView view, string url)
{
base.OnPageFinished (view, url);
HideLearningDivs (view);
}
void HideLearningDivs (WebView view)
{
try {
view.EvaluateJavascript ("document.getElementById(\"suiteBar\").parentNode.style.display='none'", new JavaScriptResult ());
} catch (Exception ex) {
Console.WriteLine (ex.Message);
}
}
IValueCallback Implementation
public class JavaScriptResult : IValueCallback
{
public IntPtr Handle {
get;
set;
}
public void Dispose ()
{
}
public void OnReceiveValue (Java.Lang.Object result)
{
}
}
But during the time of executing the application I am getting the following error.
java.lang.NoSuchMethodError: no method with name='evaluateJavascript' signature='(Ljava/lang/String;Landroid/webkit/ValueCallback;)V' in class Landroid/webkit/WebView;
Can anyone please help me to find what is wrong with my implementation.
I will link to where I found the answer below, but basically you need to do a check for Android KitKat (4.4), since that function was not introduced until then. If the device is running lower than 4.4, then you may need to do something different to get the value back if you actually need to do something with it. Such as using a Hybrid WebView of some kind (check out Xamarin Forms Labs version of it perhaps) and/or using the AddJavaScriptInterface()
Here is the code:
if(Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.Kitkat) {
webView.EvaluateJavascript("javascript:GoBack();", null);
} else {
webView.LoadUrl("javascript:GoBack();");
}
https://forums.xamarin.com/discussion/24894/webview-evaluatejavascript-issues
*Edit: Since writing this, I found an excellent post Adam Pedley (who I apparently have been linking to a lot lately) which covers doing this for Xamarin Forms but also mentions a change in Android 4.2 to the JS engine. Running the JavaScript might work the first time but it also sets the document object to the script result, so you may need to assign the result of document.getElementById() to a variable in order to work around this: var x = document.getElementById().
JavaScriptResult class must be inherited from Java.Lang.Object, like this:
public class JavaScriptResult : Java.Lang.Object, IValueCallback
{
public void OnReceiveValue(Object value)
{
// ...
}
}

Auto-Scroll the Webview

I want my webview to autoscroll. Below is what I have tried, it does scroll the webview but it never stops i.e. it continues even after the webview has no content to display so it just displays the white screen. Please tell me how can it be fixed.
webview.setPictureListener(new PictureListener() {
public void onNewPicture(WebView view, Picture picture) {
webview.scrollBy(0, 1);
}
});
Try my code and hope it helps :)
scroll down:
mWebView.post(new Runnable() {
public void run() {
if (mWebView.getContentHeight() * mWebView.getScale() >= mWebView.getScrollY() ){
mWebView.scrollBy(0, (int)mWebView.getHeight());
}
}
});
Scroll Up
mWebView.post(new Runnable() {
public void run() {
if (mWebView.getScrollY() - mWebView.getHeight() > 0){
mWebView.scrollBy(0, -(int)mWebView.getHeight());
}else{
mWebView.scrollTo(0, 0);
}
}
});
This code work for auto scroll as well as your scroll goes down then automatically come to top and then again start scroll.
Check and if any query update me.
Timer repeatTask = new Timer();
repeatTask.scheduleAtFixedRate(new TimerTask() {
#Override
public void run() {
runOnUiThread(new Runnable() {
#Override
public void run() {
int height = (int) Math.floor(webView.getContentHeight() * webView.getScale());
int webViewHeight = webView.getMeasuredHeight();
if (webView.getScrollY() + webViewHeight >= height) {
webView.scrollBy(0, 0);
webView.scrollTo(0, 0);
} else {
webView.scrollBy(webView.getTop(), webView.getBottom());
}
}
});
}
}, 0, 5000);//delayed auto scroll time
As best as I can tell, the only way to do this consistently with modern Android is with JavaScript:
webView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
webView.evaluateJavascript("""
|var _fullScrollIntervalID = setInterval(function() {
| if (window.scrollY + window.innerHeight >= document.body.scrollHeight) {
| window.clearInterval(_fullScrollIntervalID);
| } else {
| window.scrollBy(0, 10);
| }
|}, 17);
""".trimMargin(), null)
}
}
The JavaScript APIs are aware of the size of the content.
This solution doesn't take into account changing viewport sizes, window.innerHeight rounding errors, if document.body isn't the scrolling element, etc.
As for a Java-based solution, it seems the Java APIs give the size of the view, rather than the length of the page:
height
contentHeight
measuredHeight
bottom
Maybe this changed when enableSlowWholeDocumentDraw was introduced.
One Java API that is aware of the content length of the page is canScrollVertically but it returns false for a short time after onPageFinished is called. You could use some kind of delay to get around this. For example:
webView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
ScrollRunnable().run()
}
}
// ...
val h = Handler()
inner class ScrollRunnable() : Runnable {
override fun run() {
// introduce some delay here!
if (webView.canScrollVertically(1)) {
webView.scrollBy(0, 10)
h.postDelayed(this, 17L)
}
}
}
I tested this on an Android API26 emulator and an Android TV API 22 emulator.

Categories

Resources