Xamarin Forms Webview - Scroll event - xamarin

I need to know when the user has ended the scroll on a webview displaying a agreement, to display an "Accept" only when the user has read this.
Here is my webview (xaml) :
<WebView x:Name="webView" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
<WebView.Source>
<HtmlWebViewSource Html="{Binding Content}" />
</WebView.Source>
How can I do that (.cs side) ?..
What is the best practice ?
Xamarin Forms - Targets : iOS & Android
Thanks for your help ;)
EDIT (iOs/Android working solution) :
Here is what I implemented to listen scroll inside the webview (JS event):
Custom WebView renderer :
namespace xxx.Views.Common.Controls
{
public class WebViewer : WebView
{
public static BindableProperty EvaluateJavascriptProperty =
BindableProperty.Create(nameof(EvaluateJavascript), typeof(Func<string, Task<string>>), typeof(WebViewer),
null, BindingMode.OneWayToSource);
public static BindableProperty RefreshProperty =
BindableProperty.Create(nameof(Refresh), typeof(Action), typeof(WebViewer), null,
BindingMode.OneWayToSource);
public static BindableProperty GoBackProperty =
BindableProperty.Create(nameof(GoBack), typeof(Action), typeof(WebViewer), null,
BindingMode.OneWayToSource);
public static BindableProperty CanGoBackFunctionProperty =
BindableProperty.Create(nameof(CanGoBackFunction), typeof(Func<bool>), typeof(WebViewer), null,
BindingMode.OneWayToSource);
public static BindableProperty GoBackNavigationProperty =
BindableProperty.Create(nameof(GoBackNavigation), typeof(Action), typeof(WebViewer), null,
BindingMode.OneWay);
public Func<string, Task<string>> EvaluateJavascript
{
get { return (Func<string, Task<string>>) this.GetValue(EvaluateJavascriptProperty); }
set { this.SetValue(EvaluateJavascriptProperty, value); }
}
public Action Refresh
{
get { return (Action) this.GetValue(RefreshProperty); }
set { this.SetValue(RefreshProperty, value); }
}
public Func<bool> CanGoBackFunction
{
get { return (Func<bool>) this.GetValue(CanGoBackFunctionProperty); }
set { this.SetValue(CanGoBackFunctionProperty, value); }
}
public Action GoBackNavigation
{
get { return (Action) this.GetValue(GoBackNavigationProperty); }
set { this.SetValue(GoBackNavigationProperty, value); }
}
}
}
Android Renderer :
[assembly: ExportRenderer(typeof(WebViewer), typeof(WebViewRender))]
namespace xxx.Droid.Renderers
{
public class WebViewRender : WebViewRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.WebView> e)
{
base.OnElementChanged(e);
var oldWebView = e.OldElement as WebViewer;
if (oldWebView != null)
{
oldWebView.EvaluateJavascript = null;
}
var newWebView = e.NewElement as WebViewer;
if (newWebView != null)
{
newWebView.EvaluateJavascript = async js =>
{
ManualResetEvent reset = new ManualResetEvent(false);
var response = "";
Device.BeginInvokeOnMainThread(() =>
{
System.Diagnostics.Debug.WriteLine("Javascript Send: " + js);
this.Control?.EvaluateJavascript(js, new JavascriptCallback(r =>
{
response = r;
reset.Set();
}));
});
await Task.Run(() => { reset.WaitOne(); });
if (response == "null")
{
response = string.Empty;
}
return response;
};
}
if (this.Control != null && e.NewElement != null)
{
this.SetupControl();
}
}
/// <summary>
/// Sets up various settings for the Android WebView
/// </summary>
private void SetupControl()
{
// Ensure common functionality is enabled
this.Control.Settings.DomStorageEnabled = true;
this.Control.Settings.JavaScriptEnabled = true;
// Must remove minimum font size otherwise SAP PDF's go massive
this.Control.Settings.MinimumFontSize = 0;
// Because Android 4.4 and below doesn't respect ViewPort in HTML
if (Build.VERSION.SdkInt < BuildVersionCodes.Lollipop)
{
this.Control.Settings.UseWideViewPort = true;
}
}
}
internal class JavascriptCallback : Java.Lang.Object, IValueCallback
{
private readonly Action<string> _callback;
public JavascriptCallback(Action<string> callback)
{
this._callback = callback;
}
public void OnReceiveValue(Java.Lang.Object value)
{
System.Diagnostics.Debug.WriteLine("Javascript Return: " + Convert.ToString(value));
this._callback?.Invoke(Convert.ToString(value));
}
}
public class WebViewChromeClient : WebChromeClient
{
readonly Action<IValueCallback, Java.Lang.String, Java.Lang.String> callback;
public WebViewChromeClient(Action<IValueCallback, Java.Lang.String, Java.Lang.String> callback)
{
this.callback = callback;
}
//For Android 4.1+
[Java.Interop.Export]
public void openFileChooser(IValueCallback uploadMsg, Java.Lang.String acceptType, Java.Lang.String capture)
{
this.callback(uploadMsg, acceptType, capture);
}
// For Android 5.0+
public override bool OnShowFileChooser(WebView webView, IValueCallback filePathCallback,
FileChooserParams fileChooserParams)
{
return base.OnShowFileChooser(webView, filePathCallback, fileChooserParams);
}
}
}
iOS Renderer :
[assembly: ExportRenderer(typeof(WebViewer), typeof(WebViewRender))]
namespace xxx.iOS
{
public class WebViewRender : WebViewRenderer
{
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
//if (this.NativeView != null && e.NewElement != null)
//{
// this.InitializeCommands((WebViewer) e.NewElement);
//}
var webView = e.NewElement as WebViewer;
if (webView != null)
{
webView.EvaluateJavascript = js => { return Task.FromResult(this.EvaluateJavascript(js)); };
}
}
private void InitializeCommands(WebViewer element)
{
var ctl = (UIWebView) this.NativeView;
ctl.ScalesPageToFit = true;
}
}
}
xaml Display :
<pages:xxxContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:pages="clr-namespace:xxx.Views"
xmlns:controls="clr-namespace:xxx.Views.Common.Controls;assembly=xxx.View"
x:Class="xxx.Views.AboutPage">
<pages:xxxInfoContentPage.PageView>
<StackLayout HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand">
<controls:WebViewer x:Name="webView" EvaluateJavascript="{Binding EvaluateJavascript}" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
<WebView.Source>
<HtmlWebViewSource Html="{Binding Content}" />
</WebView.Source>
</controls:WebViewer>
</StackLayout>
</pages:DriversInfoContentPage.PageView>
</pages:DriversInfoContentPage>
JS inside webview (redirection when the bottom is reached) :
<script type="text/javascript">
function init() {
window.onscroll = function(ev) {
if ((window.innerHeight + window.pageYOffset) >= document.body.offsetHeight) {
window.location.hash = "bottom";
location.reload();
}
};
}
</script>
<body onload="init()">
<!-- Scrolled content-->
</body>
Catching & Canceling Navigating Event xaml.xs side :
public AboutPage()
{
this.webView.Navigating += this.NavigatingEvent;
}
private void NavigatingEvent(object sender, WebNavigatingEventArgs e)
{
if (e.Url.Contains("bottom") || e.Url.Contains("about:blank"))
{
e.Cancel = true;
// ACTION WHEN BOTTOM IS REACHED HERE
}
}

