Xamarin Forms Gradient Renderer not working on iOS - xamarin

I'm trying to use a Gradient Renderer for which I have written a class in PCL and written a renderer for both Android and iOS. Android renderer is working but iOS renderer is not showing the gradient colour at all.
I'm using this Gradient code from XLabs. I'm not sure what's broken. A hint in the right direction would help.
PCL Code:
using Xamarin.Forms;
namespace gradient
{
public enum GradientOrientation
{
Vertical,
Horizontal
}
public class GradientContentView : ContentView
{
public GradientOrientation Orientation
{
get { return (GradientOrientation)GetValue(OrientationProperty); }
set { SetValue(OrientationProperty, value); }
}
public static readonly BindableProperty OrientationProperty =
BindableProperty.Create<GradientContentView, GradientOrientation>(x => x.Orientation, GradientOrientation.Vertical, BindingMode.OneWay);
public Color StartColor
{
get { return (Color)GetValue(StartColorProperty); }
set { SetValue(StartColorProperty, value); }
}
public static readonly BindableProperty StartColorProperty =
BindableProperty.Create<GradientContentView, Color>(x => x.StartColor, Color.White, BindingMode.OneWay);
public Color EndColor
{
get { return (Color)GetValue(EndColorProperty); }
set { SetValue(EndColorProperty, value); }
}
public static readonly BindableProperty EndColorProperty =
BindableProperty.Create<GradientContentView, Color>(x => x.EndColor, Color.Black, BindingMode.OneWay);
}
}
iOS Renderer code:
using CoreAnimation;
using CoreGraphics;
using gradient;
using gradient.iOS;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(GradientContentView), typeof(GradientContentViewRenderer))]
namespace gradient.iOS
{
class GradientContentViewRenderer : VisualElementRenderer<ContentView>
{
private GradientContentView GradientContentView
{
get { return (GradientContentView)Element; }
}
protected CAGradientLayer GradientLayer { get; set; }
protected override void OnElementChanged(ElementChangedEventArgs<ContentView> e)
{
base.OnElementChanged(e);
if (GradientContentView != null &&
NativeView != null)
{
// Create a gradient layer and add it to the
// underlying UIView
GradientLayer = new CAGradientLayer();
GradientLayer.Frame = NativeView.Bounds;
GradientLayer.Colors = new CGColor[]
{
GradientContentView.StartColor.ToCGColor(),
GradientContentView.EndColor.ToCGColor()
};
SetOrientation();
NativeView.Layer.InsertSublayer(GradientLayer, 0);
}
}
protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (GradientLayer != null && GradientContentView != null)
{
// Turn off Animations
CATransaction.Begin();
CATransaction.DisableActions = true;
if (e.PropertyName == GradientContentView.StartColorProperty.PropertyName)
GradientLayer.Colors[0] = GradientContentView.StartColor.ToCGColor();
if (e.PropertyName == GradientContentView.EndColorProperty.PropertyName)
GradientLayer.Colors[1] = GradientContentView.EndColor.ToCGColor();
if (e.PropertyName == VisualElement.WidthProperty.PropertyName ||
e.PropertyName == VisualElement.HeightProperty.PropertyName)
GradientLayer.Frame = NativeView.Bounds;
if (e.PropertyName == GradientContentView.OrientationProperty.PropertyName)
SetOrientation();
CATransaction.Commit();
}
}
void SetOrientation()
{
if (GradientContentView.Orientation == GradientOrientation.Horizontal)
{
GradientLayer.StartPoint = new CGPoint(0, 0.5);
GradientLayer.EndPoint = new CGPoint(1, 0.5);
}
else
{
GradientLayer.StartPoint = new CGPoint(0.5, 0);
GradientLayer.EndPoint = new CGPoint(0.5, 1);
}
}
}
}

This is my code for rendering a gradient background, i am not using orientation, but maybe it helps.
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
if (e.OldElement == null) // perform initial setup
{
ModernOrderCalendar page = e.NewElement as ModernOrderCalendar;
var gradientLayer = new CAGradientLayer();
gradientLayer.Name = "gradient";
CGRect rect = View.Bounds;
gradientLayer.Frame = rect;
gradientLayer.Colors = new CGColor[] { page.StartColor.ToCGColor(), page.EndColor.ToCGColor() };
View.Layer.InsertSublayer(gradientLayer, 0);
}
}
public override void ViewWillLayoutSubviews()
{
base.ViewWillLayoutSubviews();
if (Xamarin.Forms.Device.Idiom == TargetIdiom.Tablet)
{
var gradientLayer = View.Layer.Sublayers.FirstOrDefault(l => l.Name == "gradient");
gradientLayer.Frame = View.Bounds;
View.Layer.Sublayers[0] = gradientLayer;
CGRect frame = View.Bounds;
View.Bounds = frame;
}
}
The main difference I see is that you don't seem to be overriding the ViewWillLayoutSubviews method. I had the same issue, which caused the gradient layer to be created with no height and width during the creation of the window (at that point the View has not layouted, yet).
Therefore I adapt the gradientlayer width and height when layouting the subviews, because at that point width and height of the view are definitely known.

