Xamarin IOS Custom Renderer overriden Draw method not called - xamarin

I am trying to load a customized slider control in a listview (with accordeon behaviour). When the View loads all the listview elements are collapsed so the slider control visibility is false. I observed that the overriden Draw method within the ios renderer is not called while the control is not visible so I end up having the native control within my listview.
I have reproduced the issue in a separate project:
I have the IOS custom renderer:
public class CustomGradientSliderRenderer : SliderRenderer
{
public CGColor StartColor { get; set; }
public CGColor CenterColor { get; set; }
public CGColor EndColor { get; set; }
protected override void OnElementChanged(ElementChangedEventArgs<Slider> e)
{
if (Control == null)
{
var customSlider = e.NewElement as CustomGradientSlider;
StartColor = customSlider.StartColor.ToCGColor();
CenterColor = customSlider.CenterColor.ToCGColor();
EndColor = customSlider.EndColor.ToCGColor();
var slider = new SlideriOS
{
Continuous = true,
Height = (nfloat)customSlider.HeightRequest
};
SetNativeControl(slider);
}
base.OnElementChanged(e);
}
public override void Draw(CGRect rect)
{
base.Draw(rect);
if (Control != null)
{
Control.SetMinTrackImage(CreateGradientImage(rect.Size), UIControlState.Normal);
}
}
void OnControlValueChanged(object sender, EventArgs eventArgs)
{
((IElementController)Element).SetValueFromRenderer(Slider.ValueProperty, Control.Value);
}
public UIImage CreateGradientImage(CGSize rect)
{
var gradientLayer = new CAGradientLayer()
{
StartPoint = new CGPoint(0, 0.5),
EndPoint = new CGPoint(1, 0.5),
Colors = new CGColor[] { StartColor, CenterColor, EndColor },
Frame = new CGRect(0, 0, rect.Width, rect.Height),
CornerRadius = 5.0f
};
UIGraphics.BeginImageContext(gradientLayer.Frame.Size);
gradientLayer.RenderInContext(UIGraphics.GetCurrentContext());
var image = UIGraphics.GetImageFromCurrentImageContext();
UIGraphics.EndImageContext();
return image.CreateResizableImage(UIEdgeInsets.Zero);
}
}
public class SlideriOS : UISlider
{
public nfloat Height { get; set; }
public override CGRect TrackRectForBounds(CGRect forBounds)
{
var rect = base.TrackRectForBounds(forBounds);
return new CGRect(rect.X, rect.Y, rect.Width, Height);
}
}
The View with codebehind:
Main.xaml:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
x:Class="GradientSlider.MainPage"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:GradientSlider">
<ContentPage.Content>
<Grid>
<StackLayout x:Name="SliderContainer">
<local:CustomGradientSlider
x:Name="mySlider"
CenterColor="#feeb2f"
CornerRadius="16"
EndColor="#ba0f00"
HeightRequest="20"
HorizontalOptions="FillAndExpand"
Maximum="10"
Minimum="0"
StartColor="#6bab29"
VerticalOptions="CenterAndExpand"
MaximumTrackColor="Transparent"
ThumbColor="green"
/>
<Label x:Name="lblText" Text="txt"
VerticalOptions="Center" HorizontalOptions="Center"/>
</StackLayout>
<Button Text="Magic" Clicked="Button_Tapped" WidthRequest="100" HeightRequest="50" VerticalOptions="Center" HorizontalOptions="Center"/>
</Grid>
</ContentPage.Content>
</ContentPage>
Main.xaml.cs:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
namespace GradientSlider
{
public partial class MainPage : ContentPage, INotifyPropertyChanged
{
public MainPage()
{
InitializeComponent();
SliderContainer.IsVisible = false;
}
void Button_Tapped(object sender,ClickedEventArgs a)
{
SliderContainer.IsVisible = !SliderContainer.IsVisible;
}
}
}
So in the scenario above you can see that when I load the main.xaml the control is invisible (SliderContainer.IsVisible = false;) in this case I get a native slider control and not my custom one. If I change in the constructor SliderContainer.IsVisible = true; then I get my custom control.
After an investigation I realised that if the control is not visible when the view loads the public override void Draw(CGRect rect) is not called. I could not find any solution to trigger the Draw method while the control is invisible.
Anybody has an idea how to load a custom renderer correctly while the control is not visible ?
Thank you!