I would recommend making your C# access the javascript.
See my tutorial:
https://www.youtube.com/watch?v=_0a7NzkNl-Q

Related

How to set picker row item long text in two lines using custom renderer in xamarin forms android?

I want to show long text of picker item in two lines using custom render how can I achieve it?
First , you need a custom renderer for picker .
To override the item you need to replace the original view with a AlertDialog .
Then custom a ListView and set it as AlertDialog.View .
Then you can customize everything in the Adapter(here we need to customize the textview ).
Sample code
[assembly: ExportRenderer(typeof(Picker), typeof(MyPickerRenderer))]
namespace FormsApp.Droid
{
public class MyAdapter : ArrayAdapter
{
private IList<string> _data;
Context _context;
public MyAdapter(Context context, int resource , IList<string> data) : base(context,resource)
{
_context = context;
_data = data;
}
public override int Count => _data.Count;
public override Android.Views.View GetView(int position, Android.Views.View convertView, ViewGroup parent)
{
TextView textview = new TextView(_context);
textview.TextSize = 18;
textview.SetTextColor(Android.Graphics.Color.DarkGray);
textview.Ellipsize = TruncateAt.End;
textview.SetMaxLines(2); //this line
textview.Text = _data[position];
return textview;
}
}
public class MyListView : Android.Widget.ListView
{
public MyListView(Context context, IList<string> data) : base(context)
{
this.DividerHeight = 0;
this.Adapter = new MyAdapter(context, 0, data);
}
}
class MyPickerRenderer : Xamarin.Forms.Platform.Android.AppCompat.PickerRenderer
{
IElementController ElementController => Element as IElementController;
public MyPickerRenderer(Context context):base(context)
{
}
private AlertDialog _dialog;
protected override void OnElementChanged(ElementChangedEventArgs<Picker> e)
{
base.OnElementChanged(e);
if (e.NewElement == null || e.OldElement != null)
return;
Control.Click += Control_Click;
}
protected override void Dispose(bool disposing)
{
Control.Click -= Control_Click;
base.Dispose(disposing);
}
private void Control_Click(object sender, EventArgs e)
{
Picker model = Element;
var picker = new MyListView(Context, model.Items);
var layout = new LinearLayout(Context) { Orientation = Orientation.Vertical };
layout.SetPadding(35, 30, 35, 0);
layout.AddView(picker);
ElementController.SetValueFromRenderer(VisualElement.IsFocusedProperty, true);
var builder = new AlertDialog.Builder(Context);
builder.SetView(layout);
builder.SetTitle(model.Title ?? "");
builder.SetNegativeButton("Cancel ", (s, a) =>
{
ElementController.SetValueFromRenderer(VisualElement.IsFocusedProperty, false);
// It is possible for the Content of the Page to be changed when Focus is changed.
// In this case, we'll lose our Control.
Control?.ClearFocus();
_dialog = null;
});
_dialog = builder.Create();
_dialog.DismissEvent += (ssender, args) =>
{
ElementController?.SetValueFromRenderer(VisualElement.IsFocusedProperty, false);
};
_dialog.Show();
}
}
}
Testing code in Forms xaml
<Picker x:Name="picker" Title="Select a monkey">
<Picker.ItemsSource>
<x:Array Type="{x:Type x:String}">
<x:String>abc</x:String>
<x:String>aaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbccccccccccccccccdddddddddddddddddddddd</x:String>
</x:Array>
</Picker.ItemsSource>
</Picker>
Before
After

How to implement TabLayout.IOnTabSelectedListener.OnTabUnselected with TabbedPage.ToolbarPlacement="Bottom" - Xamarin Forms?

