Custom NavigationRenderer for back button not called while navigating back in xamarin forms application - xamarin

Here is the code for the custom renderer i used to assign a custom icon as my back button.
namespace MyProjectName.Droid.Renderers
{
public class MyNavigationRenderer: PageRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.Page> e)
{
base.OnElementChanged(e);
var context = (Activity)Xamarin.Forms.Forms.Context;
var toolbar = context.FindViewById<AndroidX.AppCompat.Widget.Toolbar>(Droid.Resource.Id.toolbar);
toolbar.NavigationIcon = AndroidX.Core.Content.ContextCompat.GetDrawable(context, Resource.Drawable.bbutton_nav);
}
}
}
This code successfully replaces the native back arrow icon with my custom bbutton_nav. When i navigate forward(Navigate.PushAsync()), the custom icon appears on all the upcoming screens. But when i click on the back icon to go back one page(Navigate.PopAsync()), the old native back arrow reappears instead of the new custom icon that was set by the renderer. When i tried debugging , i found out that the renderer class was not getting called when navigating back(Navigation.PopAsync()).
Any help on how to mitigate this issue is appreciated. Thanks

Create a custom renderer for NavigationPage instead of Page , and override the OnLayout method .
Android will change the detault icon back in UpdateToolbar method , and OnLayout method is triggered every time while current page is changed.
Android Solution
[assembly: ExportRenderer(typeof(NavigationPage), typeof(MyNavigationRenderer))]
namespace FormsApp.Droid
{
public class MyNavigationRenderer : NavigationPageRenderer
{
Context _context;
AndroidX.AppCompat.Widget.Toolbar _toolbar;
public MyNavigationRenderer(Context context) : base(context)
{
_context = context;
}
public override void OnViewAdded(Android.Views.View child)
{
base.OnViewAdded(child);
if (child.GetType() == typeof(AndroidX.AppCompat.Widget.Toolbar))
{
_toolbar = (AndroidX.AppCompat.Widget.Toolbar)child;
_toolbar.SetNavigationIcon(Resource.Drawable.bbutton_nav);
}
}
protected override void OnLayout(bool changed, int l, int t, int r, int b)
{
base.OnLayout(changed, l, t, r, b);
if (_toolbar != null)
{
if (_toolbar.NavigationIcon != null)
{
_toolbar.NavigationIcon = AndroidX.Core.Content.ContextCompat.GetDrawable(_context, Resource.Drawable.bbutton_nav);
}
}
}
}
}
Refer to https://forums.xamarin.com/discussion/183344/how-to-change-navigation-back-button-icon .
iOS Solution
[assembly: ExportRenderer(typeof(NavigationPage), typeof(MyRenderer))]
namespace FormsApp.iOS
{
class MyRenderer : NavigationRenderer
{
public override void ViewDidLayoutSubviews()
{
base.ViewDidLayoutSubviews();
if (this.NavigationBar.TopItem.BackBarButtonItem == null)
{
this.NavigationBar.BackIndicatorImage = UIImage.FromFile("dots.png");
this.NavigationBar.BackIndicatorTransitionMaskImage = UIImage.FromFile("dots.png");
this.NavigationBar.TopItem.BackBarButtonItem = new UIBarButtonItem("", UIBarButtonItemStyle.Plain, null);
}
}
}
}

Related

How to hide clear button Icon inside SearchBar control Xamarin forms

