Xamarin Forms picker OK or Cancel button pressed detection - c#

I am developing applications with Xamarin Forms. I have some problems with the picker component.
After picking up with the Picker, I need to determine whether the OK or Cancel button is pressed.
I tried Focus and Unfocus, but it doesn't help me figure out which user is pressing the OK or Cancel button.

Solution:
You should rewrite the toolbar of Picker in CustomRenderer
iOS
[assembly:ExportRenderer(typeof(Picker),typeof(MyPickerRenderer))]
namespace App6.iOS
{
public class MyPickerRenderer:PickerRenderer
{
public MyPickerRenderer()
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Picker> e)
{
base.OnElementChanged(e);
if(Control!=null)
{
UIPickerView pickerView = (UIPickerView)Control.InputView;
// get the button Done and rewrite the event
UIToolbar toolbar = (UIToolbar)Control.InputAccessoryView;
UIBarButtonItem done = new UIBarButtonItem("OK", UIBarButtonItemStyle.Done, (object sender, EventArgs click) =>
{
MessagingCenter.Send<Object>(this,"Ok_Clicked");
});
UIBarButtonItem cancel = new UIBarButtonItem(UIBarButtonSystemItem.Cancel, (object sender, EventArgs click) =>
{
MessagingCenter.Send<Object>(this, "Cancel_Clicked");
});
UIBarButtonItem empty = new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace, null);
toolbar.Items = new UIBarButtonItem[] { cancel,empty, done };
}
}
}
}
Android
[assembly: ExportRenderer(typeof(Picker), typeof(MyPickerRenderer))]
namespace App6.Droid
{
public class MyPickerRenderer : 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 NumberPicker(Context);
if (model.Items != null && model.Items.Any())
{
picker.MaxValue = model.Items.Count - 1;
picker.MinValue = 0;
picker.SetDisplayedValues(model.Items.ToArray());
picker.WrapSelectorWheel = false;
picker.Value = model.SelectedIndex;
}
var layout = new LinearLayout(Context) { Orientation = Orientation.Vertical };
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) =>
{
MessagingCenter.Send<Object>(this, "Cancel_Clicked");
});
builder.SetPositiveButton("Ok ", (s, a) =>
{
ElementController.SetValueFromRenderer(Picker.SelectedIndexProperty, picker.Value);
// It is possible for the Content of the Page to be changed on SelectedIndexChanged.
// In this case, the Element & Control will no longer exist.
if (Element != null)
{
if (model.Items.Count > 0 && Element.SelectedIndex >= 0)
Control.Text = model.Items[Element.SelectedIndex];
ElementController.SetValueFromRenderer(VisualElement.IsFocusedProperty, false);
// It is also possible for the Content of the Page to be changed when Focus is changed.
// In this case, we'll lose our Control.
}
MessagingCenter.Send<Object>(this, "Ok_Clicked");
});
_dialog = builder.Create();
_dialog.DismissEvent += (ssender, args) =>
{
ElementController?.SetValueFromRenderer(VisualElement.IsFocusedProperty, false);
};
_dialog.Show();
}
}
}
And you can handle the event in forms by using MessagingCenter.
in contentPage
public MainPage()
{
InitializeComponent();
MessagingCenter.Subscribe<Object>(this, "Ok_Clicked", (sender)=> {
picker.Unfocus();
DisplayAlert("Title", "Ok has been clicked", "cancel");
//do something you want
});
MessagingCenter.Subscribe<Object>(this, "Cancel_Clicked", (sender) => {
picker.Unfocus();
DisplayAlert("Title", "Cancel has been clicked", "cancel");
//do something you want
});
}

Related

Custom TimePickerRenderer is not firing the events to update the TimeProperty

