CustomRenderer for Button background color transition - xamarin

I have made a customrenderer for changing the TitleColor and BackgroundColor when an user click my button.
For some reason, when the button is focused, the TitleColor is turning more or less transparent, and my background doesn't animate like my TitleColor.
This is my code:
public class BtnRendereriOS : ButtonRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Button> e)
{
base.OnElementChanged(e);
if (Control != null)
{
Control.Layer.CornerRadius = 0;
BackgroundNormalState(Control);
Control.TouchUpInside += (object sender, EventArgs c) =>
{
BackgroundNormalState(sender);
};
Control.TouchUpOutside += (object sender, EventArgs c) =>
{
BackgroundNormalState(sender);
};
Control.TouchDragOutside += (object sender, EventArgs c) =>
{
BackgroundNormalState(sender);
};
Control.TouchDragInside += (object sender, EventArgs c) =>
{
BackgroundChangedState(sender);
};
Control.TouchDown += (object sender, EventArgs c) =>
{
BackgroundChangedState(sender);
};
void BackgroundNormalState(object sender)
{
Action a = () => Control.SetTitleColor(UIColor.FromRGB(255,255,255), UIControlState.Normal);
UIView.PerformWithoutAnimation(a);
(sender as UIButton).BackgroundColor = UIColor.FromRGB(3, 169, 244);
}
void BackgroundChangedState(object sender)
{
Action a = () => Control.SetTitleColor(UIColor.FromRGB(0,0,0), UIControlState.Highlighted);
UIView.PerformWithoutAnimation(a);
(sender as UIButton).BackgroundColor = UIColor.FromRGB(104, 206, 253);
}
}
}
}
Currently, I have removed the animation from the TitleColor.
My questions are:
How do I add the same transition to my BackgroundColor similar to the default transition on my TitleColor?
How do I remove the "transparency" from the TitleColor when the button is being focused?

How do I remove the "transparency" from the TitleColor when the button
is being focused?
This is the feature of system button, if you don't like this try to use Custom style to avoid it:
UIButton btn = new UIButton(UIButtonType.Custom);
btn.SetTitle(Control.TitleLabel.Text, UIControlState.Normal);
SetNativeControl(btn);
// Pay attention to that the title color just needs to be set once
btn.SetTitleColor(UIColor.FromRGB(255, 255, 255), UIControlState.Normal);
btn.SetTitleColor(UIColor.FromRGB(0, 0, 0), UIControlState.Highlighted);
Then you can use UIView.Animate() to change your button's background color with "transition":
void BackgroundNormalState(object sender)
{
UIView.Animate(0.2, () =>
{
(sender as UIButton).BackgroundColor = UIColor.FromRGB(3, 169, 244);
});
}
void BackgroundChangedState(object sender)
{
UIView.Animate(0.2, () =>
{
(sender as UIButton).BackgroundColor = UIColor.FromRGB(104, 206, 253);
});
}

You can either use UIView.Animate|Async or UIView.TransitionNotify|Async depending upon the end result you are looking for. The key is to place all the changes within the same animation block so they occur at the same time via Core Animation (CA).
Here is an example using UIView.TransitionNotifyAsync using a TransitionCrossDissolve between the color changes.
// Set intial values
var normalTextColor = UIColor.FromRGB(255, 255, 255);
var normalBackgroundColor = UIColor.FromRGB(3, 169, 244);
var highlightTextColor = UIColor.Black;
var highlightBackgroundColor = UIColor.FromRGB(104, 206, 253);
Control.SetTitleColor(normalTextColor, UIControlState.Normal);
Control.BackgroundColor = normalBackgroundColor;
async Task NormalColorState(UIButton button)
{
await UIView.TransitionNotifyAsync(button, .25, UIViewAnimationOptions.TransitionCrossDissolve, () =>
{
button.BackgroundColor = normalBackgroundColor;
button.SetTitleColor(normalTextColor, UIControlState.Normal);
});
}
Control.TouchDown += async (object sender, EventArgs e) =>
{
var button = sender as UIButton;
await UIView.TransitionNotifyAsync(button, .25, UIViewAnimationOptions.TransitionCrossDissolve, () =>
{
button.BackgroundColor = highlightBackgroundColor;
button.SetTitleColor(highlightTextColor, UIControlState.Highlighted);
});
};
Control.TouchUpInside += async (object sender, EventArgs e) =>
{
await NormalColorState(sender as UIButton);
};
Control.TouchUpOutside += async (object sender, EventArgs e) =>
{
await NormalColorState(sender as UIButton);
};

