Search code examples
xamarinxamarin.formsxamarin.ioscustom-renderer

Xamarin IOS Custom Renderer overriden Draw method not called


I am trying to load a customized slider control in a listview (with accordeon behaviour). When the View loads all the listview elements are collapsed so the slider control visibility is false. I observed that the overriden Draw method within the ios renderer is not called while the control is not visible so I end up having the native control within my listview.

I have reproduced the issue in a separate project:

I have the IOS custom renderer:

public class CustomGradientSliderRenderer : SliderRenderer
{
    public CGColor StartColor { get; set; }
    public CGColor CenterColor { get; set; }
    public CGColor EndColor { get; set; }
    protected override void OnElementChanged(ElementChangedEventArgs<Slider> e)
    {

        if (Control == null)
        {
            var customSlider = e.NewElement as CustomGradientSlider;
            StartColor = customSlider.StartColor.ToCGColor();
            CenterColor = customSlider.CenterColor.ToCGColor();
            EndColor = customSlider.EndColor.ToCGColor();

            var slider = new SlideriOS
            {
                Continuous = true,

                Height = (nfloat)customSlider.HeightRequest
            };

            SetNativeControl(slider);
        }

        base.OnElementChanged(e);
    }

    public override void Draw(CGRect rect)
    {
        base.Draw(rect);

        if (Control != null)
        {
            Control.SetMinTrackImage(CreateGradientImage(rect.Size), UIControlState.Normal);
        }
    }

    void OnControlValueChanged(object sender, EventArgs eventArgs)
    {
        ((IElementController)Element).SetValueFromRenderer(Slider.ValueProperty, Control.Value);
    }

    public UIImage CreateGradientImage(CGSize rect)
    {
        var gradientLayer = new CAGradientLayer()
        {
            StartPoint = new CGPoint(0, 0.5),
            EndPoint = new CGPoint(1, 0.5),
            Colors = new CGColor[] { StartColor, CenterColor, EndColor },
            Frame = new CGRect(0, 0, rect.Width, rect.Height),
            CornerRadius = 5.0f
        };

        UIGraphics.BeginImageContext(gradientLayer.Frame.Size);
        gradientLayer.RenderInContext(UIGraphics.GetCurrentContext());
        var image = UIGraphics.GetImageFromCurrentImageContext();
        UIGraphics.EndImageContext();

        return image.CreateResizableImage(UIEdgeInsets.Zero);
    }
}

public class SlideriOS : UISlider
{
    public nfloat Height { get; set; }

    public override CGRect TrackRectForBounds(CGRect forBounds)
    {
        var rect = base.TrackRectForBounds(forBounds);
        return new CGRect(rect.X, rect.Y, rect.Width, Height);
    }
}

The View with codebehind:

Main.xaml:

    <?xml version="1.0" encoding="utf-8" ?>
    <ContentPage
    x:Class="GradientSlider.MainPage"
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:GradientSlider">
    <ContentPage.Content>

    <Grid>
    <StackLayout  x:Name="SliderContainer">

        <local:CustomGradientSlider
            x:Name="mySlider"
            CenterColor="#feeb2f"
            CornerRadius="16"
            EndColor="#ba0f00"
            HeightRequest="20"
            HorizontalOptions="FillAndExpand"
            Maximum="10"

            Minimum="0"
            StartColor="#6bab29"
            VerticalOptions="CenterAndExpand"
            MaximumTrackColor="Transparent"

            ThumbColor="green"
            />
        <Label x:Name="lblText" Text="txt"
               VerticalOptions="Center" HorizontalOptions="Center"/>
    </StackLayout>

    <Button Text="Magic" Clicked="Button_Tapped" WidthRequest="100" HeightRequest="50" VerticalOptions="Center" HorizontalOptions="Center"/>

         </Grid>
        </ContentPage.Content>

</ContentPage>

Main.xaml.cs:

    using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;

namespace GradientSlider
{
    public partial class MainPage : ContentPage, INotifyPropertyChanged

    {

        public MainPage()
        {
            InitializeComponent();
            SliderContainer.IsVisible = false;
        }


        void Button_Tapped(object sender,ClickedEventArgs a)
        {
            SliderContainer.IsVisible = !SliderContainer.IsVisible;

        }


    }
}

So in the scenario above you can see that when I load the main.xaml the control is invisible (SliderContainer.IsVisible = false;) in this case I get a native slider control and not my custom one. If I change in the constructor SliderContainer.IsVisible = true; then I get my custom control.

After an investigation I realised that if the control is not visible when the view loads the public override void Draw(CGRect rect) is not called. I could not find any solution to trigger the Draw method while the control is invisible.

Anybody has an idea how to load a custom renderer correctly while the control is not visible ?

Thank you!


Solution

  • Assuming the renderer is overriding OnElementPropertyChanged:

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