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:
Related
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
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
}
}
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
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
}
Please find the code below for the custom renderer for Android for segmented control. I copied this from one post in stack overflow. It works fine for IOS but fails for android. Anyone has any idea? Am i missing anything.
This is the event
protected override void OnElementChanged(ElementChangedEventArgs<SegmentedControl> e)
{
base.OnElementChanged(e);
var layoutInflater = (LayoutInflater)Context.GetSystemService(Context.LayoutInflaterService);
var g = new RadioGroup(Context);
g.Orientation = Orientation.Horizontal;
g.CheckedChange += (sender, eventArgs) =>
{
var rg = (RadioGroup)sender;
if (rg.CheckedRadioButtonId != -1)
{
var id = rg.CheckedRadioButtonId;
var radioButton = rg.FindViewById(id);
var radioId = rg.IndexOfChild(radioButton);
var btn = (RadioButton)rg.GetChildAt(radioId);
var selection = (String)btn.Text;
e.NewElement.SelectedValue = selection;
}
};
for (var i = 0; i < e.NewElement.Children.Count; i++)
{
var o = e.NewElement.Children[i];
var v = (SegmentedControlButton)layoutInflater.Inflate(Resource.Layout.SegmentedControl, null);
//Error at this above line
v.Text = o.Text;
if (i == 0)
v.SetBackgroundResource(Resource.Drawable.segmented_control_first_background);
else if (i == e.NewElement.Children.Count - 1)
v.SetBackgroundResource(Resource.Drawable.segmented_control_last_background);
g.AddView(v);
}
SetNativeControl(g);
}
}
Its happening at this line.
var v = (SegmentedControlButton)layoutInflater.Inflate(Resource.Layout.SegmentedControl, null);
v.Text = o.Text;
Error
Android.Views.InflateException: Binary XML file line #1: Binary XML file line #1:
Error inflating class SegmentedControl.Android.SegmentedControlButton ---> Android.Views.InflateException:
Binary XML file line #1: Error inflating class SegmentedControl.Android.SegmentedControlButton ---> Java.Lang.ClassNotFoundException:
Didn't find class "SegmentedControl.Android.SegmentedControlButton" on path: DexPathList[[zip file "/data/app/com.mytestapp.myfirstapp-1/base.apk"],
nativeLibraryDirectories=[/data/app/com.mytestapp.myfirstapp-1/lib/x86, /data/app/com.mytestapp.myfirstapp-1/base.apk!/lib/x86, /vendor/lib, /system/lib]]
Try following SegmentControlRenderer. I have added required files as well.
Use following in your xaml file
<StackLayout BackgroundColor="#0A0E3F" Padding="10" Spacing="0" Grid.Row="0">
<local:CustomSegmentedControl SelectedValue="Offices" x:Name="segmentControl" HorizontalOptions="FillAndExpand">
<local:CustomSegmentedControl.Children>
<local:CustomSegmentedControlOption Text="Control1" />
<local:CustomSegmentedControlOption Text="Control2" />
<local:CustomSegmentedControlOption Text="Control3" />
</local:CustomSegmentedControl.Children>
</local:CustomSegmentedControl>
</StackLayout>
Make sure you place all these 7 files in proper directories. Do let me if anything comes up
SegmentedControlRenderer.cs (to be placed in Droid modules)
using System;
using Xamarin.Forms.Platform.Android;
using Android.Widget;
using Android.Content;
using Android.Util;
using Android.Graphics;
using Android.Views;
using System.Collections.Generic;
[assembly: Xamarin.Forms.ExportRenderer(typeof(App.CustomSegmentedControl), typeof(App.Droid.SegmentedControlRenderer))]
namespace App.Droid
{
public class SegmentedControlRenderer : ViewRenderer<CustomSegmentedControl, RadioGroup>
{
RadioGroup g = null;
List<CustomSegmentedControlButton> listSegmentControl = new List<CustomSegmentedControlButton>();
public SegmentedControlRenderer()
{
}
protected override void OnConfigurationChanged(Android.Content.Res.Configuration newConfig)
{
base.OnConfigurationChanged(newConfig);
if (listSegmentControl == null)
return;
foreach (var control in listSegmentControl)
{
control.SetWidth(Resources.DisplayMetrics.WidthPixels / listSegmentControl.Count);
}
}
protected override void OnElementChanged(ElementChangedEventArgs<TPSegmentedControl> e)
{
base.OnElementChanged(e);
var layoutInflater = (LayoutInflater)Context.GetSystemService(Context.LayoutInflaterService);
g = new RadioGroup(Context);
g.Orientation = Orientation.Horizontal;
g.CheckedChange += (sender, eventArgs) =>
{
var rg = (RadioGroup)sender;
if (rg.CheckedRadioButtonId != -1)
{
var id = rg.CheckedRadioButtonId;
var radioButton = rg.FindViewById(id);
var radioId = rg.IndexOfChild(radioButton);
var btn = (RadioButton)rg.GetChildAt(radioId);
for (int i = 0; i < g.ChildCount; i++)
{
g.GetChildAt(i).SetBackgroundResource(Resource.Drawable.segment_control_option_bg);
}
btn.SetBackgroundResource(btn.Checked ? Resource.Drawable.segment_control_selected_option_bg : Resource.Drawable.segment_control_option_bg);
var selection = (String)btn.Text;
e.NewElement.SelectedValue = selection;
}
};
for (var i = 0; i < e.NewElement.Children.Count; i++)
{
var o = e.NewElement.Children[i];
var v = (TPSegmentedControlButton)layoutInflater.Inflate(Resource.Layout.SegmentedControl, null);
v.Text = o.Text;
int minWidth = Resources.DisplayMetrics.WidthPixels / e.NewElement.Children.Count;
v.SetWidth(minWidth);
v.SetBackgroundResource(v.Checked ? Resource.Drawable.segment_control_selected_option_bg : Resource.Drawable.segment_control_option_bg);
g.AddView(v);
listSegmentControl.Add(v);
}
try
{
g.GetChildAt(0).PerformClick();
}
catch (Exception ex)
{
}
SetNativeControl(g);
}
}
public class CustomSegmentedControlButton : RadioButton
{
private int lineHeightSelected;
private int lineHeightUnselected;
private Paint linePaint;
public CustomSegmentedControlButton(Context context) : this(context, null)
{
}
public CustomSegmentedControlButton(Context context, IAttributeSet attributes) : this(context, attributes, Resource.Attribute.segmentedControlOptionStyle)
{
}
public CustomSegmentedControlButton(Context context, IAttributeSet attributes, int defStyle) : base(context, attributes, defStyle)
{
Initialize(attributes, defStyle);
}
private void Initialize(IAttributeSet attributes, int defStyle)
{
var a = this.Context.ObtainStyledAttributes(attributes, Resource.Styleable.SegmentedControlOption, defStyle, Resource.Style.SegmentedControlOption);
var lineColor = Color.ParseColor("#4aa3f4");
linePaint = new Paint();
linePaint.Color = lineColor;
lineHeightUnselected = a.GetDimensionPixelSize(Resource.Styleable.SegmentedControlOption_lineHeightUnselected, 0);
lineHeightSelected = a.GetDimensionPixelSize(Resource.Styleable.SegmentedControlOption_lineHeightSelected, 0);
a.Recycle();
}
protected override void OnDraw(Canvas canvas)
{
base.OnDraw(canvas);
if (linePaint.Color != 0 && (lineHeightSelected > 0 || lineHeightUnselected > 0))
{
var lineHeight = Checked ? lineHeightSelected : lineHeightUnselected;
if (lineHeight > 0)
{
var rect = new Rect(0, Height - lineHeight, Width, Height);
canvas.DrawRect(rect, linePaint);
}
}
}
}
}
CustomSegmentedControl.cs (To be placed in shared app module)
using System;
using Xamarin.Forms;
using System.Collections.Generic;
namespace App
{
public class CustomSegmentedControl : View, IViewContainer<CustomSegmentedControlOption>
{
public IList<CustomSegmentedControlOption> Children { get; set; }
public TPSegmentedControl()
{
Children = new List<CustomSegmentedControlOption>();
}
public event ValueChangedEventHandler ValueChanged;
public delegate void ValueChangedEventHandler(object sender, EventArgs e);
private string selectedValue;
public string SelectedValue
{
get { return selectedValue; }
set
{
selectedValue = value;
if (ValueChanged != null)
ValueChanged(this, EventArgs.Empty);
}
}
}
public class CustomSegmentedControlOption : View
{
public static readonly BindableProperty TextProperty = BindableProperty.Create<CustomSegmentedControlOption, string>(p => p.Text, "");
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public TPSegmentedControlOption()
{
}
}
}
CustomSegmentedView.cs (To be placed in share app module)
using System;
using XLabs.Forms.Controls;
namespace App
{
public class CustomSegmentedView : SegmentedControlView
{
public ISegmentedControlView Listener { get; set;}
public CustomSegmentedView()
{
}
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == "SelectedItem") {
if (Listener == null)
return;
Listener.SegmentedControlOnValueChanged(SelectedItem);
}
}
}
public interface ISegmentedControlView {
void SegmentedControlOnValueChanged(int selectedIndex);
}
}
SegmentedControl.axml (To be placed in Droid/Resources/layout)
<?xml version="1.0" encoding="utf-8"?>
<App.Droid.CustomSegmentedControlButton
style="#style/SegmentedControlOption" />
attrs.xml (To be placed in Droid/Resources/values)
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="SegmentedControlOption">
<attr name="segmentedControlOptionStyle" format="string" />
<attr name="lineColor" format="color" />
<attr name="lineHeightUnselected" format="dimension" />
<attr name="lineHeightSelected" format="dimension" />
</declare-styleable>
<declare-styleable name="ScaleImageView">
</declare-styleable>
</resources>
segment_control_option_bg.xml (To be placed in Droid/Resources/layout)
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#4aa3f4" />
<stroke
android:width="0.5dp"
android:color="#0a0e3f" />
<corners
android:bottomLeftRadius="0dp"
android:bottomRightRadius="0dp"
android:topLeftRadius="0dp"
android:topRightRadius="0dp" />
</shape>
segment_control_selected_option_bg.xml (To be placed in Droid/Resources/layout)
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#0271d5" />
<stroke
android:width="0.5dp"
android:color="#0a0e3f" />
<corners
android:bottomLeftRadius="0dp"
android:bottomRightRadius="0dp"
android:topLeftRadius="0dp"
android:topRightRadius="0dp" />
</shape>
The problem was in the segmented control.axml i gave the wrong namespace.
<?xml version="1.0" encoding="utf-8"?>
<*MyFirstApp*.Droid.SegmentedControlButton
style="#style/SegmentedControlOption" />
It solved everything. #Needle in the haystack#
#user12345 i already have the correct code in post whatever you mentioned. Thank you for your neat explaination.