I created a custom TimePicker and the renderers to android and iphone, with the objective to allow for that be nullable. As inspiration, was used the https://xamgirl.com/clearable-datepicker-in-xamarin-forms/
But, for some reason, the event is not firing when the time is set, thats happenen only in android, and more specific, back in android 8.1.
On shared project:
public class NullableTimePicker : TimePicker
{
public NullableTimePicker()
{
Time = DateTime.Now.TimeOfDay;
NullableTime = null;
Format = #"HH\:mm";
}
public string _originalFormat = null;
public static readonly BindableProperty PlaceHolderProperty =
BindableProperty.Create(nameof(PlaceHolder), typeof(string), typeof(NullableTimePicker), " : ");
public string PlaceHolder
{
get { return (string)GetValue(PlaceHolderProperty); }
set
{
SetValue(PlaceHolderProperty, value);
}
}
public static readonly BindableProperty NullableTimeProperty =
BindableProperty.Create(nameof(NullableTime), typeof(TimeSpan?), typeof(NullableTimePicker), null, defaultBindingMode: BindingMode.TwoWay);
public TimeSpan? NullableTime
{
get { return (TimeSpan?)GetValue(NullableTimeProperty); }
set { SetValue(NullableTimeProperty, value); UpdateTime(); }
}
private void UpdateTime()
{
if (NullableTime != null)
{
if (_originalFormat != null)
{
Format = _originalFormat;
}
}
else
{
Format = PlaceHolder;
}
}
protected override void OnBindingContextChanged()
{
base.OnBindingContextChanged();
if (BindingContext != null)
{
_originalFormat = Format;
UpdateTime();
}
}
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
if (propertyName == TimeProperty.PropertyName ||
(
propertyName == IsFocusedProperty.PropertyName &&
!IsFocused &&
(Time == DateTime.Now.TimeOfDay)))
{
AssignValue();
}
if (propertyName == NullableTimeProperty.PropertyName && NullableTime.HasValue)
{
Time = NullableTime.Value;
if (Time == DateTime.Now.TimeOfDay)
{
//this code was done because when date selected is the actual date the"DateProperty" does not raise
UpdateTime();
}
}
}
public void CleanTime()
{
NullableTime = null;
UpdateTime();
}
public void AssignValue()
{
NullableTime = Time;
UpdateTime();
}
}
On Android project:
public class NullableTimePickerRenderer : ViewRenderer<NullableTimePicker, EditText>
{
public NullableTimePickerRenderer(Context context) : base(context)
{
}
TimePickerDialog _dialog;
protected override void OnElementChanged(ElementChangedEventArgs<NullableTimePicker> e)
{
base.OnElementChanged(e);
this.SetNativeControl(new Android.Widget.EditText(Context));
if (Control == null || e.NewElement == null)
return;
this.Control.Click += OnPickerClick;
if (Element.NullableTime.HasValue)
Control.Text = DateTime.Today.Add(Element.Time).ToString(Element.Format);
else
this.Control.Text = Element.PlaceHolder;
this.Control.KeyListener = null;
this.Control.FocusChange += OnPickerFocusChange;
this.Control.Enabled = Element.IsEnabled;
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == Xamarin.Forms.TimePicker.TimeProperty.PropertyName ||
e.PropertyName == Xamarin.Forms.TimePicker.FormatProperty.PropertyName)
SetTime(Element.Time);
}
void OnPickerFocusChange(object sender, Android.Views.View.FocusChangeEventArgs e)
{
if (e.HasFocus)
{
ShowTimePicker();
}
}
protected override void Dispose(bool disposing)
{
if (Control != null)
{
this.Control.Click -= OnPickerClick;
this.Control.FocusChange -= OnPickerFocusChange;
if (_dialog != null)
{
_dialog.Hide();
_dialog.Dispose();
_dialog = null;
}
}
base.Dispose(disposing);
}
void OnPickerClick(object sender, EventArgs e)
{
ShowTimePicker();
}
void SetTime(TimeSpan time)
{
Control.Text = DateTime.Today.Add(time).ToString(Element.Format);
Element.Time = time;
}
private void ShowTimePicker()
{
CreateTimePickerDialog(this.Element.Time.Hours, this.Element.Time.Minutes);
_dialog.Show();
}
void CreateTimePickerDialog(int hours, int minutes)
{
NullableTimePicker view = Element;
_dialog = new TimePickerDialog(Context, (o, e) =>
{
view.Time = new TimeSpan(hours: e.HourOfDay, minutes: e.Minute, seconds: 0);
view.AssignValue();
((IElementController)view).SetValueFromRenderer(VisualElement.IsFocusedProperty, false);
Control.ClearFocus();
_dialog = null;
}, hours, minutes, true);
_dialog.SetButton("ok", (sender, e) =>
{
SetTime(Element.Time);
this.Element.Format = this.Element._originalFormat;
this.Element.AssignValue();
});
_dialog.SetButton2("clear", (sender, e) =>
{
this.Element.CleanTime();
Control.Text = this.Element.Format;
});
}
}
On iOS project:
public class NullableTimePickerRenderer : TimePickerRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<TimePicker> e)
{
base.OnElementChanged(e);
var timePicker = (UIDatePicker)Control.InputView;
timePicker.Locale = new NSLocale("no_nb");
if (e.NewElement != null && this.Control != null)
{
this.UpdateDoneButton();
this.AddClearButton();
this.Control.BorderStyle = UITextBorderStyle.Line;
Control.Layer.BorderColor = UIColor.LightGray.CGColor;
Control.Layer.BorderWidth = 1;
if (Device.Idiom == TargetIdiom.Tablet)
{
this.Control.Font = UIFont.SystemFontOfSize(25);
}
}
}
private void UpdateDoneButton()
{
var toolbar = (UIToolbar)Control.InputAccessoryView;
var doneBtn = toolbar.Items[1];
doneBtn.Clicked += (sender, args) =>
{
NullableTimePicker baseTimePicker = this.Element as NullableTimePicker;
if (!baseTimePicker.NullableTime.HasValue)
{
baseTimePicker.AssignValue();
}
};
}
private void AddClearButton()
{
var originalToolbar = this.Control.InputAccessoryView as UIToolbar;
if (originalToolbar != null && originalToolbar.Items.Length <= 2)
{
var clearButton = new UIBarButtonItem("clear", UIBarButtonItemStyle.Plain, ((sender, ev) =>
{
NullableTimePicker baseTimePicker = this.Element as NullableTimePicker;
this.Element.Unfocus();
this.Element.Time = DateTime.Now.TimeOfDay;
baseTimePicker.CleanTime();
}));
var newItems = new List<UIBarButtonItem>();
foreach (var item in originalToolbar.Items)
{
newItems.Add(item);
}
newItems.Insert(0, clearButton);
originalToolbar.Items = newItems.ToArray();
originalToolbar.SetNeedsDisplay();
}
}
}
This code works fine on iOS and Android version 8.1 or higher, but in lower version, this just not fire the event to set time, setting always the default time.
I'm also provided a git repo with the code, maybe, make easily understand my problem.
https://github.com/aismaniotto/Nullable24hTimePicker
Thanks for your help. After more digging on code and debugging, I realize the problem was on OkButton implementation. On Android 8, apparently, the callback from the TimePickerDialog was called before the OkButton implementation e what was there, is ignored. On the older version, the OKButton implementation was called before and, for some reason, cancel the invocation of the callback.
That way, removing the OkButton implementation, the problem was solved... remembering that on the working version, was ignored anyway. Eventually, I will commit to the repository, to be registered.
Thanks a lot.