I just recently used android:TabbedPage.ToolbarPlacement="Bottom". I used to have the following code:
void TabLayout.IOnTabSelectedListener.OnTabUnselected(TabLayout.Tab tab)
{
var playPage = Element.CurrentPage as NavigationPage;
if (!(playPage.RootPage is PhrasesFrame))
return;
var tabLayout = (TabLayout)ViewGroup.GetChildAt(1);
var playTab = tabLayout.GetTabAt(4);
tab.SetText("Play");
tab.SetIcon(Resource.Drawable.ionicons_2_0_1_play_outline_25);
App.pauseCard = true;
}
Anyone knows how can I implement this with ToolbarPlacement="Bottom" ? I have implemented both BottomNavigationView.IOnNavigationItemSelectedListener, BottomNavigationView.IOnNavigationItemReselectedListener but can't find any reference for UnselectedTab if there is any.
Edit:
Previous custom renderer using the default tab position and implementing TabLayout:
namespace Japanese.Droid
{
public class MyTabbedPageRenderer: TabbedPageRenderer, TabLayout.IOnTabSelectedListener
{
ViewPager viewPager;
TabLayout tabLayout;
bool setup;
public MyTabbedPageRenderer(Context context): base(context){ }
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
// More codes here
}
void TabLayout.IOnTabSelectedListener.OnTabReselected(TabLayout.Tab tab)
{
UpdateTab(tab);
}
void TabLayout.IOnTabSelectedListener.OnTabSelected(TabLayout.Tab tab)
{
UpdateTab(tab);
}
void TabLayout.IOnTabSelectedListener.OnTabUnselected(TabLayout.Tab tab)
{
var playPage = Element.CurrentPage as NavigationPage;
if (!(playPage.RootPage is PhrasesFrame))
return;
var tabLayout = (TabLayout)ViewGroup.GetChildAt(1);
var playTab = tabLayout.GetTabAt(4);
tab.SetText("Play");
tab.SetIcon(Resource.Drawable.ionicons_2_0_1_play_outline_25);
App.pauseCard = true;
}
void UpdateTab(TabLayout.Tab tab)
{
// To have the logic only on he tab on position 1
if (tab == null || tab.Position != 4)
{
return;
}
if (tab.Text == "Play")
{
tab.SetText("Pause");
tab.SetIcon(Resource.Drawable.ionicons_2_0_1_pause_outline_22);
App.pauseCard = false;
}
else
{
tab.SetText("Play");
tab.SetIcon(Resource.Drawable.ionicons_2_0_1_play_outline_25);
App.pauseCard = true;
}
}
}
}
Current custom renderer using the ToolbarPlacement="Bottom":
namespace Japanese.Droid
{
public class BottomTabPageRenderer : TabbedPageRenderer, BottomNavigationView.IOnNavigationItemSelectedListener, BottomNavigationView.IOnNavigationItemReselectedListener
{
public BottomTabPageRenderer(Context context) : base(context) { }
protected override void OnElementChanged(ElementChangedEventArgs<TabbedPage> e)
{
base.OnElementChanged(e);
// More codes here
}
bool BottomNavigationView.IOnNavigationItemSelectedListener.OnNavigationItemSelected(IMenuItem item)
{
base.OnNavigationItemSelected(item);
UpdateTab(item)
}
void BottomNavigationView.IOnNavigationItemReselectedListener.OnNavigationItemReselected(IMenuItem item)
{
UpdateTab(item);
}
void UpdateTab(IMenuItem item)
{
var playTabId = 4;
var title = item.TitleFormatted.ToString();
if (item == null || item.ItemId != playTabId)
{
return;
}
if (item.ItemId == playTabId)
{
if (title == "Play")
{
item.SetTitle("Pause");
item.SetIcon(Resource.Drawable.ionicons_2_0_1_pause_outline_22);
App.pauseCard = false;
}
else
{
item.SetTitle("Play");
item.SetIcon(Resource.Drawable.ionicons_2_0_1_play_outline_25);
App.pauseCard = true;
}
}
}
}
}
So now my problem is I don't have any idea how will I implement the TabLayout.IOnTabSelectedListener.OnTabUnselected in the new custom renderer.
There is no official stuff for OnTabReselected event for TabbedPage's bottom navigation or
BottomNavigationView because It doesn't use TabLayout.Tab for a start. Many overridden methods of TabbedPageRenderer not being called like SetTabIcon. If you are using IOnTabSelectedListener interface(As your first part of code) you have three methods to use.
void OnTabReselected(Tab tab);
void OnTabSelected(Tab tab);
void OnTabUnselected(Tab tab);
But when it comes to BottomNavigationView interface you have only two methods
void OnNavigationItemReselected
bool OnNavigationItemSelected
So we don't have built in OnTabUnselected method. Here you need to write custom code to make unseleted event.
I have tried this code without using custom renderer using 4 tabs pages & the xaml of tabbed written in MailPage.xaml file. First declare List<string> in App.xaml.cs file to store Title of all tabs
public static List<string> Titles {get;set;}
Add tabs pages title in above list from MainPage.xaml.cs file's OnAppearing method
protected override void OnAppearing()
{
for (int i = 0; i < this.Children.Count; i++)
{
App.Titles.Add(this.Children[i].Title);
}
}
Now go to your MyTabbedPage class in which is available in shared project.
public class MyTabbedPage : Xamarin.Forms.TabbedPage
{
string selectedTab = string.Empty;
string unSelectedTab = string.Empty;
bool isValid;
public MyTabbedPage()
{
On<Xamarin.Forms.PlatformConfiguration.Android>().SetToolbarPlacement(ToolbarPlacement.Bottom);
this.CurrentPageChanged += delegate
{
unSelectedTab = selectedTab;
selectedTab = CurrentPage.Title;
if (App.Titles != null)
isValid = true;
else
App.Titles = new List<string>();
if (isValid)
{
MoveTitles(selectedTab);
//Pass 0 index for tab selected & 1 for tab unselected
var unSelecteTabTitle = App.Titles[1];
//TabEvents(1); here you know which tab unseleted call any method
}
};
}
//This method is for to moving selected title on top of App.Titles list & unseleted tab title automatic shifts at index 1
void MoveTitles(string selected)
{
var holdTitles = App.Titles;
if (holdTitles.Count > 0)
{
int indexSel = holdTitles.FindIndex(x => x.StartsWith(selected));
holdTitles.RemoveAt(indexSel);
holdTitles.Insert(0, selected);
}
App.Titles = holdTitles;
}
}
Or you can make swith case like this
void TabEvents(int index)
{
switch (index)
{
case 0:
//Tab selected
break;
case 1:
//Tab unselected
break;
}
}
Few things I should mention that MainPage.xaml.cs file inheriting MyTabbedPage
public partial class MainPage : MyTabbedPage
Structure of MainPage.xaml file
<?xml version="1.0" encoding="utf-8" ?>
<local:MyTabbedPage
<TabbedPage.Children>
<NavigationPage Title="Browse">
</NavigationPage>
</TabbedPage.Children>
</local:MyTabbedPage>
Answer seems long but hope it help you.
As per G.Hakim's suggestion, I was able to do what I wanted to do by capturing the tab item I wanted to work on and do the necessary actions in BottomNavigationView.IOnNavigationItemSelectedListener.OnNavigationItemSelected.
namespace Japanese.Droid
{
public class BottomTabPageRenderer : TabbedPageRenderer, BottomNavigationView.IOnNavigationItemSelectedListener, BottomNavigationView.IOnNavigationItemReselectedListener
{
// same as above
bool BottomNavigationView.IOnNavigationItemSelectedListener.OnNavigationItemSelected(IMenuItem item)
{
base.OnNavigationItemSelected(item);
if(item.ItemId == 4 && item.TitleFormatted.ToString() == "Play")
{
item.SetTitle("Pause");
item.SetIcon(Resource.Drawable.ionicons_2_0_1_pause_outline_22);
App.pauseCard = false;
playTab = item;
}
if(item.ItemId !=4 && playTab.TitleFormatted.ToString() == "Pause")
{
playTab.SetTitle("Play");
playTab.SetIcon(Resource.Drawable.ionicons_2_0_1_play_outline_25);
App.pauseCard = true;
}
return true;
}
// same as above
}
}