Related

CustomRenderer iOS for button TouchUpInside change background

I'm trying to write a CustomRenderer for iOS through which I want to change the BackgroundColor of the button when the user touches it. So far i got this:
public class BtnRendereriOS : ButtonRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Button> e)
{
base.OnElementChanged(e);
if (Control != null)
{
Control.BackgroundColor = UIColor.FromRGB(3, 169, 244);
Control.Layer.CornerRadius = 0;
}
Control.TouchUpInside += (sender, UIButton) => {
Control.BackgroundColor = UIColor.Brown;
};
}
}
Its not working however. I guess there needs to be some sort of eventhandler in order to make this possible.
The background colors needs to be initially set (if not the default color), then you need to set it back to that same color on TouchUpInside and TouchUpOutside. On TouchDown, set it to the color that you wish it to be while the button is being pressed.
Toggle background color:
if (Control != null)
{
void BackgroundNormalState(object sender)
{
(sender as UIButton).BackgroundColor = UIColor.Green;
}
BackgroundNormalState(Control);
Control.TouchUpInside += (object sender, EventArgs e) =>
{
BackgroundNormalState(sender);
};
Control.TouchUpOutside += (object sender, EventArgs e) =>
{
BackgroundNormalState(sender);
};
Control.TouchDown += (object sender, EventArgs e) =>
{
(sender as UIButton).BackgroundColor = UIColor.Red;
};
}
Update:
Can i change my textcolor through this methodgroup as well?
There are is a SetTitleColor where you can assign different colors to the different UIControlState values, Normal and Highlighted are the ones to start with:
Control.SetTitleColor(UIColor.Red, UIControlState.Normal);
Control.SetTitleColor(UIColor.Green, UIControlState.Highlighted);

Nullreference on popmodal and custom iOS renderer

I have a modal view, in which I have multiple Entry fields that I through an iOS customrenderer have customized to change BorderColor when Focused.
When i pop my modal view on button press:
await Navigation.PopModalAsync(true);
I get a nullreference in my iOS customrenderer, because I guess the element suddently becomes null, and i somehow haven't told it, that the view is gone.
public class BorderColorChange : EntryRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
{
base.OnElementChanged(e);
if (Control != null)
{
Control.Layer.BorderWidth = 1;
Control.Layer.CornerRadius = 4;
e.NewElement.Focused += (sender, evt) =>
{
Control.Layer.BorderColor = UIColor.FromRGB(3, 169, 244).CGColor;
};
e.NewElement.Unfocused += (sender, evt) =>
{
Control.Layer.BorderColor = UIColor.LightGray.CGColor;
};
};
}
}
I've noticed, that when i remove the await keyword from Navigation.PopModalAsync(true); it doesn't produce the error.
Any help on how to solve this error?
It is perfectly normal for OnElementChanged to be called with e.NewElement==null. This just means that the element is being removed (like when you await the PopModelAsync), so it should handle the change that the new element to associate with is null.
With custom renderers, you need to both subscribe and unsubscribe to events when changes occur in associating your custom renderer with a native control. So for example:
public class BorderColorChange : EntryRenderer
{
private void MyFocusedEventHandler(...) ...
private void MyUnfocusedEventHandler(...) ...
protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
{
base.OnElementChanged(e);
if (Control != null)
{
Control.Layer.BorderWidth = 1;
Control.Layer.CornerRadius = 4;
if (e.OldElement != null) // unsubscribe from events on old element
{
e.OldElement.Focused -= MyFocusedEventHandler;
e.OldElement.Unfocused -= MyUnfocusedEventHandler;
}
if (e.NewElement != null) // subscribe to events on new element
{
e.NewElement.Focused += MyFocusedEventHandler;
e.NewElement.Unfocused += MyUnfocusedEventHandler;
}
}
}
}
The logic for what to do when the entry gets/loses focus goes into the MyFocusedEventHandler/MyUnfocusedEventHandler rather than inline to allow for both subscribing and unsubscribing.

Receiving event Button in a Grid event from outside