You must update the layer's size in VisualElementRenderer.LayoutSubviews:
public override void LayoutSubviews()
{
base.LayoutSubviews();
CATransaction.Begin();
CATransaction.DisableActions = true;
GradientLayer.Frame = NativeView.Bounds;
CATransaction.Commit();
}

Related

Xmarin.forms iOS check whether captured photo contains face or not

In my xamarin.forms app, I created a custom camera by using Camera view and custom renders. Everything works fine. In android after the photo capture I can check whether the taken photo contains a face or not.It is done by using Camera.IFaceDetectionListener. My question is how can I achieve this in iOS? I know there is vision API. But I don't want the live face tracking. I just simply want to check whether the taken photo contains face. Any help is appreciated.
My iOS CameraPreview
public class UICameraPreview : UIView, IAVCaptureMetadataOutputObjectsDelegate
{
AVCaptureVideoPreviewLayer previewLayer;
public AVCaptureDevice[] videoDevices;
CameraOptions cameraOptions;
public AVCaptureStillImageOutput stillImageOutput;
public AVCaptureDeviceInput captureDeviceInput;
public AVCaptureDevice device;
public event EventHandler<EventArgs> Tapped;
public AVCaptureSession CaptureSession { get; set; }
public bool IsPreviewing { get; set; }
public AVCaptureStillImageOutput CaptureOutput { get; set; }
public UICameraPreview(CameraOptions options)
{
cameraOptions = options;
IsPreviewing = false;
Initialize();
}
public override void LayoutSubviews()
{
base.LayoutSubviews();
if (previewLayer != null)
previewLayer.Frame = Bounds;
}
public override void TouchesBegan(NSSet touches, UIEvent evt)
{
base.TouchesBegan(touches, evt);
OnTapped();
}
protected virtual void OnTapped()
{
var eventHandler = Tapped;
if (eventHandler != null)
{
eventHandler(this, new EventArgs());
}
}
void Initialize()
{
CaptureSession = new AVCaptureSession();
previewLayer = new AVCaptureVideoPreviewLayer(CaptureSession)
{
Frame = Bounds,
VideoGravity = AVLayerVideoGravity.ResizeAspectFill
};
videoDevices = AVCaptureDevice.DevicesWithMediaType(AVMediaType.Video);
var cameraPosition = (cameraOptions == CameraOptions.Front) ? AVCaptureDevicePosition.Front : AVCaptureDevicePosition.Back;
device = videoDevices.FirstOrDefault(d => d.Position == cameraPosition);
if (device == null)
{
return;
}
NSError error;
captureDeviceInput = new AVCaptureDeviceInput(device, out error);
CaptureSession.AddInput(captureDeviceInput);
var dictionary = new NSMutableDictionary();
dictionary[AVVideo.CodecKey] = new NSNumber((int)AVVideoCodec.JPEG);
stillImageOutput = new AVCaptureStillImageOutput()
{
OutputSettings = new NSDictionary()
};
CaptureSession.AddOutput(stillImageOutput);
Layer.AddSublayer(previewLayer);
CaptureSession.StartRunning();
IsPreviewing = true;
}
// Photo Capturing
public async Task CapturePhoto()
{
try
{
var videoConnection = stillImageOutput.ConnectionFromMediaType(AVMediaType.Video);
var sampleBuffer = await stillImageOutput.CaptureStillImageTaskAsync(videoConnection);
var jpegData = AVCaptureStillImageOutput.JpegStillToNSData(sampleBuffer);
var photo = new UIImage(jpegData);
var rotatedPhoto = RotateImage(photo, 180f);
var img = rotatedPhoto;
CALayer layer = new CALayer
{
ContentsScale = 1.0f,
Frame = Bounds,
Contents = rotatedPhoto.CGImage //Contents = photo.CGImage,
};
CaptureSession.StopRunning();
photo.SaveToPhotosAlbum((image, error) =>
{
if (!string.IsNullOrEmpty(error?.LocalizedDescription))
{
Console.Error.WriteLine($"\t\t\tError: {error.LocalizedDescription}");
}
});
}
catch (Exception ex)
{
}
//MainPage.UpdateSource(UIImageFromLayer(layer).AsJPEG().AsStream());
//MainPage.UpdateImage(UIImageFromLayer(layer).AsJPEG().AsStream());
}
}
My CameraPreviewRenderer
public class CameraPreviewRenderer : ViewRenderer<CameraPreview, UICameraPreview>, IAVCaptureMetadataOutputObjectsDelegate
{
UICameraPreview uiCameraPreview;
AVCaptureSession captureSession;
AVCaptureDeviceInput captureDeviceInput;
AVCaptureStillImageOutput stillImageOutput;
protected override void OnElementChanged(ElementChangedEventArgs<CameraPreview> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
// Unsubscribe
uiCameraPreview.Tapped -= OnCameraPreviewTapped;
}
if (e.NewElement != null)
{
if (Control == null)
{
uiCameraPreview = new UICameraPreview(e.NewElement.Camera);
SetNativeControl(uiCameraPreview);
MessagingCenter.Subscribe<Camera_Popup>(this, "CaptureClick", async (sender) =>
{
try
{
// Using messeging center to take photo when clicking button from shared code
var data = new AVCaptureMetadataOutputObjectsDelegate();
await uiCameraPreview.CapturePhoto();
}
catch (Exception ex)
{
return;
}
});
}
MessagingCenter.Subscribe<Camera_Popup>(this, "RetryClick", (sender) =>
{
Device.BeginInvokeOnMainThread(() =>
{
uiCameraPreview.CaptureSession.StartRunning();
uiCameraPreview.IsPreviewing = true;
});
});
MessagingCenter.Subscribe<Camera_Popup>(this, "FlipClick", (sender) =>
{
try
{
var devicePosition = uiCameraPreview.captureDeviceInput.Device.Position;
if (devicePosition == AVCaptureDevicePosition.Front)
{
devicePosition = AVCaptureDevicePosition.Back;
}
else
{
devicePosition = AVCaptureDevicePosition.Front;
}
uiCameraPreview.device = uiCameraPreview.videoDevices.FirstOrDefault(d => d.Position == devicePosition);
uiCameraPreview.CaptureSession.BeginConfiguration();
uiCameraPreview.CaptureSession.RemoveInput(uiCameraPreview.captureDeviceInput);
uiCameraPreview.captureDeviceInput = AVCaptureDeviceInput.FromDevice(uiCameraPreview.device);
uiCameraPreview.CaptureSession.AddInput(uiCameraPreview.captureDeviceInput);
uiCameraPreview.CaptureSession.CommitConfiguration();
}
catch (Exception ex)
{
var abc = ex.InnerException.Message;
}
});
uiCameraPreview.Tapped += OnCameraPreviewTapped;
}
}
void OnCameraPreviewTapped(object sender, EventArgs e)
{
if (uiCameraPreview.IsPreviewing)
{
uiCameraPreview.CaptureSession.StopRunning();
uiCameraPreview.IsPreviewing = false;
}
else
{
uiCameraPreview.CaptureSession.StartRunning();
uiCameraPreview.IsPreviewing = true;
}
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
Control.CaptureSession.Dispose();
Control.Dispose();
}
base.Dispose(disposing);
}
}
The CoreImage framework has CIDetector that provides image detectors for faces, QR codes, text, .... in which you pass in an image and you get a specific "feature set" back.
Example from Xamarin docs:
var imageFile = "photoFace2.jpg";
var image = new UIImage(imageFile);
var context = new CIContext ();
var detector = CIDetector.CreateFaceDetector (context, true);
var ciImage = CIImage.FromCGImage (image.CGImage);
var features = detector.GetFeatures (ciImage);
Console.WriteLine ("Found " + features.Length + " faces");
re: https://learn.microsoft.com/en-us/dotnet/api/CoreImage.CIDetector?view=xamarin-ios-sdk-12