I am using xamarin forms SearchBar control. I want to remove clear button x icon without using custom renderer.
<controls:ExSearchBar
x:Name="entrySearch"
BackgroundColor="White"
CornerRadius="6"
BorderWidth="1"
HeightRequest="45"
Text="{Binding SearchText}"
HorizontalOptions="FillAndExpand"
Placeholder="search">
</controls:ExSearchBar>
This is ExSearchBar control in shared project
public class ExSearchBar : SearchBar
{
public static readonly BindableProperty ElevationProperty = BindableProperty.Create(nameof(Elevation), typeof(float), typeof(ExFrame), default(float));
public float Elevation
{
get { return (float)GetValue(ElevationProperty); }
set { SetValue(ElevationProperty, value); }
}
}
How can I do that?
The situation you are describing is the exact reason why Xamarin Forms ships with the ability to create custom renderers. The forms team define the UI elements in abstract (seperate from their native implementation) and when there is a specific feature that is not defined in their API, you must go down to the platform level to change it.
You can also use an Effect to achieve the same result, I have provided a custom renderer for iOS & Android to show you how you would go about achieving the UI you desire:
iOS:
public class SearchBarButtonRenderer : SearchBarRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<SearchBar> e)
{
base.OnElementChanged(e);
if (e.NewElement != null)
{
Control.SearchTextField.ClearButtonMode = UITextFieldViewMode.Never;
}
}
}
Really simple, just remove the clear button from the underlying UITextField
Android
public class SearchBarButtonRenderer : SearchBarRenderer
{
private readonly Context _context;
public SearchBarButtonRenderer(Context context)
: base(context)
{
_context = context;
}
protected override void OnElementChanged(ElementChangedEventArgs<SearchBar> e)
{
base.OnElementChanged(e);
if (e.NewElement != null)
{
// Get Search Close Button Drawable
var closeButtonId = Resources.GetIdentifier("android:id/search_close_btn", null, null);
var searchEditText = Control.FindViewById<ImageView>(closeButtonId);
// Get Close Button Drawable To Replace Existing Drawable
var closeDrawable = GetCloseButtonDrawable() as VectorDrawable;
if (closeDrawable is null) return;
// Apply Transparent Color To Drawable (To Make Invisible)
var buttonColor = Xamarin.Forms.Color.Transparent.ToAndroid();
closeDrawable.SetTint(buttonColor);
// Set Drawable On Control
searchEditText.SetImageDrawable(closeDrawable);
}
}
private Drawable GetCloseButtonDrawable()
{
return ContextCompat.GetDrawable(_context, Resource.Drawable.abc_ic_clear_material);
}
}
A little bit of a fiddle, find the close button drawable and replace it with a custom styled drawable

Xamarin Forms iOS custom renderer for tabbed page

I have tabbed page in Xamarin Forms (iOS side). I need custom renderer for tabbed page - make first tab not scrollable (it could be shown as button or label), rest of tabs should be scrollable.
I think creators of Xamarin Forms tabbed page implemented tabs like a horizontal listview. I just want to put a button as first element on the left and then put that listview with tabs. When button is clicked, the new view is being opened. How to do that?
I am using Naxam Library to provide top tabbed page - this is extension to tabbed page (at iOS it is at bottom). I have tried to use custom renderer, but no breakpoint is hitted. I don's know why.
using CoreAnimation;
using CoreGraphics;
using Foundation;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(CustomTabbedPage), typeof(CustomTabbedPageRenderer))]
namespace Layout.xxx.iOS.CustomControlRenderers
{
public class CustomTabbedPageRenderer : Naxam.Controls.Platform.iOS.TopTabbedRenderer
{
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
DependencyService.Get<IAlertHandler>().ShowCustomAlertVoid("", "OnElementChanged");
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
AddButtonToTabbedPage();
DependencyService.Get<IAlertHandler>().ShowCustomAlertVoid("", "ViewDidLoad");
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
DependencyService.Get<IAlertHandler>().ShowCustomAlertVoid("", "Dispose");
}
public override void ViewDidAppear(bool animated)
{
base.ViewDidAppear(animated);
DependencyService.Get<IAlertHandler>().ShowCustomAlertVoid("", "ViewDidAppear");
}
public override void ViewDidDisappear(bool animated)
{
base.ViewDidDisappear(animated);
DependencyService.Get<IAlertHandler>().ShowCustomAlertVoid("", "ViewDidDisappear");
}
public override void ViewDidLayoutSubviews()
{
base.ViewDidLayoutSubviews();
DependencyService.Get<IAlertHandler>().ShowCustomAlertVoid("", "ViewDidLayoutSubviews");
}
public override void DidMoveToParentViewController(UIViewController parent)
{
base.DidMoveToParentViewController(parent);
DependencyService.Get<IAlertHandler>().ShowCustomAlertVoid("", "DidMoveToParentViewController");
}
private void AddButtonToTabbedPage()
{
var btn = new UIButton();
CAGradientLayer btnGradient = new CAGradientLayer();
btnGradient.Frame = btn.Bounds;
btnGradient.Colors = new CGColor[] { Color.Black.ToCGColor(), Color.White.ToCGColor() };
btnGradient.Locations = new NSNumber[] { 0.0f, 0.1f };
btn.Layer.AddSublayer(btnGradient);
btn.Layer.MasksToBounds = true;
btn.Layer.BorderColor = Color.Blue.ToCGColor();
btn.Layer.BorderWidth = 2;
btn.Layer.SetNeedsDisplay();
}
}
}
If you want to fix the first tab, you can use custom renderer to achieve it. Change the index<0 to index<1 to fix the first tab in the method GetPreviousViewController and GetNextViewController:
[assembly: ExportRenderer(typeof(myTopTabbedPage), typeof(myTopTabbedRenderer))]
namespace App12.iOS
{
class myTopTabbedRenderer : TopTabbedRenderer, IUIPageViewControllerDataSource
{
public override void ViewDidLoad()
{
base.ViewDidLoad();
}
public new UIViewController GetPreviousViewController(UIPageViewController pageViewController, UIViewController referenceViewController)
{
var index = ViewControllers.IndexOf(referenceViewController) - 1;
//in the source, it is if (index < 0) return null;
//change here to if (index < 1) will fix the first tab
if (index < 1) return null;
return ViewControllers[index];
}
public new UIViewController GetNextViewController(UIPageViewController pageViewController, UIViewController referenceViewController)
{
var index = ViewControllers.IndexOf(referenceViewController) + 1;
//in the source, it is if (index == ViewControllers.Count) return null;
//change here to if (index == ViewControllers.Count || index == 1) will fix the first tab
if (index == ViewControllers.Count || index == 1) return null;
return ViewControllers[index];
}
}
}
And in your xamarin.forms project, use myTopTabbedPage to create tabs:
public class myTopTabbedPage : TopTabbedPage {
}
var tabs = new myTopTabbedPage
{
Title = "TopTabs",
BarBackgroundColor = Color.FromHex("#9C27B0"),
SwipeEnabled = true,
BarIndicatorColor = Color.Green,
BarTextColor = Color.White
};
Try and let me know if it works for you.