Is there a way to bring up the native iOS and Android busy indicators with Xamarin Forms apps?

I have a Forms app that takes a few seconds to populate data when I click on a viewCell.
Is there a way that I can show a circular busy indicator during this time through custom renderers or something like that?
You can implement the same by using ActivityIndicator control.
If you are expecting have busy-indicators on multiple pages, then would recommend to implement this using the ControlTemplate (it also allows you to define overlays if needed).
Page Template
<Application.Resources>
<ResourceDictionary>
<ControlTemplate x:Key="DefaultTemplate">
<Grid>
<!-- page content -->
<ContentPresenter />
<!-- overlay -->
<BoxView BackgroundColor="Black" Opacity="0.5"
IsVisible="{TemplateBinding BindingContext.IsBusy}"/>
<!-- busy indicator with text -->
<Frame HorizontalOptions="Center" VerticalOptions="Center"
IsVisible="{TemplateBinding BindingContext.IsBusy}">
<StackLayout>
<ActivityIndicator IsRunning="{TemplateBinding BindingContext.IsBusy}" />
<Label Text="{TemplateBinding BindingContext.BusyText}" />
</StackLayout>
</Frame>
</Grid>
</ControlTemplate>
</ResourceDictionary>
</Application.Resources>
Sample usage:
XAML - assign template to page
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
ControlTemplate="{StaticResource DefaultTemplate}"
.. >
....
</ContentPage>
View Model
public class BaseViewModel : ObservableObject
{
bool _isBusy;
public bool IsBusy
{
get => _isBusy;
set => SetProperty(ref _isBusy, value);
}
string _busyText = "loading..";
public string BusyText
{
get => _busyText;
set => SetProperty(ref _busyText, value);
}
}
public class TestViewModel : BaseViewModel
{
public ICommand OnTapCommand {
get => new Command(async (obj) =>
{
IsBusy = true;
//do heavy lifting here
await Task.Delay(2000);
IsBusy = false;
});
}
...
You can use Acr.UserDialogs, it's a cross-platform package with busy indicators, dialogs, toasts, etc.
In your case, you need to use Loading.
using (Acr.UserDialogs.UserDialogs.Instance.Loading("your message here"))
{
//your long task here
}
For example...
I accomplished this by creating an activity indicator control that can be used in my entire app. I even made it so that you can change the activity indicator text to show any text that you want such as 'Logging in', 'loading', 'uploading', etc. See my post below. Let me know if you have any questions.
Is it possible to have one Activity indicator for entire app?
you can use a DependencyService to Show and Hide a loading indicator.
You will have to download AndHUD for android and BTProgressHUD for iOS NuGet packages.
DependencyService interface
using MyApp.Helpers;
namespace MyApp
{
interface IHudService
{
void ShowHud(string ProgressText = StaticData.Loading);
void HideHud();
void SetText(string Text);
void SetProgress(double Progress, string ProgressText = "");
}
}
Android Code
using AndroidHUD;
using Android.Views;
using Xamarin.Forms;
using MyApp.Droid;
using MyApp.DependencyServices;
using MyApp.Helpers;
[assembly: Dependency(typeof(DroidHudService))]
namespace MyApp.Droid
{
public class DroidHudService : IHudService
{
#region IHudManager implementation
bool isHudVisible;
public void ShowHud(string ProgressText = StaticData.Loading)
{
Device.BeginInvokeOnMainThread(() =>
{
AndHUD.Shared.Show(Forms.Context, ProgressText, maskType: MaskType.Black);
isHudVisible = true;
});
}
public void HideHud()
{
Device.BeginInvokeOnMainThread(() =>
{
AndHUD.Shared.Dismiss(Forms.Context);
isHudVisible = false;
});
}
public void SetProgress(double Progress, string ProgressText = "")
{
if (!isHudVisible)
return;
Device.BeginInvokeOnMainThread(() =>
{
int progress = (int)(Progress * 100);
AndHUD.Shared.Show(Forms.Context, ProgressText + progress + "%", progress, MaskType.Black);
});
}
public void SetText(string Text)
{
if (!isHudVisible)
return;
Device.BeginInvokeOnMainThread(() =>
{
AndHUD.Shared.Show(Forms.Context, Text, maskType: MaskType.Black);
});
}
Android.Views.View CustomLoadingView(string ProgressText)
{
Android.Views.View loadingView = new Android.Views.View(Forms.Context);
return loadingView;
}
#endregion
}
}
iOS Code
using System;
using BigTed;
using CoreAnimation;
using CoreGraphics;
using MyApp.DependencyServices;
using MyApp.Helpers;
using MyApp.iOS;
using Foundation;
using UIKit;
using Xamarin.Forms;
[assembly: Dependency(typeof(IosHudService))]
namespace MyApp.iOS
{
public class IosHudService : IHudService
{
UIView _load;
bool isHudVisible;
#region IHudManager implementation
public void ShowHud(string ProgressText = StaticData.Loading)
{
isHudVisible = true;
SetText(ProgressText);
}
public void HideHud()
{
Device.BeginInvokeOnMainThread(() =>
{
BTProgressHUD.Dismiss();
if (_load != null)
_load.Hidden = true;
isHudVisible = false;
});
}
public void SetProgress(double Progress, string ProgressText = "")
{
int progress = (int)(Progress * 100);
string text = ProgressText + progress + "%";
SetText(text);
}
public void SetText(string text)
{
if (!isHudVisible)
return;
Device.BeginInvokeOnMainThread(() =>
{
BTProgressHUD.Show(status: text, maskType: ProgressHUD.MaskType.Black);
try
{
lblTitle.Text = text;
UIView[] subView = ProgressHUD.Shared.Subviews;
for (int i = 0; i < subView.Length; i++)
{
subView[i].Hidden = true;
}
_load.Hidden = false;
ProgressHUD.Shared.BringSubviewToFront(_load);
}
catch (Exception ex)
{
Console.WriteLine("IosHudService.cs - SetText() " + ex.Message);
}
});
}
UILabel lblTitle;
UIView CustomLoadingView(string ProgressText)
{
UIView loadingView = new UIView();
loadingView.Frame = new CGRect(0, 0, UIScreen.MainScreen.Bounds.Width, UIScreen.MainScreen.Bounds.Height);
UIImageView imgBg = new UIImageView();
imgBg.Image = UIImage.FromFile("load_bg.png");
imgBg.Frame = new CGRect((loadingView.Frame.Width / 2) - 65, (loadingView.Frame.Height / 2) - 70, 130, 140);
loadingView.Add(imgBg);
UIImageView someImageView = new UIImageView();
someImageView.Frame = new CGRect((loadingView.Frame.Width / 2) - 40, (loadingView.Frame.Height / 2) - 50, 75, 75);
someImageView.AnimationImages = new UIImage[]
{
UIImage.FromBundle("spinner.png"),
};
someImageView.AnimationRepeatCount = nint.MaxValue; // Repeat forever.
someImageView.AnimationDuration = 1.0; // Every 1s.
someImageView.StartAnimating();
CABasicAnimation rotationAnimation = new CABasicAnimation();
rotationAnimation.KeyPath = "transform.rotation.z";
rotationAnimation.To = new NSNumber(Math.PI * 2);
rotationAnimation.Duration = 1;
rotationAnimation.Cumulative = true;
rotationAnimation.RepeatCount = float.PositiveInfinity;
someImageView.Layer.AddAnimation(rotationAnimation, "rotationAnimation");
loadingView.Add(someImageView);
lblTitle = new UILabel();
lblTitle.Text = ProgressText;
lblTitle.Frame = new CGRect(imgBg.Frame.X, someImageView.Frame.Y + someImageView.Frame.Height + 15, 130, 20);
lblTitle.TextAlignment = UITextAlignment.Center;
lblTitle.TextColor = UIColor.White;
lblTitle.AdjustsFontSizeToFitWidth = true;
loadingView.Add(lblTitle);
return loadingView;
}
#endregion
}
}
Show/Hide via DependencyService Method
public static void ShowLoadingIndicator(string progressText = "Loading...")
{
Device.BeginInvokeOnMainThread(() =>
{
DependencyService.Get<IHudService>().ShowHud(progressText);
});
}
public static void HideLoadingIndicator()
{
Device.BeginInvokeOnMainThread(() =>
{
DependencyService.Get<IHudService>().HideHud();
});
}
I have manage my code by Creating Disposable class and use it in ViewModels like this:
public class Busy : IDisposable
{
readonly object _sync = new object();
readonly BaseViewModel _viewModel;
readonly bool _showProgressView;
public Busy(BaseViewModel viewModel, bool showProgressView, string displayMessage = null)
{
try
{
_viewModel = viewModel;
lock (_sync)
{
_viewModel.IsBusy = true;
_showProgressView = showProgressView;
if (_showProgressView)
{
if (string.IsNullOrEmpty(displayMessage))
{
displayMessage = "Loading...";
}
DependencyService.Get<IHudService>().ShowHud(displayMessage);
}
}
}
catch(Exception ex)
{
ex.Track();
}
}
public void Dispose()
{
try
{
lock (_sync)
{
_viewModel.IsBusy = false;
if (_showProgressView)
{
DependencyService.Get<IHudService>().HideHud();
}
}
}
catch(Exception ex)
{
ex.Track();
}
}
}
Show the loader indicater via using instance of Busy class Like this:
using (new Busy(this, true))
{
//Your api or waiting stuff
}

Buttons inside infowindow doesn't get clicked - Xamarin forms

Hi everyone, i had to create custom info-window in xamarin forms as shown in the screenshot above. I have created a custom renderer for that but the problem i am having right now is that buttons are not getting clicked. Xamarin is taking entire info-window as a clickable view. Please guide me what i am doing wrong or is it possible to achieve button clicks in Xamarin forms. Thanks in advance.
Here is the code for custom renderer:
using System;
using System.Collections.Generic;
using Android.Content;
using Android.Gms.Maps;
using Android.Gms.Maps.Model;
using Xamarin.Forms;
using Xamarin.Forms.Maps;
using Xamarin.Forms.Maps.Android;
using SalesApp.Droid.CustomRenderer;
using Android.Widget;
using SalesApp.CustomControls;
using SalesApp.Droid.Listeners;
[assembly: ExportRenderer(typeof(CustomMap), typeof(CustomMapRenderer))]
namespace SalesApp.Droid.CustomRenderer
{
public class CustomMapRenderer : MapRenderer, GoogleMap.IInfoWindowAdapter, IOnMapReadyCallback
{
GoogleMap map;
List<CustomPin> customPins;
bool isDrawn;
protected override void OnElementChanged(Xamarin.Forms.Platform.Android.ElementChangedEventArgs<Map> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
map.InfoWindowClick -= OnInfoWindowClick;
}
if (e.NewElement != null)
{
var formsMap = (CustomMap)e.NewElement;
customPins = formsMap.CustomPins
Control.GetMapAsync(this);
}
}
public void OnMapReady(GoogleMap googleMap)
{
map = googleMap;
map.InfoWindowClick += OnInfoWindowClick;
map.SetInfoWindowAdapter(this);
}
protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName.Equals("VisibleRegion") && !isDrawn)
{
map.Clear();
if (customPins != null)
{
foreach (var pin in customPins)
{
var marker = new MarkerOptions();
marker.SetPosition(new LatLng(pin.Pin.Position.Latitude, pin.Pin.Position.Longitude));
marker.SetTitle(pin.Pin.Label);
marker.SetSnippet(pin.Pin.Address);
marker.SetIcon(BitmapDescriptorFactory.FromResource((int)typeof(Resource.Drawable).GetField(pin.Image).GetValue(null)));
map.AddMarker(marker);
}
isDrawn = true;
}
}
}
protected override void OnLayout(bool changed, int l, int t, int r, int b)
{
base.OnLayout(changed, l, t, r, b);
if (changed)
{
isDrawn = false;
}
}
void OnInfoWindowClick(object sender, GoogleMap.InfoWindowClickEventArgs e)
{
var customPin = GetCustomPin(e.Marker);
if (customPin == null)
{
throw new Exception("Custom pin not found");
}
//if (!string.IsNullOrWhiteSpace(customPin.Url))
//{
// var url = Android.Net.Uri.Parse(customPin.Url);
// var intent = new Intent(Intent.ActionView, url);
// intent.AddFlags(ActivityFlags.NewTask);
// Android.App.Application.Context.StartActivity(intent);
//}
Android.App.Application.Context.StartActivity(new Intent(Android.App.Application.Context, typeof(DialogActivity)));
}
void IOnMapReadyCallback.OnMapReady(GoogleMap googleMap)
{
InvokeOnMapReadyBaseClassHack(googleMap);
map = googleMap;
map.SetInfoWindowAdapter(this);
map.InfoWindowClick += OnInfoWindowClick;
}
public Android.Views.View GetInfoContents(Marker marker)
{
var inflater = Android.App.Application.Context.GetSystemService(Context.LayoutInflaterService) as Android.Views.LayoutInflater;
if (inflater != null)
{
Android.Views.View view;
var customPin = GetCustomPin(marker);
if (customPin == null)
{
throw new Exception("Custom pin not found");
}
if (customPin.Id == "Xamarin")
{
view = inflater.Inflate(Resource.Layout.XamarinMapInfoWindow, null);
}
else
{
view = inflater.Inflate(Resource.Layout.MapInfoWindow, null);
}
var infoTitle = view.FindViewById<TextView>(Resource.Id.InfoWindowTitle);
var address = view.FindViewById<TextView>(Resource.Id.Address);
var contactPerson = view.FindViewById<TextView>(Resource.Id.ContactPerson);
var phone = view.FindViewById<TextView>(Resource.Id.Phone);
var zip = view.FindViewById<TextView>(Resource.Id.Zip);
var email = view.FindViewById<TextView>(Resource.Id.Email);
var cvr = view.FindViewById<TextView>(Resource.Id.CVR);
var turnover = view.FindViewById<TextView>(Resource.Id.Turnover);
var noOfEmp = view.FindViewById<TextView>(Resource.Id.NoOfEmp);
var leadStatus = view.FindViewById<TextView>(Resource.Id.LeadStatus);
var category = view.FindViewById<TextView>(Resource.Id.Cat);
if (infoTitle != null)
{
infoTitle.Text = customPin.LeedsAndCustomersData.FullName;
}
if (address != null)
{
address.Text = customPin.LeedsAndCustomersData.Address + " " + customPin.LeedsAndCustomersData.CityName;
}
if (contactPerson != null)
{
contactPerson.Text = customPin.LeedsAndCustomersData.FirstName;
}
if (phone != null)
{
phone.Text = customPin.LeedsAndCustomersData.Phone;
}
if (zip != null)
{
zip.Text = customPin.LeedsAndCustomersData.ZipCode;
}
if (email != null)
{
email.Text = customPin.LeedsAndCustomersData.Email;
}
if (cvr != null)
{
cvr.Text = customPin.LeedsAndCustomersData.CVR;
}
if (turnover != null)
{
turnover.Text = customPin.LeedsAndCustomersData.Turnover;
}
if (noOfEmp != null)
{
noOfEmp.Text = customPin.LeedsAndCustomersData.NoOfEmployees.ToString();
}
if (leadStatus != null)
{
leadStatus.Text = customPin.LeedsAndCustomersData.LeadStatus;
}
if (category != null)
{
category.Text = customPin.LeedsAndCustomersData.BusinessType;
}
//add listeners
OnTouchPhoneListener callButtonListener = new OnTouchPhoneListener();
phone.SetOnTouchListener(callButtonListener);
return view;
}
return null;
}
public Android.Views.View GetInfoWindow(Marker marker)
{
return null;
}
CustomPin GetCustomPin(Marker annotation)
{
var position = new Position(annotation.Position.Latitude, annotation.Position.Longitude);
foreach (var pin in customPins)
{
if (pin.Pin.Position == position)
{
return pin;
}
}
return null;
}
void InvokeOnMapReadyBaseClassHack(GoogleMap googleMap)
{
System.Reflection.MethodInfo onMapReadyMethodInfo = null;
Type baseType = typeof(MapRenderer);
foreach (var currentMethod in baseType.GetMethods(System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance |
System.Reflection.BindingFlags.DeclaredOnly))
{
if (currentMethod.IsFinal && currentMethod.IsPrivate)
{
if (string.Equals(currentMethod.Name, "OnMapReady", StringComparison.Ordinal))
{
onMapReadyMethodInfo = currentMethod;
break;
}
if (currentMethod.Name.EndsWith(".OnMapReady", StringComparison.Ordinal))
{
onMapReadyMethodInfo = currentMethod;
break;
}
}
}
if (onMapReadyMethodInfo != null)
{
onMapReadyMethodInfo.Invoke(this, new[] { googleMap });
}
}
}
}
OnInfoWindowElemTouchListener:
using Android.OS;
using Android.Views;
using Android.Widget;
using System.Threading.Tasks;
using Android.Gms.Maps.Model;
using Android.Graphics.Drawables;
using Java.Lang;
namespace SalesApp.Droid.Listeners
{
public abstract class OnInfoWindowElemTouchListener : Java.Lang.Object, View.IOnTouchListener
{
private View view;
private Drawable bgDrawableNormal;
private Drawable bgDrawablePressed;
private Handler handler = new Handler();
private Marker marker;
private static bool endPressStatus = false;
private bool pressed = false;
//public OnInfoWindowElemTouchListener(View view, Drawable bgDrawableNormal, Drawable bgDrawablePressed)
//{
// this.view = this.view;
// this.bgDrawableNormal = this.bgDrawableNormal;
// this.bgDrawablePressed = this.bgDrawablePressed;
//}
public OnInfoWindowElemTouchListener(View view)
{
this.view = this.view;
}
public OnInfoWindowElemTouchListener(Button button)
{
}
public OnInfoWindowElemTouchListener()
{
}
public void setMarker(Marker marker)
{
this.marker = this.marker;
}
/*public bool OnTouch(View v, MotionEvent e)
{
if (e.Action == MotionEventActions.Down)
{
// do stuff
return true;
}
if (e.Action == MotionEventActions.Up)
{
// do other stuff
return true;
}
return false;
}*/
public bool OnTouch(View vv, MotionEvent e)
{
if (0 <= e.GetX() && e.GetX() <= vv.Width && 0 <= e.GetY() && e.GetY() <= vv.Height)
{
switch (e.ActionMasked)
{
case MotionEventActions.Down:
startPress();
break;
// We need to delay releasing of the view a little so it shows the
// pressed state on the screen
case MotionEventActions.Up:
//handler.PostDelayed(ConfirmClickRunnable, 150);
//Task.Factory.StartNew(() =>onClickConfirmed(view, marker));
Task.Factory.StartNew(() => onClickConfirmed());
Task.Delay(150);
break;
case MotionEventActions.Cancel:
endPress();
break;
default:
break;
}
}
else
{
// If the touch goes outside of the view's area
// (like when moving finger out of the pressed button)
// just release the press
endPress();
}
return false;
}
private void startPress()
{
if (!pressed)
{
pressed = true;
//handler.RemoveCallbacks(ConfirmClickRunnable);
if ((marker != null))
{
marker.ShowInfoWindow();
}
}
}
public bool endPress()
{
if (pressed)
{
this.pressed = false;
//handler.RemoveCallbacks(ConfirmClickRunnable);
view.SetBackgroundColor(Android.Graphics.Color.Green);
if ((marker != null))
{
marker.ShowInfoWindow();
}
endPressStatus = true;
return true;
}
else
{
endPressStatus = false;
return false;
}
}
//private Runnable confirmClickRunnable = new RunnableAnonymousInnerClassHelper(this);
private Runnable ConfirmClickRunnable = new Java.Lang.Runnable(() =>
{
if (endPressStatus)
{
//onClickConfirmed(view, marker);
}
});
/*private class RunnableAnonymousInnerClassHelper : Java.Lang.Object, Java.Lang.IRunnable
{
private readonly Context outerInstance;
public RunnableAnonymousInnerClassHelper(Context outerInstance)
{
this.outerInstance = outerInstance;
}
public void Run()
{
if (endPressStatus)
{
onClickConfirmed();
}
}
}*/
//public abstract void onClickConfirmed(View v, Marker marker);
public abstract void onClickConfirmed();
}
}
OnTouchPhoneListener:
using Android.Widget;
using System;
namespace SalesApp.Droid.Listeners
{
public class OnTouchPhoneListener : OnInfoWindowElemTouchListener
//public class OnTouchPhoneListener
{
Button button;
public OnTouchPhoneListener(Button button)
{
}
public OnTouchPhoneListener()
{
}
public override void onClickConfirmed() {
Console.WriteLine("call Button Clicked");
}
}
}
Why is the info window implemented in your custom renderer? This makes the class CustomMapRenderer do (at least) do two things, violate the SRP and makes the code way harder to understand.
Instead I'd go with the custom renderer just for the map. Then in Xamarin.Forms you can implement the overlay with Xamarin.Forms means (e.g. the map view in an absolute or relative layout and the overlay in the same layout but a reduced size). Please see the following pseudo-XAML
<ContentPage [...]>
<AbsoluteLayout>
<local:MapView x:Name="MapView" AbsoluteLayout.LayoutFlags="All" AbsoluteLayout.LayoutBounds="0,0,1,1" />
<local:MapOverlay AbsoluteLayout.LayoutFlags="All" AbsoluteLayout.LayoutBounds=".5, .5, .5, .5" />
</AbsoluteLayout>
</ContentPage>
Then you can introduce a viewmodel PinInfoViewModel which is exposed by MapView.SelectedPinInfo and can be set as MapOverlay.BindingContext
<local:MapOverlay AbsoluteLayout.LayoutFlags="All" AbsoluteLayout.LayoutBounds=".5, .5, .5, .5" BindingContext="{Binding Source={x:Reference MapView}, Path=SelectedPinInfo}" />
MapOverlay - in turn - binds to all the properties of PinInfoViewModel. Last thing we'll have to do is making the overlay invisible if no pin is selected. For this purpose we expose MapView.IsPinSelected and bind MapOverlay.IsVisible to it
<local:MapOverlay AbsoluteLayout.LayoutFlags="All" AbsoluteLayout.LayoutBounds=".5, .5, .5, .5" BindingContext="{Binding Source={x:Reference MapView}, Path=SelectedPinInfo}" IsVisible="{Binding Source=MapView, Path=IsPinSelected}" />
While up to this point we have not done anything to solve your issue, you can now implement the overlay way simpler, e.g. with a StackLayout. And you can bind the Buttons commands to do whatever you'd like to do.