I have created a custom control.
It consists of a Grid in which a Button is placed:
using Xamarin.Forms;
namespace MyApp
{
public class clsButton : ContentView
{
private Grid _grid;
private Button _button;
public clsButton()
{
_grid = new Grid
{
Margin = new Thickness(0),
VerticalOptions = LayoutOptions.FillAndExpand,
HorizontalOptions = LayoutOptions.FillAndExpand,
};
_grid.BindingContext = this;
_button = new Button()
{
};
_button.Clicked += async (sender, e) =>
{
//I tried different things here, but none gave me the right results. I need to "bubble" this click to the outside
return;
};
_grid.Children.Add(_button, 0, 0);
this.Content = _grid;
}
}
}
I create some of these custom controls in a ContentPage like this:
_MyButton = new clsImageButton()
{
};
var nTapGestureRecognizer = new TapGestureRecognizer();
nTapGestureRecognizer.Tapped += OnButtonClicked;
_MyButton.GestureRecognizers.Add(nTapGestureRecognizer);
And this is the void in the same ContentPage:
async void OnButtonClicked(object sender,EventArgs e)
{
//I don't managed to get here
}
This doesn't work.
"OnButtonClicked" is never called.
I think I have to raise an event from within the custom control.
I tried some things, but none of them were successful.
How would I do this correctly?
in your clsButton, declare a public event
public EventHandler ButtonClicked { get; set; }
then raise the event when your button is clicked
_button.Clicked += async (sender, e) =>
{
if (ButtonClicked != null) ButtonClicked(this,e);
};
finally, where ever you are using clsButton, you can subscribe to the event (the gesture recognizer is not needed)
var btn = new clsButton();
btn.ButtonClicked += async (sender, e) => {
// respond here
}

How to change Picker Border color in xamarin forms

My borderless custom renderer for picker
public class BorderlessPickerRenderer : PickerRenderer
{
public static void Init() { }
protected override void OnElementChanged(ElementChangedEventArgs<Picker> e)
{
base.OnElementChanged(e);
if (e.OldElement == null)
{
Control.Background = null;
}
}
}
It will change the picker list text color as white. please see the screenshot
If you check the source code of PickerRenderer, you will find that the Dialog is totally generated in the code behind.
So here to set a Transparent(border-less) background, we can re-write the Click event of this control, for example:
public class BorderlessPickerRenderer : Xamarin.Forms.Platform.Android.PickerRenderer
{
private IElementController ElementController => Element as IElementController;
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.DescendantFocusability = DescendantFocusability.BlockDescendants;
picker.Value = model.SelectedIndex;
}
var layout = new LinearLayout(Context) { Orientation = Orientation.Vertical };
layout.AddView(picker);
ElementController.SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, true);
var builder = new AlertDialog.Builder(Context);
builder.SetView(layout);
builder.SetTitle(model.Title ?? "");
builder.SetNegativeButton(global::Android.Resource.String.Cancel, (s, a) =>
{
ElementController.SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, 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;
});
builder.SetPositiveButton(global::Android.Resource.String.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.IsFocusedPropertyKey, 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.
Control?.ClearFocus();
}
_dialog = null;
});
_dialog = builder.Create();
_dialog.DismissEvent += (ssender, args) =>
{
ElementController?.SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, false);
};
_dialog.Show();
_dialog.Window.SetBackgroundDrawable(new ColorDrawable(Android.Graphics.Color.Transparent));
}
}
Rendering image of this custom picker:
The font color and button's style can be modified as you need since you created this dialog by yourself. And the style of the dialog also depends on the style of your app.

Xamarin.Forms gradient background for Button using Custom Renderer with iOS