Assuming the renderer is overriding OnElementPropertyChanged:
protected override void OnElementChanged(ElementChangedEventArgs<MyFormsSlider> e)
{
if (e.NewElement != null)
{
if (Control == null)
{
// Instantiate the native control and assign it to the Control property with
// the SetNativeControl method
SetNativeControl(new MyNativeControl(...
...
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
//assuming MyFormsSlider derives from View / VisualElement; the latter has IsVisibleProperty
if (e.PropertyName == MyFormsSlider.IsVisibleProperty.PropertyName)
{
//Control is the control set with SetNativeControl
Control. ...
}
...
}

Related

Xamarin WebView GestureRecognizer not working

I have an awkward problem with GestureRecognizers on Xamarin WebView:
Although the documentation any some questions/answers here and in Xamarin Forum say that WebView GestureRecognizers should all work, I can't get it to fire any event.
My XAML code looks like this:
<StackLayout BackgroundColor="LightGray" >
<WebView x:Name="webView" VerticalOptions="FillAndExpand" >
<WebView.GestureRecognizers>
<SwipeGestureRecognizer Direction="Left" Swiped="onSwiped"/>
</WebView.GestureRecognizers>
<WebView.Source>
<HtmlWebViewSource Html="{Binding HTML}" />
</WebView.Source>
</WebView>
</StackLayout>
Alternatives treid so far:
Same GestureRecognizer on the Title of the same page: works
Same GestureRecognizer on a ListView of another page: works
Tried Nuget package Vapolia.XamarinGestures which also didn't work on the webview
Tried to put the GestureRecoginzer on the StackLayout around the WebView: didn't work either.
Tried it on iOS device and simulator. Normally iOS should be the easy part here...
What I actually want to achieve: with a swipe left move forward to another (programatically defined) web page.
I assume those gestures are somehow absorbed by the webview for regular navigation, but I was wondering why some examples would say that all gestures work on the webview.
An alternative could be to add that target webpage to the webview history stack on the "forward" path.. but not sure how to do that.
Anyone has some hints?
You could use Custom Renderer to add the swipe event on specific platform. And handle them in Forms .
in Forms
create a CustomWebView
public class CustomWebView : WebView
{
public event EventHandler SwipeLeft;
public event EventHandler SwipeRight;
public void OnSwipeLeft() =>
SwipeLeft?.Invoke(this, null);
public void OnSwipeRight() =>
SwipeRight?.Invoke(this, null);
}
in Android
using Android.Content;
using Android.Views;
using App11;
using App11.Droid;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
[assembly: ExportRenderer(typeof(CustomWebView), typeof(MyWebViewRenderer))]
namespace App11.Droid
{
public class MyWebViewRenderer : WebViewRenderer
{
public MyWebViewRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
{
base.OnElementChanged(e);
Control.SetOnTouchListener(new MyOnTouchListener((CustomWebView)Element));
}
}
public class MyOnTouchListener : Java.Lang.Object, Android.Views.View.IOnTouchListener
{
float oldX;
float newX;
CustomWebView myWebView;
public MyOnTouchListener(CustomWebView webView)
{
myWebView = webView;
}
public bool OnTouch(Android.Views.View v, MotionEvent e)
{
if (e.Action == MotionEventActions.Down)
{
oldX = e.GetX(0);
}
if (e.Action == MotionEventActions.Up)
{
newX = e.GetX();
if (newX - oldX > 0)
{
myWebView.OnSwipeRight();
}
else
{
myWebView.OnSwipeLeft();
}
}
return false;
}
}
}
in iOS
using App11;
using App11.iOS;
using Foundation;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
using ObjCRuntime;
[assembly: ExportRenderer(typeof(CustomWebView), typeof(MyWebViewRenderer))]
namespace App11.iOS
{
public class MyWebViewRenderer:WkWebViewRenderer
{
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
if(e.NewElement!=null)
{
this.BackgroundColor = UIColor.Red;
UISwipeGestureRecognizer leftgestureRecognizer = new UISwipeGestureRecognizer(this,new Selector("SwipeEvent:"));
leftgestureRecognizer.Direction = UISwipeGestureRecognizerDirection.Left;
UISwipeGestureRecognizer rightgestureRecognizer = new UISwipeGestureRecognizer(this, new Selector("SwipeEvent:"));
rightgestureRecognizer.Direction = UISwipeGestureRecognizerDirection.Right;
leftgestureRecognizer.Delegate = new MyWebViewDelegate();
rightgestureRecognizer.Delegate = new MyWebViewDelegate();
this.AddGestureRecognizer(leftgestureRecognizer);
this.AddGestureRecognizer(rightgestureRecognizer);
}
}
[Export("SwipeEvent:")]
void SwipeEvent(UISwipeGestureRecognizer recognizer)
{
var webview = Element as CustomWebView;
if(recognizer.Direction == UISwipeGestureRecognizerDirection.Left)
{
webview.OnSwipeLeft();
}
else if(recognizer.Direction == UISwipeGestureRecognizerDirection.Right)
{
webview.OnSwipeRight();
}
}
}
public class MyWebViewDelegate: UIGestureRecognizerDelegate
{
public override bool ShouldRecognizeSimultaneously(UIGestureRecognizer gestureRecognizer, UIGestureRecognizer otherGestureRecognizer)
{
return false;
}
}
}
Now you just need to use it like
<local:CustomWebView x:Name="browser"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand"
SwipeLeft="browser_SwipeLeft"
SwipeRight="browser_SwipeRight">
There was an additional trick to make it finally work. All the above (correct) solution was ignored due to my Xamarin MasterDetailPage setup.
This was capturing all horizontal swipes and not putting them through to the HybridWebView.
MasterDetailPage.IsGestureEnabled = false;
finally fixed it and enabled the swipe gestures in my WebView.

Xamarin iOS webview underlap behind navigation bar

I am using xamarin custom webview to load my page in app. But facing issue that title of webpage hides behind navigation bar . Or sometimes bottom of page not shown. I have tried adding scrollbar to my layout but still facing issue. Same works perfectly on android. Is it due to custom webview? I just want my webview to start below navigation bar and load completely according to device size.
my custom webview code :
public class CustomWebView : WebView
{
public static readonly BindableProperty UriProperty = BindableProperty.Create(
propertyName: "Uri",
returnType: typeof(string),
declaringType: typeof(CustomWebView),
defaultValue: default(string));
public string Uri
{
get { return (string)GetValue(UriProperty); }
set { SetValue(UriProperty, value); }
}
}
Xaml Page :
<StackLayout Orientation="Vertical" HorizontalOptions="StartAndExpand" VerticalOptions="StartAndExpand">
<StackLayout>
<Label x:Name="type" Text="Loading..." FontSize="Medium"/>
</StackLayout>
<StackLayout VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand">
<ScrollView Orientation="Vertical" FlowDirection="MatchParent" HorizontalOptions="StartAndExpand" VerticalOptions="StartAndExpand" Visual="Material" VerticalScrollBarVisibility="Always">
<OnPlatform x:TypeArguments="View">
<On Platform="Android">
<WebView x:Name="dashboard_android" HeightRequest="1000" WidthRequest="1000" />
</On>
<On Platform="iOS">
<local:CustomWebView x:Name="dashboard_ios" VerticalOptions="StartAndExpand" HorizontalOptions="FillAndExpand" WidthRequest="1000" HeightRequest="1000"/>
</On>
</OnPlatform>
</ScrollView>
</StackLayout>
</StackLayout>
code behind :
dashboard_android.Source = url;
dashboard_ios.Uri = url;
Following are solutions i have tried but no success
Solution 1 :
I have tried adding two properties, but no use
this.EdgesForExtendedLayout = UIRectEdge.None;
this.ExtendedLayoutIncludesOpaqueBars = false;
Solution 2 :
Tried enabling this unsafe area property , still no success
ios:Page.UseSafeArea="true"
Solution 3 :
Tried setting webview height on content size dynamically , but no success
public override async void DidFinishNavigation(WKWebView webView, WKNavigation navigation)
{
// base.DidFinishNavigation(webView, navigation);
var wv = _webViewRenderer.Element as CustomWebView;
if (wv != null)
{
await System.Threading.Tasks.Task.Delay(100); // wait here till content is rendered
wv.HeightRequest = (double)webView.Frame.Size.Height; // ScrollView.ContentSize.Height;
}
}
Updated Xaml Code :
<StackLayout VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand">
<local:CustomWebView x:Name="dashboard" HeightRequest="1000" WidthRequest="1000" />
</StackLayout>
Updated Code behind :
public partial class DashboardView : ContentPage
{
string url;
public DashboardView()
{
InitializeComponent();
url= ""; //adding url to load here
dashboard.Uri = url;
}
}
Custom WebView Renderer
[assembly: ExportRenderer(typeof(CustomWebView), typeof(MyCustomWebViewRenderer))]
namespace Report.iOS
{
public class MyCustomWebViewRenderer : ViewRenderer<CustomWebView, WKWebView>
{
WKWebView webView;
protected override void OnElementChanged(ElementChangedEventArgs<CustomWebView> e)
{
base.OnElementChanged(e);
if (Control == null)
{
webView = new WKWebView(Frame, new WKWebViewConfiguration());
webView.NavigationDelegate = new WebViewDelegate();
SetNativeControl(webView);
}
if (e.NewElement != null)
{
Control.LoadRequest(new NSUrlRequest(new NSUrl(Element.Uri)));
}
}
}
public class WebViewDelegate : WKNavigationDelegate, INSUrlConnectionDataDelegate
{
string uname = null;
string pass = null;
public override async void DidReceiveAuthenticationChallenge(WKWebView webView, NSUrlAuthenticationChallenge challenge, Action<NSUrlSessionAuthChallengeDisposition, NSUrlCredential> completionHandler)
{
try
{
uname = Xamarin.Forms.Application.Current.Properties.ContainsKey("Username") ? Convert.ToString(Xamarin.Forms.Application.Current.Properties["Username"]) : null;
pass = await SecureStorage.GetAsync("Password");
}
catch (Exception ex)
{
}
completionHandler(NSUrlSessionAuthChallengeDisposition.UseCredential, new NSUrlCredential(uname, pass, NSUrlCredentialPersistence.ForSession));
return;
}
}
}
Screenshot of webview screen :
Here i am loading this webpage(https://learn.microsoft.com/en-us/xamarin/essentials/device-display?tabs=android). As you can see half of footer is hidden and i am not able to scroll it.
Screenshot of app
The reason for it quite simple actually you have added the WebView inside a scrollView which is, in turn, causing the issue webview has its own scroll so all you have to do is something like:
<StackLayout VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand">
<local:CustomWebView x:Name="dashboard" />
</StackLayout>
Also, you do not need the on the platform you can directly use the below and the custom renderer you have created.
The Height/Width request & layout options are not needed Webview by default will capture the whole viewport, You could actually even remove the StackLayouts, But that's on you.
Also, you might wanna read more about the webview
Good luck
Feel free to get back if you have queries
You can use latest WkWebViewRenderer:
public class MyCustomWebViewRenderer : WkWebViewRenderer
{
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
//this.LoadUrl("https://learn.microsoft.com/en-us/xamarin/essentials/device-display?tabs=android");
this.NavigationDelegate = new WebViewDelegate();
}
}
In your code behind, you can directly set the source or set your binding:
dashboard.Source = "https://learn.microsoft.com/en-us/xamarin/essentials/device-display?tabs=android";
Also, start from xamarin.forms 4.5+, xamarin use WKWebview as the default control in iOS and that means you no longer need a custom renderer if you use xamarin.forms 4.5+. Refer:
UIWebView Deprecation and App Store Rejection (ITMS-90809)
I was facing that issue just beacuse i was using custom renderer.
My solution code is as follows :
Xaml Code :
<ContentPage.Content>
<StackLayout VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand">
<WebView x:Name="dashboard" HeightRequest="1000" WidthRequest="1000"/>
</StackLayout>
</ContentPage.Content>
Code Behind :
public partial class DashboardView : ContentPage
{
public DashboardView()
{
InitializeComponent();
dashboard.Source = "url";
}
}
Authentication Renderer iOS :
[assembly: ExportRenderer(typeof(WebView), typeof(Report.iOS.WebViewRenderer))]
namespace Report.iOS
{
class WebViewRenderer : WkWebViewRenderer
{
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
this.NavigationDelegate = new WebViewDelegate();
}
}
public class WebViewDelegate : WKNavigationDelegate, INSUrlConnectionDataDelegate
{
string uname = null;
string pass = null;
public override async void DidReceiveAuthenticationChallenge(WKWebView webView, NSUrlAuthenticationChallenge challenge, Action<NSUrlSessionAuthChallengeDisposition, NSUrlCredential> completionHandler)
{
try
{
uname = Xamarin.Forms.Application.Current.Properties.ContainsKey("Username") ? Convert.ToString(Xamarin.Forms.Application.Current.Properties["Username"]) : null;
pass = await SecureStorage.GetAsync("Password");
}
catch (Exception ex)
{
}
completionHandler(NSUrlSessionAuthChallengeDisposition.UseCredential, new NSUrlCredential(uname, pass, NSUrlCredentialPersistence.ForSession));
return;
}
}
}
Authentication Renderer Android :
[assembly: ExportRenderer(typeof(WebView), typeof(AuthWebViewRenderer))]
namespace Report.Droid
{
public class AuthWebViewRenderer : Xamarin.Forms.Platform.Android.WebViewRenderer
{
AuthWebViewClient _authWebClient = null;
public AuthWebViewRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(Xamarin.Forms.Platform.Android.ElementChangedEventArgs<WebView> e)
{
base.OnElementChanged(e);
if (_authWebClient == null)
{
_authWebClient = new AuthWebViewClient();
}
Control.SetWebViewClient(_authWebClient);
}
}
public class AuthWebViewClient : WebViewClient
{
public AuthWebViewClient()
{
}
public override async void OnReceivedHttpAuthRequest(global::Android.Webkit.WebView view, HttpAuthHandler handler, string host, string realm)
{
string uname = null;
string pass = null;
try
{
uname = Application.Current.Properties.ContainsKey("Username") ? Convert.ToString(Application.Current.Properties["Username"]) : null;
pass = await SecureStorage.GetAsync("Password");
}
catch (Exception ex)
{
Log.Error("Apprise :", "Error Occurred while getting login credentials " + ex);
}
handler.Proceed(uname, pass);
}
}
}

Is there a way to center the page title on Android when using Xamarin Forms Shell?

I recently changed to Xamarin Forms and notice that the title isn't centered at the top of the page for Android devices.
Is there a way that I can do this?
Here's an example of what I mean with the title:
You can use the TitleView:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:TitleViewSample"
x:Class="TitleViewSample.MainPage">
<NavigationPage.TitleView>
<Label Text="Hello World" HorizontalTextAlignement="Center"/>
</NavigationPage.TitleView>
<ContentPage.Content>
<StackLayout>
<!-- Place new controls here -->
<Label Text="Welcome to Xamarin.Forms!"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand" />
</StackLayout>
</ContentPage.Content>
</ContentPage>
https://www.andrewhoefling.com/Blog/Post/xamarin-forms-title-view-a-powerful-navigation-view
You will have to implement ShellRenderer in this case as you have Xamarin.Forms Shell Project.
using System;
using System.Collections.Specialized;
using System.ComponentModel;
using Android.App;
using Android.Content;
using Android.Content.Res;
using Android.Support.V4.Widget;
using Android.Support.V7.Widget;
using Android.Util;
using Android.Widget;
using Japanese.Droid.CustomRenderers;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using Toolbar = Android.Support.V7.Widget.Toolbar;
[assembly: ExportRenderer(typeof(Xamarin.Forms.Shell), typeof(MyShellRenderer))]
namespace MyProject.Droid.CustomRenderers
{
public class MyShellRenderer : ShellRenderer
{
public MyShellRenderer(Context context) : base(context)
{
}
protected override IShellToolbarAppearanceTracker CreateToolbarAppearanceTracker()
{
return new MyShellToolbarAppearanceTracker(this);
}
protected override IShellToolbarTracker CreateTrackerForToolbar(Toolbar toolbar)
{
return new MyShellToolbarTracker(this, toolbar, ((IShellContext)this).CurrentDrawerLayout);
}
}
public class MyShellToolbarAppearanceTracker : ShellToolbarAppearanceTracker
{
public MyShellToolbarAppearanceTracker(IShellContext context) : base(context)
{
}
public override void SetAppearance(Android.Support.V7.Widget.Toolbar toolbar, IShellToolbarTracker toolbarTracker, ShellAppearance appearance)
{
base.SetAppearance(toolbar, toolbarTracker, appearance);
//Change the following code to change the icon of the Header back button.
toolbar?.SetNavigationIcon(Resource.Drawable.back);
}
}
public class MyShellToolbarTracker : ShellToolbarTracker
{
public MyShellToolbarTracker(IShellContext shellContext, Toolbar toolbar, DrawerLayout drawerLayout) : base(shellContext, toolbar, drawerLayout)
{
}
protected override void UpdateTitleView(Context context, Toolbar toolbar, View titleView)
{
base.UpdateTitleView(context, toolbar, titleView);
for (int index = 0; index < toolbar.ChildCount; index++)
{
if (toolbar.GetChildAt(index) is TextView)
{
var title = toolbar.GetChildAt(index) as TextView;
//Change the following code to change the font size of the Header title.
title.SetTextSize(ComplexUnitType.Sp, 20);
toolbar.SetTitleMargin(MainActivity.displayMetrics.WidthPixels / 4 - Convert.ToInt32(title.TextSize) - title.Text.Length / 2, 0, 0, 0);
}
}
}
}
}
Here is the code for MainActivity.cs
public class MainActivity : FormsAppCompatActivity
{
public static DisplayMetrics displayMetrics;
protected override void OnCreate(Bundle savedInstanceState)
{
Window.AddFlags(WindowManagerFlags.Fullscreen);
base.OnCreate(savedInstanceState);
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
displayMetrics = new DisplayMetrics();
WindowManager.DefaultDisplay.GetRealMetrics(displayMetrics);
LoadApplication(new App());
if (Window != null) Window.SetStatusBarColor(Android.Graphics.Color.Transparent);
if (isPhone(this)) RequestedOrientation = ScreenOrientation.Portrait;
}
}
As the title textview having wrapped width within toolbar not updating alignment on TextAlignment with center, you can update the layout params of the toolbar to matchparent and textview gravity as follows.
If the hamburger image added with custom image then need check that resoulution if that is too big just reduce it lower one
[assembly: ExportRenderer(typeof(MainPage), typeof(MyRenderer))]//MainPage - navigation page
namespace MyProject.Droid
{
public class MyRenderer: MasterDetailPageRenderer
{
public MyRenderer(Context context) : base(context)
{
}
protected override void OnLayout(bool changed, int l, int t, int r, int b)
{
base.OnLayout(changed, l, t, r, b);
var toolbar = FindViewById<AndroidX.AppCompat.Widget.Toolbar>(Resource.Id.toolbar);
for (var i = 0; i < toolbar.ChildCount; i++)
{
var title = toolbar.GetChildAt(i) as TextView;
if (title != null && !string.IsNullOrEmpty(title.Text))
{
title.TextAlignment = Android.Views.TextAlignment.Center;
title.Gravity = GravityFlags.CenterHorizontal;
var layoutParams = (AndroidX.AppCompat.Widget.Toolbar.LayoutParams)title.LayoutParameters;
layoutParams.Width = ViewGroup.LayoutParams.MatchParent;
toolbar.RequestLayout();
}
}
}
}
}

How can I set up different footers for TableSections when using a Custom TableView Renderer

I am using a renderer to allow me to set a custom footer in my TableView. The renderer works but I would like to have the capability to set up different footers for the different table sections. For example one footer for table section 0 and another for table section 1, all the way up to table section 5.
Here's the XAML that I am using:
<!-- <local:ExtFooterTableView x:Name="tableView" Intent="Settings" HasUnevenRows="True">-->
<TableView x:Name="tableView" Intent="Settings" HasUnevenRows="True">
<TableSection Title="Cards1">
<ViewCell Height="50">
<Label Text="Hello1" />
</ViewCell>
<ViewCell Height="50">
<Label Text="Hello2" />
</ViewCell>
</TableSection>
<TableSection Title="Cards2">
<TextCell Height="50" Text="Hello"></TextCell>
</TableSection>
</TableSection>
<!-- </local:ExtFooterTableView>-->
</TableView>
and here is the C# class and renderer:
public class ExtFooterTableView : TableView
{
public ExtFooterTableView()
{
}
}
and:
using System;
using Japanese;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(ExtFooterTableView), typeof(Japanese.iOS.ExtFooterTableViewRenderer))]
namespace Japanese.iOS
{
public class ExtFooterTableViewRenderer : TableViewRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<TableView> e)
{
base.OnElementChanged(e);
if (Control == null)
return;
var tableView = Control as UITableView;
var formsTableView = Element as TableView;
tableView.WeakDelegate = new CustomFooterTableViewModelRenderer(formsTableView);
}
private class CustomFooterTableViewModelRenderer : TableViewModelRenderer
{
public CustomFooterTableViewModelRenderer(TableView model) : base(model)
{
}
public override UIView GetViewForFooter(UITableView tableView, nint section)
{
Debug.WriteLine("xx");
if (section == 0)
{
return new UILabel()
{
// Text = TitleForFooter(tableView, section), // or use some other text here
Text = "abc",
TextAlignment = UITextAlignment.Left
// TextAlignment = NSTextAlignment.NSTextAlignmentJustified
};
}
else
{
return new UILabel()
{
// Text = TitleForFooter(tableView, section), // or use some other text here
Text = "def",
TextAlignment = UITextAlignment.Left
// TextAlignment = NSTextAlignment.NSTextAlignmentJustified
};
}
}
}
}
}
The code works but I would like to find out how I can set up a different footer text for different sections in the XAML. Something like this:
From what I see it looks like the code is partly there TitleForFooter(tableView, section) but I am not sure how to use it and how I could set it up. Note that I am not really looking for a view model solution. I would be happy to be simply able to specify the section footer text as part of the TableView XAML.
I'd appreciate if anyone could give me some advice on this.
First of all, in order to be able to specify the section footer text in XAML - simplest option would be to create a bindable property in TableSection. But as TableSection is sealed, we can't derive it to define our custom bindable properties.
So, the next option is to create a attached bindable property.
public class Ex
{
public static readonly BindableProperty FooterTextProperty =
BindableProperty.CreateAttached("FooterText", typeof(string), typeof(Ex), defaultValue: default(string));
public static string GetFooterText(BindableObject view)
{
return (string)view.GetValue(FooterTextProperty);
}
public static void SetFooterText(BindableObject view, string value)
{
view.SetValue(FooterTextProperty, value);
}
}
Next step would be to update renderer to retrieve this value for every section:
private class CustomFooterTableViewModelRenderer : TableViewModelRenderer
{
public CustomFooterTableViewModelRenderer(TableView model) : base(model)
{
}
public override UIView GetViewForFooter(UITableView tableView, nint section)
{
return new UILabel()
{
Text = TitleForFooter(tableView, section), // or use some other text here
Font = UIFont.SystemFontOfSize(14),
ShadowColor = Color.White.ToUIColor(),
ShadowOffset = new CoreGraphics.CGSize(0, 1),
TextColor = Color.DarkGray.ToUIColor(),
BackgroundColor = Color.Transparent.ToUIColor(),
Opaque = false,
TextAlignment = UITextAlignment.Center
};
}
//Retrieves the footer text for corresponding section through the attached property
public override string TitleForFooter(UITableView tableView, nint section)
{
var tblSection = View.Root[(int)section];
return Ex.GetFooterText(tblSection);
}
}
Sample Usage
<local:ExtFooterTableView x:Name="tableView" Intent="Settings" HasUnevenRows="True">
<TableSection Title="Cards1" local:Ex.FooterText="Sample description">
<ViewCell Height="50">
<Label Margin="20,0,20,0" Text="Hello1" />
</ViewCell>
<ViewCell Height="50">
<Label Margin="20,0,20,0" Text="Hello2" />
</ViewCell>
</TableSection>
<TableSection Title="Cards2" local:Ex.FooterText="Disclaimer note">
<TextCell Height="50" Text="Hello"></TextCell>
</TableSection>
</local:ExtFooterTableView>
It is very simple. you need to add the bindable property for pass value from XAML to CustomRenderer in CustomControl like this:
Customer TableView
public class ExtFooterTableView : TableView
{
public ExtFooterTableView()
{
}
}
Xaml control code
<local:ExtFooterTableView x:Name="tableView" Intent="Settings" HasUnevenRows="True">
Renderer class
using System;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
using yournamespace;
using System.ComponentModel;
[assembly: ExportRenderer(typeof(ExtFooterTableView), typeof(FooterTableViewRenderer))]
namespace yournamespace
{
public class FooterTableViewRenderer : TableViewRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<TableView> e)
{
base.OnElementChanged(e);
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
var view = (ExtFooterTableView)Element;
if (e.PropertyName == ExtFooterTableView.IntentProperty.PropertyName)
{
string intent = view.Intent;
// Do your stuff for intent property
}
if (e.PropertyName == ExtFooterTableView.HasUnevenRowsProperty.PropertyName)
{
bool hasUnevenRows = view.HasUnevenRows;
// Do yout stuff for HasUnevenRow
}
}
}
}