Hiding Floating action button when parent scrolled

I'm creating a custom FAB button for both android and iOS platforms using Xamarin.Forms.
Everything is working fine, but now I want to be able to somehow receive scrolled events from the nearest parent of the FAB button if any, so I will be able to hide the FAB button when the user is scrolling and then show it again after 2 seconds of scrolling finished?.
The parent could be a plain ScrollView item, or it could be a ListView.
Can this be achieved via the same custom renderers for each platform? Or, can this even be achieved at all?
This is what I did so far:
FabButton class:
public partial class FabButton : Button
{
public static BindableProperty ButtonColorProperty = BindableProperty.Create(nameof(ButtonColor), typeof(Color), typeof(FabButton), Color.Accent);
public Color ButtonColor
{
get => (Color)GetValue(ButtonColorProperty);
set => SetValue(ButtonColorProperty, value);
}
public FabButton()
{
InitializeComponent();
}
}
iOS custom renderer:
public class FabButtonRenderer:ButtonRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Button> e)
{
base.OnElementChanged(e);
if(e.NewElement == null)
return;
if (Element.WidthRequest <= 0)
Element.WidthRequest = 60;
if (Element.HeightRequest <= 0)
Element.HeightRequest = 60;
if (Element.Margin == new Thickness(0, 0, 0, 0))
Element.Margin = new Thickness(0, 0, 20, 20);
Element.CornerRadius = (int) Element.WidthRequest / 2;
Element.BorderWidth = 0;
Element.Text = null;
Control.BackgroundColor = ((FabButton) Element).ButtonColor.ToUIColor();
}
public override void Draw(CGRect rect)
{
base.Draw(rect);
Layer.ShadowRadius = 0.2f;
Layer.ShadowColor = UIColor.Black.CGColor;
Layer.ShadowOffset = new CGSize(1, 1);
Layer.ShadowOpacity = 0.80f;
Layer.ShadowPath = UIBezierPath.FromOval(Layer.Bounds).CGPath;
Layer.MasksToBounds = false;
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == nameof(FabButton.ButtonColor))
Control.BackgroundColor = ((FabButton) Element).ButtonColor.ToUIColor();
}
}
android custom renderer:
public class FabButtonRenderer: Xamarin.Forms.Platform.Android.AppCompat.ViewRenderer<FabButton,FloatingActionButton>
{
public static void InitRenderer() { }
public FabButtonRenderer(Context context):base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<FabButton> e)
{
base.OnElementChanged(e);
if (e.NewElement == null)
return;
if (e.NewElement.HeightRequest <= 0)
e.NewElement.HeightRequest = 85;
if (e.NewElement.WidthRequest <= 0)
e.NewElement.WidthRequest = 75;
if (e.NewElement.Margin.Equals(new Thickness(0, 0, 0, 0)))
e.NewElement.Margin = new Thickness(0, 0, 5, 10);
var fabButton = new FloatingActionButton(Context);
ViewCompat.SetBackgroundTintList(fabButton, ColorStateList.ValueOf(Element.ButtonColor.ToAndroid()));
fabButton.UseCompatPadding = true;
if (!string.IsNullOrEmpty(Element.Image?.File))
fabButton.SetImageDrawable(Context.GetDrawable(Element.Image.File));
fabButton.Click += FabButton_Clicked;
SetNativeControl(fabButton);
}
protected override void OnLayout(bool changed, int l, int t, int r, int b)
{
base.OnLayout(changed, l, t, r, b);
Control.BringToFront();
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(Element.ButtonColor))
ViewCompat.SetBackgroundTintList(Control, ColorStateList.ValueOf(Element.ButtonColor.ToAndroid()));
if (e.PropertyName == nameof(Element.Image))
{
if (!string.IsNullOrEmpty(Element.Image?.File))
Control.SetImageDrawable(Context.GetDrawable(Element.Image.File));
}
base.OnElementPropertyChanged(sender, e);
}
public void FabButton_Clicked(object sender, EventArgs e)
{
Element.SendClicked();
}
}
I think Scrollview's "Scrolled" event can help you , you will get X & Y axis values , based on it you have to hide your fab button while it's scrolling.
And for it after 2 seconds you can use "Device.StartTimer" .
Unfortunately, there is no built-in way to do this.Since in my case, the Floating Action Button might be affected by ScrollView control scrolling, or a list view control scrolling, or others.The only way for me to do this that I can think of is to specify explicitly to which ScrollView control the Floating Action Button should be connected, and since this will be a troublesome to implement when the Floating Action Button should be connected to a list view ScrollView and since it's better to use the MVVM pattern, I have find a simpler way to this.
First I declared an interface IFloatingActionButtonHost:
public interface IFloatingActionButtonHost
{
bool ShouldBeHiddenWhenScrolling { get; }
event Action LinkedScrollViewScrolled;
}
And then, I declared a BindableProperty inside the FabButton control like this:
public static BindableProperty ButtonHostProperty = BindableProperty.Create(nameof(ButtonHost), typeof(IFloatingActionButtonHost), typeof(FabButton));
public IFloatingActionButtonHost ButtonHost
{
get => (IFloatingActionButtonHost)GetValue(ButtonHostProperty);
set => SetValue(ButtonHostProperty, value);
}
And in the Renderer for example, on iOS renderer, I checked for this property if it's not null, and if the Floating Action Button should be hidden when the target ScrollView get scrolled or not and then subscribed to the event in the interface.
var fabButton = (FabButton)Element;
if (fabButton.ButtonHost != null && fabButton.ButtonHost.ShouldBeHiddenWhenScrolling)
{
fabButton.ButtonHost.LinkedScrollViewScrolled += OnLinkedScrollView_Scrolled;
}
And then I handled the event in the renderer:
private void OnLinkedScrollView_Scrolled()
{
Element.IsVisible = false;
Device.StartTimer(TimeSpan.FromSeconds(3), () =>
{
if(Element != null){
Element.IsVisible = true;
}
return false;
});
}
The OnElementPropertyChanged implementation should be changed as well.
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == nameof(FabButton.ButtonColor))
Control.BackgroundColor = ((FabButton) Element).ButtonColor.ToUIColor();
else if(e.PropertyName == nameof(FabButton.ButtonHost)){
var fabButton = (FabButton)Element;
if (fabButton.ButtonHost != null && fabButton.ButtonHost.ShouldBeHiddenWhenScrolling)
{
fabButton.ButtonHost.LinkedScrollViewScrolled += OnLinkedScrollView_Scrolled;
}
}
}
On Each page code-behind, I make the page inherit form this interface, and just passed the page instance to the target ViewModel.
P.S. For ListView I had to write a custom renderer to expose the Scrolled event. Sorry, I haven't find a better way to do this.
This is my attempt to solve this problem, if you have a better solution, please just write another answer to this question and I will mark it as the best answer.