I am using Xamarin.Forms and I want to globally make the buttons look a little nicer.
I have achieved this just fine for the Android version using a custom renderer, but I am having trouble doing the same with iOS.
When defining buttons in my XAML pages, I reference "CustomButton" instead of "Button", and then I have the following CustomButtonRenderer in my iOS app.
Most of the style changes work just fine (border radius, etc), but I cannot seem to make it render a background gradient for the button.
Here is my code so far, but the background just displays as white. How can I make it display a gradient with the text on top?
class CustomButtonRenderer : ButtonRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<Button> e)
{
base.OnElementChanged(e);
if (Control != null)
{
var gradient = new CAGradientLayer();
gradient.Frame = Control.Layer.Bounds;
gradient.Colors = new CGColor[]
{
UIColor.FromRGB(51, 102, 204).CGColor,
UIColor.FromRGB(51, 102, 204).CGColor
};
Control.Layer.AddSublayer(gradient);
Control.Layer.CornerRadius = 10;
Control.Layer.BorderColor = UIColor.FromRGB(51, 102, 204).CGColor;
Control.Layer.BorderWidth = 1;
Control.VerticalAlignment = UIControlContentVerticalAlignment.Center;
}
}
}
1st) Do not use AddSublayer, use InsertSublayerBelow so that the Z-order will be correct and your Title text will be on top.
2nd) Override LayoutSubviews and update your CAGradientLayer frame to match your UIButton.
3rd) Enjoy your gradient:
Complete Example:
[assembly: ExportRenderer(typeof(CustomButton), typeof(CustomButtonRenderer))]
namespace AppCompatRender.iOS
{
public class CustomButtonRenderer : Xamarin.Forms.Platform.iOS.ButtonRenderer
{
public override void LayoutSubviews()
{
foreach (var layer in Control?.Layer.Sublayers.Where(layer => layer is CAGradientLayer))
layer.Frame = Control.Bounds;
base.LayoutSubviews();
}
protected override void OnElementChanged(ElementChangedEventArgs<Button> e)
{
base.OnElementChanged(e);
if (e.OldElement == null)
{
var gradient = new CAGradientLayer();
gradient.CornerRadius = Control.Layer.CornerRadius = 10;
gradient.Colors = new CGColor[]
{
UIColor.FromRGB(51, 102, 104).CGColor,
UIColor.FromRGB(51, 202, 204).CGColor
};
var layer = Control?.Layer.Sublayers.LastOrDefault();
Control?.Layer.InsertSublayerBelow(gradient, layer);
}
}
}
}
Update:
If you are using iOS 10+ with newer version of Xamarin.Forms, the Control.Bounds during calls to LayoutSubViews will all be zeros. You will need to set the gradient layer Frame size during sets to the control's Frame property, i.e.:
public class CustomButtonRenderer : Xamarin.Forms.Platform.iOS.ButtonRenderer
{
public override CGRect Frame
{
get
{
return base.Frame;
}
set
{
if (value.Width > 0 && value.Height > 0)
{
foreach (var layer in Control?.Layer.Sublayers.Where(layer => layer is CAGradientLayer))
layer.Frame = new CGRect(0, 0, value.Width, value.Height);
}
base.Frame = value;
}
}
protected override void OnElementChanged(ElementChangedEventArgs<Button> e)
{
base.OnElementChanged(e);
if (e.OldElement == null)
{
var gradient = new CAGradientLayer();
gradient.CornerRadius = Control.Layer.CornerRadius = 20;
gradient.Colors = new CGColor[]
{
UIColor.FromRGB(51, 102, 104).CGColor,
UIColor.FromRGB(51, 202, 204).CGColor
};
var layer = Control?.Layer.Sublayers.LastOrDefault();
Control?.Layer.InsertSublayerBelow(gradient, layer);
}
}
}
In the moment that OnElementChanged is called, Control.Layer.Bounds is completely zero'd out. In your rendered you will need to add methods to update the Gradient's Frame to match the Control.Layer's frame.
Responding to the comment by Robert Cafazzo, I can help to slightly adjust this render so that it works correctly:
public class GdyBtnRendererIos : ButtonRenderer
{
#region Colors
static Color rosecolor = (Color)App.Current.Resources["ClrGeneralrose"];
static Color orangecolor = (Color)App.Current.Resources["ClrRoseOrange"];
CGColor roseCGcolor = rosecolor.ToCGColor();
CGColor orangeCGcolor = orangecolor.ToCGColor();
#endregion
CAGradientLayer gradient;
public override CGRect Frame
{
get => base.Frame;
set
{
if (value.Width > 0 && value.Height > 0)
{
if (Control?.Layer.Sublayers != null)
foreach (var layer in Control?.Layer.Sublayers.Where(layer => layer is CAGradientLayer))
layer.Frame = new CGRect(0, 0, value.Width, value.Height);
}
base.Frame = value;
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == "Renderer")
{
gradient.Frame = new CGRect(0, 0, Frame.Width, Frame.Height);
}
}
protected override void OnElementChanged(ElementChangedEventArgs<Button> e)
{
base.OnElementChanged(e);
if (e.OldElement != null) return;
gradient = new CAGradientLayer
{
CornerRadius = Control.Layer.CornerRadius,
Colors = new CGColor[] { roseCGcolor, orangeCGcolor },
StartPoint = new CGPoint(0.1, 0.5),
EndPoint = new CGPoint(1.1, 0.5)
};
var layer = Control?.Layer.Sublayers.LastOrDefault();
Control?.Layer.InsertSublayerBelow(gradient, layer);
base.Draw(Frame);
}

Resources