Xamarin Forms Add shadow on MasterDetailPage content page

I'm using a MasterDetailPage, where both the Menu and the Content page have a white background. So I need to add a shadow separator to the Content page when the menu is showing. Like this:
The only example I could find is this: https://gist.github.com/SeeD-Seifer/120971b4dda7a785a7f4bda928c9dc2b
I've implemented the code, and the shadow effect works on Labels, Images and other elements. But I cannot get it to work on a NavigationPage.
My code:
ShadowEffect.cs
public class ShadowEffect : RoutingEffect
{
public float Radius { get; set; }
public Color Color { get; set; }
public float DistanceX { get; set; }
public float DistanceY { get; set; }
public ShadowEffect() : base("MyCompany.PanelShadowEffect")
{
}
}
ShadowNavigationPage.cs
public ShadowNavigationPage(Page root) : base(root)
{
}
protected override void OnAppearing()
{
base.OnAppearing();
Effects.Add(new ShadowEffect()
{
Radius = 0,
DistanceX = -20,
DistanceY = 0,
Color = Color.Black
});
}
PanelShadowEffect
[assembly: ResolutionGroupName("MyCompany")]
[assembly: ExportEffect(typeof(PanelShadowEffect), "PanelShadowEffect")]
namespace MyApp.iOS.Renderer
{
public class PanelShadowEffect : PlatformEffect
{
protected override void OnAttached()
{
try
{
var effect = (ShadowEffect)Element.Effects.FirstOrDefault(e => e is ShadowEffect);
if (effect == null)
{
return;
}
var control = Control;
if (control == null)
{
var renderer = Platform.GetRenderer((VisualElement)Element);
control = renderer.ViewController.View;
}
control.Layer.CornerRadius = effect.Radius;
control.Layer.ShadowColor = effect.Color.ToCGColor();
control.Layer.ShadowOffset = new CGSize(effect.DistanceX, effect.DistanceY);
control.Layer.ShadowOpacity = .5f;
control.Layer.MasksToBounds = false;
// This doesn't work either
//Container.Layer.CornerRadius = effect.Radius;
//Container.Layer.ShadowColor = effect.Color.ToCGColor();
//Container.Layer.ShadowOffset = new CGSize(effect.DistanceX, effect.DistanceY);
//Container.Layer.ShadowOpacity = .5f;
//Container.Layer.MasksToBounds = false;
}
catch (Exception ex)
{
Console.WriteLine("Cannot set property on attached control. Error: {0}", ex.Message);
}
}
protected override void OnDetached()
{
}
}
}
I'm pretty sure that you're attaching the effect to a wrong control or in a wrong place. I got it to work by subscribing to the Appearing event of the NavigationPage (as seen in MainPage.xaml) and attaching the effect there.
PanelShadowEffect.cs
[assembly: ResolutionGroupName("MasterDetailPageNavigation")]
[assembly: ExportEffect(typeof(PanelShadowEffect), "PanelShadowEffect")]
namespace MasterDetailPageNavigation.iOS
{
public class PanelShadowEffect : PlatformEffect
{
protected override void OnAttached()
{
try
{
var effect = (ShadowEffect)Element.Effects.FirstOrDefault(e => e is ShadowEffect);
if (effect == null)
{
return;
}
Container.Layer.CornerRadius = effect.Radius;
Container.Layer.ShadowColor = effect.Color.ToCGColor();
Container.Layer.ShadowOffset = new CGSize(effect.DistanceX, effect.DistanceY);
Container.Layer.ShadowOpacity = .5f;
Container.Layer.MasksToBounds = false;
}
catch (Exception ex)
{
Console.WriteLine("Cannot set property on attached control. Error: {0}", ex.Message);
}
}
protected override void OnDetached()
{
}
}
}
MainPage.xaml
<?xml version="1.0" encoding="UTF-8"?>
<MasterDetailPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:effects="clr-namespace:MasterDetailPageNavigation;assembly=MasterDetailPageNavigation"
xmlns:local="clr-namespace:MasterDetailPageNavigation;assembly=MasterDetailPageNavigation"
x:Class="MasterDetailPageNavigation.MainPage">
<MasterDetailPage.Master>
<local:MasterPage x:Name="masterPage" />
</MasterDetailPage.Master>
<MasterDetailPage.Detail>
<NavigationPage x:Name="NaviPage" Appearing="NavigationPage_Appearing">
<x:Arguments>
<local:ContactsPage />
</x:Arguments>
</NavigationPage>
</MasterDetailPage.Detail>
</MasterDetailPage>
MainPage.xaml.cs
void NavigationPage_Appearing(object sender, System.EventArgs e)
{
NaviPage.Effects.Add(new ShadowEffect()
{
Radius = 0,
DistanceX = -20,
DistanceY = 0,
Color = Color.Black
});
}
Here's the result:

Resources