How to set height of ProgressBar on Xamarin Forms 3.3?

I'm using Xamarin Forms 3.3, I want change height of ProgressBar. I execute it:
How to change height of progress bar in Xamarin Forms?
But I can't change height of ProgressBar. This is my code:
1) Class custom control ProgressBar:
public class ColorProgressBar : ProgressBar
{
public static BindableProperty BarColorProperty = BindableProperty.Create(
nameof(BarColor), typeof(Color), typeof(ColorProgressBar), default(Color));
public static BindableProperty ProgressTintColorProperty = BindableProperty.Create(
nameof(ProgressTintColor), typeof(Color), typeof(ColorProgressBar), default(Color));
public static BindableProperty TrackTintColorProperty = BindableProperty.Create(
nameof(TrackTintColor), typeof(Color), typeof(ColorProgressBar), default(Color));
public Color BarColor
{
get => (Color)GetValue(BarColorProperty);
set => SetValue(BarColorProperty, value);
}
public Color ProgressTintColor
{
get => (Color)GetValue(ProgressTintColorProperty);
set => SetValue(ProgressTintColorProperty, value);
}
public Color TrackTintColor
{
get => (Color)GetValue(TrackTintColorProperty);
set => SetValue(TrackTintColorProperty, value);
}
public float HeightOfProgressBar { get; set; }
}
2) For iOS:
public class ColorProgressBarRenderer : ProgressBarRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs<ProgressBar> e)
=> iOS.Control.Logger.InvokeAction(() =>
{
base.OnElementChanged(e);
if (e.NewElement == null)
return;
if (Control != null)
{
UpdateBarColor();
}
});
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
=> iOS.Control.Logger.InvokeAction(() =>
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == ColorProgressBar.BarColorProperty.PropertyName)
{
UpdateBarColor();
}
});
private void UpdateBarColor() => iOS.Control.Logger.InvokeAction(() =>
{
var element = (ColorProgressBar) Element;
if (element == null)
return;
Control.TintColor = element.BarColor.ToUIColor();
Control.ProgressTintColor = element.ProgressTintColor.ToUIColor();
Control.TrackTintColor = element.TrackTintColor.ToUIColor();
});
public override void LayoutSubviews()
{
base.LayoutSubviews();
var element = (ColorProgressBar)Element;
if (element == null)
return;
var X = 1.0f;
var Y = element.HeightOfProgressBar > 0.0f ? element.HeightOfProgressBar : 1.0f;
var transform = CGAffineTransform.MakeScale(X, Y);
Transform = transform;
}
}
And At View .xaml:
<control:ColorProgressBar Grid.Row="1"
HeightOfProgressBar="8.0"
TrackTintColor="{x:Static color:BasePalette.DarkestColor}"
ProgressTintColor="{x:Static color:NeutralPalette.RedColor}"
Progress="{Binding PercentDataValue}">
</control:ColorProgressBar>
I test on device run iOS 12.0.
Thanks!
You missed a comment on the linked SO thread that said:
To get the iOS renderer to work I needed to change this.Transform = transform; to Control.Transform = transform;
So changing Transform = transform; in the LayoutSubviews method of the ColorProgressBarRenderer to Control.Transform = transform; makes your sample work as expected, i.e the height of the progress bar is changed:
public override void LayoutSubviews()
{
base.LayoutSubviews();
var element = (ColorProgressBar)Element;
if (element == null)
return;
var X = 1.0f;
var Y = element.HeightOfProgressBar > 0.0f ? element.HeightOfProgressBar : 1.0f;
var transform = CGAffineTransform.MakeScale(X, Y);
Control.Transform = transform; // <---- change here
}