How to replace hamburger icon with other icon in UWP xamarin forms

I have an application, I added other hamburger icon instead of default hamburger icon. It works in both Android and iOS but not in UWP. Is there any way to change the picture consistently?
I created a project, and this project I change the icon of the hamburger menu icon.
I have a solution in Android, but I think in iOS is the same way.
You have to create a Custom MasterDetailPageRenderer class and you have to change the icon from the OnLayout function.
Project
https://github.com/officialdoniald/Xamarin.Forms.CustomControls/blob/master/XamarinForms.CustomControls/XamarinForms.CustomControls.Android/CustomRenderer/MasterNavigationPageRenderer.cs
Android
[assembly: ExportRenderer(typeof(CustomMasterDetailPage), typeof(MasterNavigationPageRenderer))]
namespace XamarinForms.CustomControls.Droid.CustomRenderer
{
public class MasterNavigationPageRenderer : MasterDetailPageRenderer
{
private static Android.Support.V7.Widget.Toolbar GetToolbar() => (CrossCurrentActivity.Current?.Activity as MainActivity)?.FindViewById<Android.Support.V7.Widget.Toolbar>(Resource.Id.toolbar);
private Android.Support.V7.Widget.Toolbar toolbar;
public MasterNavigationPageRenderer() : base() { }
public MasterNavigationPageRenderer(Context context) : base(context) { }
public MasterNavigationPageRenderer(IntPtr a, JniHandleOwnership b) : base() { }
protected override void OnLayout(bool changed, int l, int t, int r, int b)
{
base.OnLayout(changed, l, t, r, b);
toolbar = GetToolbar();
if (toolbar != null)
{
SetNavigationButton(Resource.Drawable.menu);
}
}
private void SetNavigationButton(int resourceID)
{
var icon = Forms.Context.GetDrawable(resourceID);
using (var drawable = ((BitmapDrawable)icon).Bitmap)
using (var bitmap = Bitmap.CreateScaledBitmap(drawable, 80, 80, false))
using (var newDrawable = new BitmapDrawable(Resources, bitmap))
{
toolbar.NavigationIcon = newDrawable;
}
}
}

How can I change the Font Size and Weight for the Header in a Navigation Page?

I can change the Font Color like this:
var homePage = new NavigationPage(new HomePage())
{
Title = "Home",
Icon = "ionicons_2_0_1_home_outline_25.png",
BarTextColor = Color.Gray,
};
But is there a way to change the Font Size and Weight for the Title? I would like to change it for the iOS and Android platforms only. Hoping that someone knows of Custom Renderer code that can help me to do this.
Note that this question is similar to my question on how to change the Font which has been answered here:
How can I change the font for the header of a Navigation page with Xamarin Forms?
Here is an Custom Renderer for Android where you are able to change the Font Size and also the Font Weight. I've marked the values you have to change with an TODO.
using Android.Content;
using Android.Graphics;
using App5.Droid;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android.AppCompat;
[assembly: ExportRenderer(typeof(NavigationPage), typeof(CustomNavigationPageRenderer))]
namespace App5.Droid
{
public class CustomNavigationPageRenderer : NavigationPageRenderer
{
private Android.Support.V7.Widget.Toolbar _toolbar;
public CustomNavigationPageRenderer(Context context) : base(context)
{
}
public override void OnViewAdded(Android.Views.View child)
{
base.OnViewAdded(child);
if (child.GetType() == typeof(Android.Support.V7.Widget.Toolbar))
{
_toolbar = (Android.Support.V7.Widget.Toolbar)child;
_toolbar.ChildViewAdded += Toolbar_ChildViewAdded;
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
_toolbar.ChildViewAdded -= Toolbar_ChildViewAdded;
}
}
private void Toolbar_ChildViewAdded(object sender, ChildViewAddedEventArgs e)
{
var view = e.Child.GetType();
System.Diagnostics.Debug.WriteLine(view);
if (e.Child.GetType() == typeof(Android.Support.V7.Widget.AppCompatTextView))
{
var textView = (Android.Support.V7.Widget.AppCompatTextView)e.Child;
// TODO: CHANGE VALUES HERE
textView.TextSize = 25;
textView.SetTypeface(null, TypefaceStyle.Bold);
_toolbar.ChildViewAdded -= Toolbar_ChildViewAdded;
}
}
}
}
Here is an implementation of a Custom Renderer for iOS.
using App5.iOS;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(NavigationPage), typeof(CustomNavigationPageRenderer))]
namespace App5.iOS
{
public class CustomNavigationPageRenderer : NavigationRenderer
{
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
if (e.NewElement != null)
{
var att = new UITextAttributes();
// TODO: Create your FontSize and FontWeight here
var fontSize = Font.SystemFontOfSize(30.0);
var boldFontSize = Font.SystemFontOfSize(35.0, FontAttributes.Bold);
// TODO: Apply your selected FontSize and FontWeight combination here
att.Font = boldFontSize.ToUIFont();
UINavigationBar.Appearance.SetTitleTextAttributes(att);
}
}
}
}