Xamarin Forms - finding databound object from custom renderer while responding to a swipe

From an iOS swipe event, I am trying to figure out how to work my way back to the model databound to the ViewCell (model is my own Drive object, a simple POCO).
I am using a subclassed StackLayout ...
public class MainPageStackLayout : StackLayout { }
with a custom renderer...
[assembly: ExportRenderer(typeof(MainPageStackLayout), typeof(MainPageStackLayoutRenderer))]
namespace DriveLive.iOS
{
public class MainPageStackLayoutRenderer : VisualElementRenderer<StackLayout>
{
UISwipeGestureRecognizer swipeGestureRecognizer;
protected override void OnElementChanged(ElementChangedEventArgs<StackLayout> e)
{
base.OnElementChanged(e);
swipeGestureRecognizer = new UISwipeGestureRecognizer(() =>
{
//********************
Console.WriteLine("How to access the underlying model here?");
//********************
}) { Direction = UISwipeGestureRecognizerDirection.Left, NumberOfTouchesRequired = 1 };
if (e.NewElement == null)
{
if (swipeGestureRecognizer != null)
this.RemoveGestureRecognizer(swipeGestureRecognizer);
}
if (e.OldElement == null)
{
this.AddGestureRecognizer(swipeGestureRecognizer);
}
}
}
}
and the code that uses the MainPageStackLayout ...
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:DriveLive"
x:Class="DriveLive.Views.MainPage">
<ListView x:Name="___drives" HasUnevenRows="True">
<ListView.ItemTemplate />
</ListView>
</ContentPage>
C#
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
IDriveRespository repo = new DriveLive.Repository.Fakes.DriveRespository();
___drives.ItemsSource = repo.GetDrivesForUser(45); // returns a Drive list of objects
___drives.ItemTemplate = new DataTemplate(typeof(CustomViewCell));
___drives.SeparatorColor = Color.FromHex("#81C1B5");
}
}
public class CustomViewCell : ViewCell
{
bool _initialized = false;
StackLayout _cellStack;
public CustomViewCell()
{
_cellStack = new MainPageStackLayout()
{
Orientation = StackOrientation.Vertical,
HorizontalOptions = LayoutOptions.FillAndExpand
};
View = _cellStack;
var label = new Label() { FontAttributes = FontAttributes.Bold };
label.SetBinding(Label.TextProperty, new Binding("DestinationName"));
_cellStack.Children.Add(label);
}
}
From the handler for the UISwipeGestureRecognizer, how can I access the underlying Drive object which is databound to the ViewCell?
My issue is resolved by leveraging this piece of XForms.
ListView Interactivity - Context Actions
Credit goes to #skar's comment for pointing me in the right direction.

Resources