Custom Renderer for Picker in Xamarin.Forms

I want to customize my picker. I created a custom renderer for my picker but I dont know how the customization settings. How can I change the font style and size of the item? and How can I remove the two lines?
public class CustomPickerRenderer : PickerRenderer
{
public CustomPickerRenderer(Context context) : base(context)
{
AutoPackage = false;
}
protected override void OnElementChanged(ElementChangedEventArgs<Picker> e)
{
base.OnElementChanged(e);
if (e.OldElement == null)
{
Control.Background = null;
var layoutParams = new MarginLayoutParams(Control.LayoutParameters);
layoutParams.SetMargins(0, 0, 0, 0);
Control.LayoutParameters = layoutParams;
Control.SetPadding(0, 0, 0, 0);
SetPadding(0, 0, 0, 0);
}
}
}
If you want to set the fontSize of the text , you first need to customize a subclass extends from NumberPicker and overwrite the method AddView.
public class TextColorNumberPicker: NumberPicker
{
public TextColorNumberPicker(Context context) : base(context)
{
}
public override void AddView(View child, int index, ViewGroup.LayoutParams #params)
{
base.AddView(child, index, #params);
UpdateView(child);
}
public void UpdateView(View view)
{
if ( view is EditText ) {
//set the font of text
((EditText)view).TextSize = 8;
}
}
}
If you want to remove the lines,you should rewrite the NumberPicker
in Android Custom Renderer
public class MyAndroidPicker:PickerRenderer
{
IElementController ElementController => Element as IElementController;
public MyAndroidPicker()
{
}
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 SetPickerDividerColor(TextColorNumberPicker picker)
{
Field[] fields = picker.Class.GetDeclaredFields();
foreach (Field pf in fields)
{
if(pf.Name.Equals("mSelectionDivider"))
{
pf.Accessible = true;
// set the color as transparent
pf.Set(picker, new ColorDrawable(this.Resources.GetColor(Android.Resource.Color.Transparent)));
}
}
}
private void Control_Click(object sender, EventArgs e)
{
Picker model = Element;
var picker = new TextColorNumberPicker(Context);
if (model.Items != null && model.Items.Any())
{
picker.MaxValue = model.Items.Count - 1;
picker.MinValue = 0;
picker.SetBackgroundColor(Android.Graphics.Color.Yellow);
picker.SetDisplayedValues(model.Items.ToArray());
//call the method after you setting DisplayedValues
SetPickerDividerColor(picker);
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) =>
{
ElementController.SetValueFromRenderer(VisualElement.IsFocusedProperty, false);
// It is possible for the Content of the Page to be changed when Focus is changed.
// In this case, we'll lose our Control.
Control?.ClearFocus();
_dialog = null;
});
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.
Control?.ClearFocus();
}
_dialog = null;
});
_dialog = builder.Create();
_dialog.DismissEvent += (ssender, args) =>
{
ElementController?.SetValueFromRenderer(VisualElement.IsFocusedProperty, false);
};
_dialog.Show();
}
}
I also used this CustomRenderer which was posted before only instead of overriding it you can change the properties like this.
private void Control_Click(object sender, EventArgs e)
{
Picker model = Element;
var picker = new MyNumberPicker(Context);
if (model.Items != null && model.Items.Any())
{
// set style here
picker.MaxValue = model.Items.Count - 1;
picker.MinValue = 0;
picker.SetBackgroundColor(Android.Graphics.Color.Transparent);
picker.SetDisplayedValues(model.Items.ToArray());
//call the method after you setting DisplayedValues
SetPickerDividerColor(picker);
picker.WrapSelectorWheel = false;
picker.Value = model.SelectedIndex;
// change Text Size and Divider
picker.TextSize = 30;
picker.SelectionDividerHeight = 1;
}

How can I change the font for the header of a Navigation page with Xamarin Forms?

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 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.
You need Custom Renderer , refer to this sample
iOS
[assembly: ExportRenderer(typeof(CustomNavigationPage), typeof(CustomNavigationPageRenderer))]
namespace CustomFontsNavigationPage.iOS.Renderers
{
public class CustomNavigationPageRenderer : NavigationRenderer
{
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
if (e.NewElement != null)
{
var att = new UITextAttributes();
UIFont customFont = UIFont.FromName("Trashtalk", 20);
UIFont systemFont = UIFont.SystemFontOfSize(20.0);
UIFont systemBoldFont = UIFont.SystemFontOfSize(20.0 , FontAttributes.Bold);
att.Font = font;
UINavigationBar.Appearance.SetTitleTextAttributes(att);
}
}
}
}
Android
[assembly: ExportRenderer(typeof(CustomNavigationPage), typeof(CustomNavigationPageRenderer))]
namespace CustomFontsNavigationPage.Droid.Renderers
{
public class CustomNavigationPageRenderer : NavigationPageRenderer
{
private Android.Support.V7.Widget.Toolbar _toolbar;
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();
if (e.Child.GetType() == typeof(Android.Widget.TextView))
{
var textView = (Android.Widget.TextView)e.Child;
var spaceFont = Typeface.CreateFromAsset(Forms.Context.ApplicationContext.Assets, "Trashtalk.ttf");
var systemFont = Typeface.DEFAULT;
var systemBoldFont = Typeface.DEFAULT_BOLD;
textView.Typeface = spaceFont;
_toolbar.ChildViewAdded -= Toolbar_ChildViewAdded;
}
}
}
}
There is no need in a custom renderer on iOS, you can just use the Appearance API:
UINavigationBar.Appearance.SetTitleTextAttributes(new UITextAttributes
{
Font = UIFont.FromName("MyCoolFont", 20)
});
In Android you do need a renderer, however you should check against Android.Support.V7.Widget.AppCompatTextView and not Android.Widget.TextView.
Tested on Xamarin.Forms 3.4.0

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