How to change TabbedPage Icon when the tab is reselected in Android?

I have an application using Xamarin Forms TabbedPage which has a feature that would allow the user to pause and play a page. Please see the code below.
Shared Code
public partial class MainPage : TabbedPage
{
public MainPage()
{
InitializeComponent();
var homePage = new NavigationPage(new HomePage())
{
Title = "Home",
Icon = "ionicons_2_0_1_home_outline_25.png"
};
var phrasesPage = new NavigationPage(new PhrasesPage())
{
Title = "Play",
Icon = "ionicons_2_0_1_play_outline_25.png"
};
Children.Add(homePage);
Children.Add(phrasesPage);
}
}
In iOS renderer:
public class TabbedPageRenderer : TabbedRenderer
{
private MainPage _page;
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
var tabbarController = (UITabBarController)this.ViewController;
if (null != tabbarController)
{
tabbarController.ViewControllerSelected += OnTabBarReselected;
}
}
void OnTabBarReselected(object sender, UITabBarSelectionEventArgs e)
{
var tabs = Element as TabbedPage;
var playTab = tabs.Children[4];
if (TabBar.SelectedItem.Title == "Play") {
if (tabs != null)
{
playTab.Title = "Pause";
playTab.Icon = "ionicons_2_0_1_pause_outline_22.png";
}
App.pauseCard = false;
}
else {
if (tabs != null) {
playTab.Title = "Play";
playTab.Icon = "ionicons_2_0_1_play_outline_25.png";
}
App.pauseCard = true;
}
}
Android Renderer
public class MyTabbedPageRenderer: TabbedPageRenderer, TabLayout.IOnTabSelectedListener
{
if (e.PropertyName == "Renderer")
{
viewPager = (ViewPager)ViewGroup.GetChildAt(0);
tabLayout = (TabLayout)ViewGroup.GetChildAt(1);
setup = true;
ColorStateList colors = null;
if ((int)Build.VERSION.SdkInt >= 23)
{
colors = Resources.GetColorStateList(Resource.Color.icon_tab, Forms.Context.Theme);
}
else
{
colors = Resources.GetColorStateList(Resource.Color.icon_tab);
}
for (int i = 0; i < tabLayout.TabCount; i++)
{
var tab = tabLayout.GetTabAt(i);
var icon = tab.Icon;
if (icon != null)
{
icon = Android.Support.V4.Graphics.Drawable.DrawableCompat.Wrap(icon);
Android.Support.V4.Graphics.Drawable.DrawableCompat.SetTintList(icon, colors);
}
}
}
void TabLayout.IOnTabSelectedListener.OnTabReselected(TabLayout.Tab tab)
{
var tabs = Element as TabbedPage;
var playTab = tabs.Children[4];
var selectedPosition = tab.Position;
if(selectedPosition == 4)
{
if (playTab.Title == "Play")
{
if (tabs != null)
{
playTab.Title = "Pause";
playTab.Icon = "ionicons_2_0_1_pause_outline_22.png";
}
App.pauseCard = false;
}
else
{
if (tabs != null)
{
playTab.Title = "Play";
playTab.Icon = "ionicons_2_0_1_play_outline_25.png";
}
App.pauseCard = true;
}
}
}
}
This is perfectly working in iOS. But somehow in Android only the Title would change but not the Icon. Anyone knows what Im missing or how it should be done? Also, is this possible to be done in the shared code instead of repeating almost exactly the same lines on code in each platform?
You can do it by using the tab that is being passe to you in the OnTabReselected parameters in the TabRenderer.
You can move your whole logic with this object.
This is my whole renderer file (Android):
[assembly: ExportRenderer(typeof(SWTabSelection.MainPage), typeof(SWTabSelection.Droid.MyTabbedPageRenderer))]
namespace SWTabSelection.Droid
{
public class MyTabbedPageRenderer : TabbedPageRenderer, TabLayout.IOnTabSelectedListener
{
private ViewPager viewPager;
private TabLayout tabLayout;
private bool setup;
public MyTabbedPageRenderer() { }
public MyTabbedPageRenderer(Context context) : base(context)
{
//Use this constructor for newest versions of XF saving the context parameter
// in a field so it can be used later replacing the Xamarin.Forms.Forms.Context which is deprecated.
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == "Renderer")
{
viewPager = (ViewPager)ViewGroup.GetChildAt(0);
tabLayout = (TabLayout)ViewGroup.GetChildAt(1);
setup = true;
ColorStateList colors = GetTabColor();
for (int i = 0; i < tabLayout.TabCount; i++)
{
var tab = tabLayout.GetTabAt(i);
SetTintColor(tab, colors);
}
}
}
void TabLayout.IOnTabSelectedListener.OnTabReselected(TabLayout.Tab tab)
{
// To have the logic only on he tab on position 1
if(tab == null || tab.Position != 1)
{
return;
}
if(tab.Text == "Play")
{
tab.SetText("Pause");
tab.SetIcon(Resource.Drawable.ionicons_2_0_1_pause_outline_25);
App.pauseCard = false;
}
else
{
tab.SetText("Play");
tab.SetIcon(Resource.Drawable.ionicons_2_0_1_play_outline_25);
App.pauseCard = true;
}
SetTintColor(tab, GetTabColor());
}
void SetTintColor(TabLayout.Tab tab, ColorStateList colors)
{
var icon = tab?.Icon;
if(icon != null)
{
icon = Android.Support.V4.Graphics.Drawable.DrawableCompat.Wrap(icon);
Android.Support.V4.Graphics.Drawable.DrawableCompat.SetTintList(icon, colors);
}
}
ColorStateList GetTabColor()
{
return ((int)Build.VERSION.SdkInt >= 23)
? Resources.GetColorStateList(Resource.Color.icon_tab, Forms.Context.Theme)
: Resources.GetColorStateList(Resource.Color.icon_tab);
}
}
}
The only thing that I had with the code above is that the icon was not taking the Tint color so created a function with the same logic you had to set the Tint and I am using it on the Tab Reselection. If you have only one tab in your app you can set a global tint in the Android Theme/Style xml.
Hope this helps.
Custom Renderer is no needed , you can change the Title and Icon directly in Shared code.
Just implement CurrentPageChanged event in TabbedPage
Complete code
public partial class TabbedPage1 : TabbedPage
{
NavigationPage homePage;
NavigationPage phrasesPage;
public TabbedPage1 ()
{
InitializeComponent();
var homePage = new NavigationPage(new Page1())
{
Title = "Home",
Icon = "1.png"
};
var phrasesPage = new NavigationPage (new Page2())
{
Title = "Play",
Icon = "1.png"
};
Children.Add(homePage);
Children.Add(phrasesPage);
this.CurrentPageChanged += (object sender, EventArgs e) => {
var i = this.Children.IndexOf(this.CurrentPage);
if (i == 0)
{
homePage.Title = "HomeChanged";
homePage.Icon = "2.png";
}
else {
phrasesPage.Title = "PlayChanged";
phrasesPage.Icon = "2.png";
}
};
}
}
Result
PS: Make the image files access from a different platform.
iOS - Resources
Android - Resources->drawable
There isn't a way to detect when a tab is reselected in Xamarin.Forms, so we'll have to use custom rederers to detect the logic.
For Android, we'll have to handle two cases: Current Tab Page Changed, and Current Tab Page Reselected.
We'll subscribe to to CurrentPageChanged and in its EventHandler, we'll check to see if the tab selected is PhrasesPage. If it is, we'll update the Icon/Text.
In OnTabReselected, we can check which page is currently selected, and if it's the PhrasesPage, we can update PhrasesPage.Icon and PhrasesPage.Text.
Sample App
https://github.com/brminnick/ChangeTabbedPageIconSample/tree/master
Android Custom Renderer
[assembly: ExportRenderer(typeof(MainPage), typeof(TabbedPageRenderer))]
namespace YourNameSpace
{
public class TabbedPageRenderer : TabbedRenderer, TabLayout.IOnTabSelectedListener
{
//Overloaded Constructor required for Xamarin.Forms v2.5+
public TabbedPageRenderer(Android.Content.Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<TabbedPage> e)
{
base.OnElementChanged(e);
Element.CurrentPageChanged += HandleCurrentPageChanged;
}
void HandleCurrentPageChanged(object sender, EventArgs e)
{
var currentNavigationPage = Element.CurrentPage as NavigationPage;
if (!(currentNavigationPage.RootPage is PhrasesPage))
return;
var tabLayout = (TabLayout)ViewGroup.GetChildAt(1);
for (int i = 0; i < tabLayout.TabCount; i++)
{
var currentTab = tabLayout.GetTabAt(i);
var currentTabText = currentTab.Text;
if (currentTabText.Equals("Play") || currentTabText.Equals("Pause"))
{
Device.BeginInvokeOnMainThread(() => UpdateTab(currentTabText, currentTab, currentNavigationPage));
break;
}
}
}
void TabLayout.IOnTabSelectedListener.OnTabReselected(TabLayout.Tab tab)
{
System.Diagnostics.Debug.WriteLine("Tab Reselected");
var mainPage = Application.Current.MainPage as MainPage;
var currentNavigationPage = mainPage.CurrentPage as NavigationPage;
if(currentNavigationPage.RootPage is PhrasesPage)
Device.BeginInvokeOnMainThread(() => UpdateTab(currentNavigationPage.Title, tab, currentNavigationPage));
}
void UpdateTab(string currentTabText, TabLayout.Tab tab, NavigationPage currentNavigationPage)
{
if (currentTabText.Equals("Puzzle"))
{
tab.SetIcon(IdFromTitle("Settings", ResourceManager.DrawableClass));
currentNavigationPage.Title = "Settings";
}
else
{
tab.SetIcon(IdFromTitle("Puzzle", ResourceManager.DrawableClass));
currentNavigationPage.Title = "Puzzle";
}
}
int IdFromTitle(string title, Type type)
{
string name = System.IO.Path.GetFileNameWithoutExtension(title);
int id = GetId(type, name);
return id;
}
int GetId(Type type, string memberName)
{
object value = type.GetFields().FirstOrDefault(p => p.Name == memberName)?.GetValue(type)
?? type.GetProperties().FirstOrDefault(p => p.Name == memberName)?.GetValue(type);
if (value is int)
return (int)value;
return 0;
}
}
}
I think you are using the custom render for tabbed page customization. For, Android you should refer the icon from Resource.Drawable. Please try with below code snippet in android renderer.
public class CustomTabRenderer: TabbedRenderer
{
private Activity _act;
protected override void OnModelChanged(VisualElement oldModel, VisualElement newModel)
{
base.OnModelChanged(oldModel, newModel);
_act = this.Context as Activity;
}
// You can do the below function anywhere.
public override void OnWindowFocusChanged(bool hasWindowFocus)
{
ActionBar actionBar = _act.ActionBar;
if (actionBar.TabCount > 0)
{
Android.App.ActionBar.Tab tabOne = actionBar.GetTabAt(0);
tabOne.SetIcon(Resource.Drawable.shell);
}
base.OnWindowFocusChanged(hasWindowFocus);
}
}
Refer this also: https://forums.xamarin.com/discussion/17654/tabbedpage-icons-not-visible-android
Try adding this code to OnElementChanged in TabbedPageRenderer
if (e.PropertyName == "Renderer")
{
ViewPager pager = (ViewPager)ViewGroup.GetChildAt(0);
TabLayout layout = (TabLayout)ViewGroup.GetChildAt(1);
for (int i = 0; i < layout.TabCount; i++)
{
var tab = layout.GetTabAt(i);
var icon = tab.Icon;
if (icon != null)
{
icon = Android.Support.V4.Graphics.Drawable.DrawableCompat.Wrap(icon);
Android.Support.V4.Graphics.Drawable.DrawableCompat.SetTintList(icon, colors);
}
}
}
More info here : https://montemagno.com/xamarin-forms-android-selected-and-unselected-tab-colors/