How to create a code-only webview with Xamarin.Forms

I'm trying to use a library that doesn't has a .Net SDK, but as I want to use it only to return a string, I thought I could use it's JS SDK by creating a custom WebView that returns strings (https://xamarinhelp.com/xamarin-forms-webview-executing-javascript/).
The first problem that I faced was that a CustomRenderer is not called in Xamarin.Forms until the View is added to a Page (or at least I couldn't make it be called). To fix this I added a call to Platform.CreateRenderer in each platform.
It did the trick and the CustomRenderer executed. But when I tried to call a JS function to retrieve a string, the app just hung and stayed that way.
I didn't try to insert the WebView in a Page because I want it to be independent of the page that the app is current on, and as I want a "code-only" html, I don't see the point of adding it somewhere.
My classes:
JSEvaluator
namespace MyNamespace.Views
{
public class JSEvaluator : WebView
{
public static BindableProperty EvaluateJavascriptProperty = BindableProperty.Create(nameof(EvaluateJavascript), typeof(Func<string, Task<string>>), typeof(JSEvaluator), null, BindingMode.OneWayToSource);
public Func<string, Task<string>> EvaluateJavascript
{
get { return (Func<string, Task<string>>)GetValue(EvaluateJavascriptProperty); }
set { SetValue(EvaluateJavascriptProperty, value); }
}
public JSEvaluator()
{
}
}
}
UWP Renderer
[assembly: ExportRenderer(typeof(JSEvaluator), typeof(JSEvaluatorRenderer))]
namespace MyNamespace.UWP.Renderers
{
public class JSEvaluatorRenderer : WebViewRenderer
{
public JSEvaluatorRenderer() { }
protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
{
base.OnElementChanged(e);
var webView = e.NewElement as JSEvaluator;
if (webView != null)
webView.EvaluateJavascript = async (js) =>
{
return await Control.InvokeScriptAsync("eval", new[] { js });
};
}
}
}
Creation and use
if (jsEvaluator == null)
{
jsEvaluator = new JSEvaluator { Source = new HtmlWebViewSource { Html = HTML.html } };
#if __ANDROID__
Xamarin.Forms.Platform.Android.Platform.CreateRenderer(jsEvaluator);
#elif __IOS__
Xamarin.Forms.Platform.iOS.Platform.CreateRenderer(jsEvaluator);
#elif WINDOWS_UWP
Xamarin.Forms.Platform.UWP.Platform.CreateRenderer(jsEvaluator);
#endif
}
Thanks for the help :)
I had to add the WebView to a page, as #SushiHangover said in the comment. With this done, it worked as expected.

Resources