UWP drag and drop custom type/class

Hy there,
I'm trying to enable drag & drop between 2 GridViews, I managed to do it with the custom types of the "DataPackage" class (SetText, SetBitmap, etc..) but I can't figure out how to do this with a custom class/type.
Both GridViews are data-bound to the same custom class (only a couple of properties, int, string, bitmapimage), I simply would like to drag directly this data items from one GridView to the other.
Thank you very much for your help!
So to summarize for the benefit of others, I added these to event handlers to DataTemplate content, as I only wanted items of a certain (ViewModel) type to be draggable.
private void Grid_Drop(object sender, DragEventArgs e)
{
if (sender is FrameworkElement)
{
var fe = sender as FrameworkElement;
var targetIvm = fe.DataContext as ItemViewModel;
object obj = null;
if(e.DataView.Properties.TryGetValue("ItemViewModel", out obj))
{
var sourceIvm = obj as ItemViewModel;
vm.MoveItem(sourceIvm, targetIvm);
}
}
}
private void Grid_DragStarting(Windows.UI.Xaml.UIElement sender, DragStartingEventArgs args)
{
if (sender is FrameworkElement)
{
var fe = sender as FrameworkElement;
var item = new KeyValuePair<string, object>("ItemViewModel", fe.DataContext);
args.Data.RequestedOperation = DataPackageOperation.Move;
args.Data.Properties.Add("ItemViewModel", fe.DataContext);
}
}
I had the same issue please check this example I used Behaviors because I used MVVM pattern but I did this for ListView but is the same for GridView with small changes.
Change the Behaviors <ListView> to <GridView>
This behavior is attached in the ListView where you want drag the item
public class StartingDragBehavior:Behavior<ListView>
{
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.CanDragItems = true;
this.AssociatedObject.DragItemsStarting += AssociatedObject_DragItemsStarting;
}
private void AssociatedObject_DragItemsStarting(object sender, DragItemsStartingEventArgs e)
{
e.Data.RequestedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Copy;
if(e.Items!=null && e.Items.Any())
{
e.Data.Properties.Add("item", e.Items.FirstOrDefault());
}
}
protected override void OnDetaching()
{
base.OnDetaching();
this.AssociatedObject.DragItemsStarting -= AssociatedObject_DragItemsStarting;
}
}
This behavior is attached in the ListView where you want to drop the item
Here another Behavior to catch the drop event.
public class EndDropBehavior : Behavior<ListView>
{
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.AllowDrop = true;
this.AssociatedObject.Drop += AssociatedObject_Drop;
this.AssociatedObject.DragOver += AssociatedObject_DragOver;
}
private void AssociatedObject_Drop(object sender, Windows.UI.Xaml.DragEventArgs e)
{
if (e.DataView != null &&
e.DataView.Properties != null &&
e.DataView.Properties.Any(x => x.Key == "item" && x.Value.GetType() == typeof(MyObject)))
{
try
{
var def = e.GetDeferral();
var item = e.Data.Properties.FirstOrDefault(x => x.Key == "item");
var card = item.Value as MyObject;
var list = sender as ListView;
var vm = list.DataContext as Infrastructure.ViewModels.CreditCardsViewModel;
vm.MyCollection.Add(card);
def.Complete();
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
}
}
else
{
e.AcceptedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.None;
}
}
private void AssociatedObject_DragOver(object sender, Windows.UI.Xaml.DragEventArgs e)
{
if (e.DataView != null &&
e.DataView.Properties != null &&
e.DataView.Properties.Any(x => x.Key == "item" && x.Value.GetType() == typeof(MyObject)))
{
e.AcceptedOperation = e.AcceptedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Copy;
}
else
{
e.AcceptedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.None;
}
}
protected override void OnDetaching()
{
base.OnDetaching();
this.AssociatedObject.Drop -= AssociatedObject_Drop;
this.AssociatedObject.DragOver -= AssociatedObject_DragOver;
}
}
If you are not using MVVM pattern just check the events of the to Behaviors.

Xamarin UISwipeGestureRecognizer Renderer

In Xamarin.Forms I want to be able to swipe left and right to navigate a list of pictures. At the moment I just want to be able to fire an event each time a swipe is detected.
child class to be used in the renderer:
public class LRMasterDetailPage : ContentView
{
}
I have a ContentPage that uses the LRM class like this:
public class ImagePage : ContentPage
{
public ImagePage(Photo photo)
{
_image = new WebImage
{
Url = photo.Url,
Placeholder = "placeHolder2.png"
};
var imageView = new LRMasterDetailPage {
Content = _image
};
this.Content = imageView;
}
}
This is my Renderer:
[assembly:ExportRenderer (typeof(LRMasterDetailPage), typeof(LRMDPRenderer))]
namespace Project.iOS
{
public class LRMDPRenderer : ViewRenderer<LRMasterDetailPage,UIView>
{
UISwipeGestureRecognizer swipe;
protected override void OnElementChanged (ElementChangedEventArgs<LRMasterDetailPage> e)
{
base.OnElementChanged (e);
// Do someting else, init for example
swipe = new UISwipeGestureRecognizer();
this.AddGestureRecognizer (swipe);
if (swipe.Direction == UISwipeGestureRecognizerDirection.Left)
{
UpdateLeft ();
}
if (swipe.Direction == UISwipeGestureRecognizerDirection.Right)
{
UpdateRight ();
}
}
protected override void OnElementPropertyChanged (object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == "Renderer")
return;
base.OnElementPropertyChanged (sender, e);
}
private void UpdateLeft(){
// Insert view of DetailLeft element into subview
// Add button to open Detail to parent navbar, if not yet there
// Add gesture recognizer for left swipe
Console.WriteLine ("Left swipe");
}
private void UpdateRight(){
// same as for left, but flipped
Console.WriteLine ("Right swipe");
}
}
}
When the ContentPage is display the swipe right event is activated, but nothing happens when I try to swipe on top of the image. I am guessing my logic on the renderer is wrong?
After much pain and searching around the web I found the solution.
What you need to do is simple: in the renderer declare and add the gesture. Make sure you create a swipe gesture for right and left each by declaring their direction. from there use a lambda to call the function you want activated for the specific swipe:
Placeholder class for the Swipe Gestures.
public class LRMasterDetailPage : ContentView
{
}
Image Page that holds one image at the time
public class ImagePage : ContentPage
{
//view holding the image
LRMasterDetailPage imageView;
//collection of images using the photo.Url
ObservableCollection<Image> images;
//current image index
int index = 0;
public ImagePage(){
images = new ObservableCollection<Image> ();
imageView = new LRMasterDetailPage {
Content = this.images [index]
};
this.Content = imageView;
}
//Subscribe to the swipeLeft and swipeRight message
protected override void OnAppearing ()
{
base.OnAppearing ();
MessagingCenter.Subscribe <string> (this,"LeftSwipe", (sender) => {
//Do something
if(index < images.Count-1){
index++;
}
imageView.Content = this.images[index];
});
MessagingCenter.Subscribe <string> (this, "RightSwipe", (sender) => {
//Do something
if(index > 0){
index--;
}
imageView.Content = this.images[index];
});
}
protected override void OnDisappearing()
{
base.OnDisappearing();
//this._image = null;
images = null;
MessagingCenter.Unsubscribe<string>(this,"LeftSwipe");
MessagingCenter.Unsubscribe<string>(this, "RightSwipe");
MessagingCenter.Unsubscribe<string>(this, "LongPress");
//GC.Collect();
}
}
Renderer for LRMasterDetailPage
[assembly:ExportRenderer (typeof(LRMasterDetailPage), typeof(LRMDPRenderer))]
namespace Manager.iOS
{
public class LRMDPRenderer : ViewRenderer<LRMasterDetailPage,UIView>
{
UILongPressGestureRecognizer longPressGestureRecognizer;
UIPinchGestureRecognizer pinchGestureRecognizer;
//UIPanGestureRecognizer panGestureRecognizer;
UISwipeGestureRecognizer swipeRightGestureRecognizer;
UISwipeGestureRecognizer swipeLeftGestureRecognizer;
UIRotationGestureRecognizer rotationGestureRecognizer;
protected override void OnElementChanged (ElementChangedEventArgs<LRMasterDetailPage> e)
{
base.OnElementChanged (e);
longPressGestureRecognizer = new UILongPressGestureRecognizer (() => Console.WriteLine ("Long Press"));
pinchGestureRecognizer = new UIPinchGestureRecognizer (() => Console.WriteLine ("Pinch"));
//panGestureRecognizer = new UIPanGestureRecognizer (() => Console.WriteLine ("Pan"));
swipeRightGestureRecognizer = new UISwipeGestureRecognizer ( () => UpdateRight()){Direction = UISwipeGestureRecognizerDirection.Right};
swipeLeftGestureRecognizer = new UISwipeGestureRecognizer ( () => UpdateLeft()){Direction = UISwipeGestureRecognizerDirection.Left};
rotationGestureRecognizer = new UIRotationGestureRecognizer (() => Console.WriteLine ("Rotation"));
if (e.NewElement == null) {
if (longPressGestureRecognizer != null) {
this.RemoveGestureRecognizer (longPressGestureRecognizer);
}
if (pinchGestureRecognizer != null) {
this.RemoveGestureRecognizer (pinchGestureRecognizer);
}
/*
if (panGestureRecognizer != null) {
this.RemoveGestureRecognizer (panGestureRecognizer);
}
*/
if (swipeRightGestureRecognizer != null) {
this.RemoveGestureRecognizer (swipeRightGestureRecognizer);
}
if (swipeLeftGestureRecognizer != null) {
this.RemoveGestureRecognizer (swipeLeftGestureRecognizer);
}
if (rotationGestureRecognizer != null) {
this.RemoveGestureRecognizer (rotationGestureRecognizer);
}
}
if (e.OldElement == null) {
this.AddGestureRecognizer (longPressGestureRecognizer);
this.AddGestureRecognizer (pinchGestureRecognizer);
//this.AddGestureRecognizer (panGestureRecognizer);
this.AddGestureRecognizer (swipeRightGestureRecognizer);
this.AddGestureRecognizer (swipeLeftGestureRecognizer);
this.AddGestureRecognizer (rotationGestureRecognizer);
}
}
private void UpdateLeft(){
MessagingCenter.Send ("Swiped to the left", "LeftSwipe");
}
private void UpdateRight(){
// same as for left, but flipped
MessagingCenter.Send ("Swiped to the Right", "RightSwipe");
}
}
}

Categories