Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I add a border to the top and bottom of an iOS grid in Xamarin?

I have this XAML. What I would like to do is to put a 1px line at the top and bottom of the grid with an iOS renderer. Can someone tell me is there a special way to put a border line just at the top and bottom of a grid using a renderer?

<Grid x:Name="phraseGrid" BackgroundColor="Transparent" 
        Margin="0,55,0,0" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
        <Grid.RowDefinitions>
            <RowDefinition Height="10*" />
            <RowDefinition Height="6*" />
            <RowDefinition Height="80*" />
            <RowDefinition Height="13*" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <Grid x:Name="prGrid" Grid.Row="0" Grid.Column="0" 
            Padding="5,0,0,0" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
            BackgroundColor="#EEEEEE">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="25*" />
                <ColumnDefinition Width="25*" />
                <ColumnDefinition Width="50*" />
            </Grid.ColumnDefinitions>
            <Label x:Name="cards" Style="{StaticResource smallLabel}" Grid.Row="0" Grid.Column="0" />
            <Label x:Name="points" Style="{StaticResource smallLabel}" Grid.Row="0" Grid.Column="1" />
            <Label x:Name="timer" Style="{StaticResource smallLabel}" Grid.Row="0" Grid.Column="2" />
        </Grid>
like image 862
Alan2 Avatar asked Aug 11 '17 21:08

Alan2


1 Answers

From maintainability and complexity point of view, I would recommend that you create a couple of bindable properties and use it to render borders.

There are three options available to implement this:

1. Platform-renderer: Extend Grid with properties and draw borders at platform level.

2. Forms control: Use Padding and BackgroundColor to give appearance of a border.

3. Platform-effect: Create a PlatformEffect to render border (in this case we define attached bindable properties), and attach to any visual-element.


Option-1: Platform renderer approach

You can extend Grid to create a custom control and implement its corresponding renderer. This code sample illustrates how to implement this using custom control approach.

Custom control implementation:

public class ExtendedGrid : Grid
{
    /// <summary>
    /// The border color property.
    /// </summary>
    public static readonly BindableProperty BorderColorProperty =
        BindableProperty.Create(
            "BorderColor", typeof(Color), typeof(ExtendedGrid),
        defaultValue: Color.Black);

    /// <summary>
    /// Gets or sets the color of the border.
    /// </summary>
    /// <value>The color of the border.</value>
    public Color BorderColor
    {
        get { return (Color)GetValue(BorderColorProperty); }
        set { SetValue(BorderColorProperty, value); }
    }

    /// <summary>
    /// The border width property.
    /// </summary>
    public static readonly BindableProperty BorderWidthProperty =
        BindableProperty.Create(
        "BorderWidth", typeof(Thickness), typeof(ExtendedGrid),
        defaultValue: new Thickness(1));

    /// <summary>
    /// Gets or sets the width of the border.
    /// </summary>
    /// <value>The width of the border.</value>
    public Thickness BorderWidth
    {
        get { return (Thickness)GetValue(BorderWidthProperty); }
        set { SetValue(BorderWidthProperty, value); }
    }

    protected override void OnPropertyChanged(string propertyName = null)
    {
        base.OnPropertyChanged(propertyName);

        if(nameof(Padding).Equals(propertyName) || nameof(BorderWidth).Equals(propertyName))
        {
            double minLeft, minRight, minTop, minBottom;
            // ensure padding is always greater than borderwidth - we will have overlapping issue with client-area
            minLeft = Math.Max(Padding.Left, BorderWidth.Left);
            minRight = Math.Max(Padding.Right, BorderWidth.Right);
            minTop = Math.Max(Padding.Top, BorderWidth.Top);
            minBottom = Math.Max(Padding.Bottom, BorderWidth.Bottom);

            var minPadding = new Thickness(minLeft, minTop, minRight, minBottom);
            if (!minPadding.Equals(Padding)) //add this check to ensure we don't end up in a recursive loop
                Padding = minPadding;
        }

    }
}

And, renderer can be implemented as:

[assembly: ExportRenderer(typeof(ExtendedGrid), typeof(ExtendedGridRenderer))]
namespace AppNamespace.iOS
{
    public class ExtendedGridRenderer : VisualElementRenderer<ExtendedGrid>
    {
        protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            //redraw border if any of these properties changed
            if (e.PropertyName == VisualElement.WidthProperty.PropertyName ||
                e.PropertyName == VisualElement.HeightProperty.PropertyName ||
                e.PropertyName == ExtendedGrid.BorderWidthProperty.PropertyName ||
                e.PropertyName == ExtendedGrid.BorderColorProperty.PropertyName)
                SetNeedsDisplay();
        }

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

            var box = Element;
            if (box == null)
                return;

            RemoveBorderLayers(); //remove previous layers - this can further be optimized.

            CGColor lineColor = box.BorderColor.ToCGColor();
            nfloat leftBorderWidth = new nfloat(box.BorderWidth.Left);
            nfloat rightBorderWidth = new nfloat(box.BorderWidth.Right);
            nfloat topBorderWidth = new nfloat(box.BorderWidth.Top);
            nfloat bottomBorderWidth = new nfloat(box.BorderWidth.Bottom);

            if(box.BorderWidth.Left > 0)
            {
                var leftBorderLayer = new BorderCALayer();
                leftBorderLayer.BackgroundColor = lineColor;
                leftBorderLayer.Frame = new CGRect(0, 0, leftBorderWidth, box.Height);
                InsertBorderLayer(leftBorderLayer);
            }

            if (box.BorderWidth.Right > 0)
            {
                var rightBorderLayer = new BorderCALayer();
                rightBorderLayer.BackgroundColor = lineColor;
                rightBorderLayer.Frame = new CGRect(box.Width - box.BorderWidth.Right, 0, rightBorderWidth, box.Height);
                InsertBorderLayer(rightBorderLayer);
            }

            if (box.BorderWidth.Top > 0)
            {
                var topBorderLayer = new BorderCALayer();
                topBorderLayer.BackgroundColor = lineColor;
                topBorderLayer.Frame = new CGRect(0, 0, box.Width, topBorderWidth);
                InsertBorderLayer(topBorderLayer);
            }

            if (box.BorderWidth.Bottom > 0)
            {
                var bottomBorderLayer = new BorderCALayer();
                bottomBorderLayer.BackgroundColor = lineColor;
                bottomBorderLayer.Frame = new CGRect(0, box.Height - box.BorderWidth.Bottom, box.Width, bottomBorderWidth);
                InsertBorderLayer(bottomBorderLayer);
            }
        }

        void RemoveBorderLayers()
        {
            if (NativeView.Layer.Sublayers?.Length > 0)
            {
                var layers = NativeView.Layer.Sublayers.OfType<BorderCALayer>();
                foreach(var layer in layers)
                    layer.RemoveFromSuperLayer();
            }
        }

        void InsertBorderLayer(BorderCALayer layer)
        {
            var index = (NativeView.Layer.Sublayers?.Length > 0) ? NativeView.Layer.Sublayers.Length - 1 : 0;
            //This is needed to get every background redrawn if the color changes on runtime
            NativeView.Layer.InsertSublayer(layer, index);
        }
    }

    public class BorderCALayer : CoreAnimation.CALayer { } //just create a type for easier replacement

}

Sample usage and output:

<Grid Margin="20">
    <Grid x:Name="phraseGrid" BackgroundColor="Transparent" 
            Margin="0,55,0,0" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
        <Grid.RowDefinitions>
            <RowDefinition Height="10*" />
            <RowDefinition Height="6*" />
            <RowDefinition Height="80*" />
            <RowDefinition Height="13*" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <local:ExtendedGrid x:Name="prGrid1" Grid.Row="0" Grid.Column="0" 
            Padding="5,0,0,0" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
            BackgroundColor="#EEEEEE"
            BorderColor="Gray"
            BorderWidth="0,2,0,2">
            <Label Text="only top and bottom set" Grid.Row="0" Grid.Column="0" />
        </local:ExtendedGrid>
        <local:ExtendedGrid x:Name="prGrid2" Grid.Row="1" Grid.Column="0" 
            Padding="5,0,0,0" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
            BackgroundColor="Gray"
            BorderColor="Blue"
            BorderWidth="2">
            <Label Text="all border set" Grid.Row="0" Grid.Column="0" />
        </local:ExtendedGrid>
        <local:ExtendedGrid x:Name="prGrid3" Grid.Row="2" Grid.Column="0" 
            HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
            BackgroundColor="Silver"
            BorderColor="Red"
            BorderWidth="0,2,0,2">
            <Label Text="no horizontal borders" Grid.Row="0" Grid.Column="0" />
        </local:ExtendedGrid>
    </Grid>
</Grid>

enter image description here


Option-2: Forms only approach

If you don't want to get into hassle of implementing renderers for each platform - you can also create a custom control BorderView as wrapper for rendering border at forms level itself (using a simple Padding, and BackgroundColor hack) and it should work on all platforms. The disadvantage is that it introduces an extra wrapper view for adding border, and child view can't have a transparent background.

BorderView implementation:

public class BorderView : ContentView
{
    /// <summary>
    /// The border color property.
    /// </summary>
    public static readonly BindableProperty BorderColorProperty =
        BindableProperty.Create(
            "BorderColor", typeof(Color), typeof(BorderView),
        defaultValue: Color.Black);

    /// <summary>
    /// Gets or sets the color of the border.
    /// </summary>
    /// <value>The color of the border.</value>
    public Color BorderColor
    {
        get { return (Color)GetValue(BorderColorProperty); }
        set { SetValue(BorderColorProperty, value); }
    }

    /// <summary>
    /// The border width property.
    /// </summary>
    public static readonly BindableProperty BorderWidthProperty =
        BindableProperty.Create(
        "BorderWidth", typeof(Thickness), typeof(BorderView),
        defaultValue: new Thickness(1));

    /// <summary>
    /// Gets or sets the width of the border.
    /// </summary>
    /// <value>The width of the border.</value>
    public Thickness BorderWidth
    {
        get { return (Thickness)GetValue(BorderWidthProperty); }
        set { SetValue(BorderWidthProperty, value); }
    }

    protected override void OnPropertyChanged(string propertyName = null)
    {
        base.OnPropertyChanged(propertyName);

        if (nameof(BorderColor).Equals(propertyName))
        {
            BackgroundColor = BorderColor;
        }

        if (nameof(BorderWidth).Equals(propertyName))
        {
            Padding = BorderWidth;
        }
    }
} 

And sample usage (output is same as above image):

        <local:BorderView Grid.Row="0" Grid.Column="0" BorderColor="Gray" BorderWidth="0,2,0,2">
            <Grid x:Name="prGrid1" 
                Padding="5,0,0,0" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
                BackgroundColor="#EEEEEE">
                <Label Text="only top and bottom set" Grid.Row="0" Grid.Column="0" />
            </Grid>
        </local:BorderView>

        <local:BorderView Grid.Row="1" Grid.Column="0"  BorderColor="Blue" BorderWidth="2">
            <Grid x:Name="prGrid2"
                Padding="5,0,0,0" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
                BackgroundColor="Gray">
                <Label Text="all border set" Grid.Row="0" Grid.Column="0" />
            </Grid>
        </local:BorderView>

        <local:BorderView Grid.Row="2" Grid.Column="0" BorderColor="Red" BorderWidth="0,2,0,2">
            <Grid x:Name="prGrid3" 
            HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
            BackgroundColor="Silver">
            <Label Text="no horizontal borders" Grid.Row="0" Grid.Column="0" />
        </Grid>
        </local:BorderView>

    </Grid>
</Grid>

Option-3: Platform effect approach

Another option is to create a custom PlatformEffect and a couple of attached bindable properties to implement border for any visual control.

Attached properties and effect (portable/shared code):

public class VisualElementBorderEffect : RoutingEffect
{
    public VisualElementBorderEffect() : base("MyCompany.VisualElementBorderEffect")
    {

    }
}

public static class BorderEffect
{
    public static readonly BindableProperty HasBorderProperty =
        BindableProperty.CreateAttached("HasBorder", typeof(bool), typeof(BorderEffect), false, propertyChanged: OnHasBorderChanged);
    public static readonly BindableProperty ColorProperty =
      BindableProperty.CreateAttached("Color", typeof(Color), typeof(BorderEffect), Color.Default);
    public static readonly BindableProperty WidthProperty =
      BindableProperty.CreateAttached("Width", typeof(Thickness), typeof(BorderEffect), new Thickness(0));

    public static bool GetHasBorder(BindableObject view)
    {
        return (bool)view.GetValue(HasBorderProperty);
    }

    public static void SetHasBorder(BindableObject view, bool value)
    {
        view.SetValue(HasBorderProperty, value);
    }

    public static Color GetColor(BindableObject view)
    {
        return (Color)view.GetValue(ColorProperty);
    }

    public static void SetColor(BindableObject view, Color value)
    {
        view.SetValue(ColorProperty, value);
    }

    public static Thickness GetWidth(BindableObject view)
    {
        return (Thickness)view.GetValue(WidthProperty);
    }

    public static void SetWidth(BindableObject view, Thickness value)
    {
        view.SetValue(WidthProperty, value);
    }

    static void OnHasBorderChanged(BindableObject bindable, object oldValue, object newValue)
    {
        var view = bindable as View;
        if (view == null)
        {
            return;
        }

        bool hasBorder = (bool)newValue;
        if (hasBorder)
        {
            view.Effects.Add(new VisualElementBorderEffect());
        }
        else
        {
            var toRemove = view.Effects.FirstOrDefault(e => e is VisualElementBorderEffect);
            if (toRemove != null)
            {
                view.Effects.Remove(toRemove);
            }
        }
    }
}

Platform effect for iOS:

[assembly: ResolutionGroupName("MyCompany")]
[assembly: ExportEffect(typeof(VisualElementBorderEffect), "VisualElementBorderEffect")]
namespace AppNamespace.iOS
{
    public class BorderCALayer : CoreAnimation.CALayer { } //just create a type for easier replacement

    public class VisualElementBorderEffect : PlatformEffect
    {
        protected override void OnAttached()
        {
            //no need to do anything here - we wait for size update to draw border
        }

        protected override void OnDetached()
        {
            RemoveBorderLayers();
        }

        void UpdateBorderLayers()
        {
            var box = Element as View;
            if (box == null)
                return;

            RemoveBorderLayers(); //remove previous layers - this can further be optimized.

            CGColor lineColor = BorderEffect.GetColor(Element).ToCGColor();
            var borderWidth = BorderEffect.GetWidth(Element);

            nfloat leftBorderWidth = new nfloat(borderWidth.Left);
            nfloat rightBorderWidth = new nfloat(borderWidth.Right);
            nfloat topBorderWidth = new nfloat(borderWidth.Top);
            nfloat bottomBorderWidth = new nfloat(borderWidth.Bottom);

            if (borderWidth.Left > 0)
            {
                var leftBorderLayer = new BorderCALayer();
                leftBorderLayer.BackgroundColor = lineColor;
                leftBorderLayer.Frame = new CGRect(0, 0, leftBorderWidth, box.Height);
                InsertBorderLayer(leftBorderLayer);
            }

            if (borderWidth.Right > 0)
            {
                var rightBorderLayer = new BorderCALayer();
                rightBorderLayer.BackgroundColor = lineColor;
                rightBorderLayer.Frame = new CGRect(box.Width - borderWidth.Right, 0, rightBorderWidth, box.Height);
                InsertBorderLayer(rightBorderLayer);
            }

            if (borderWidth.Top > 0)
            {
                var topBorderLayer = new BorderCALayer();
                topBorderLayer.BackgroundColor = lineColor;
                topBorderLayer.Frame = new CGRect(0, 0, box.Width, topBorderWidth);
                InsertBorderLayer(topBorderLayer);
            }

            if (borderWidth.Bottom > 0)
            {
                var bottomBorderLayer = new BorderCALayer();
                bottomBorderLayer.BackgroundColor = lineColor;
                bottomBorderLayer.Frame = new CGRect(0, box.Height - borderWidth.Bottom, box.Width, bottomBorderWidth);
                InsertBorderLayer(bottomBorderLayer);
            }
        }

        void RemoveBorderLayers()
        {
            if ((Control ?? Container).Layer.Sublayers?.Length > 0)
            {
                var layers = (Control ?? Container).Layer.Sublayers.OfType<BorderCALayer>();
                foreach (var layer in layers)
                    layer.RemoveFromSuperLayer();
            }
        }

        void InsertBorderLayer(BorderCALayer layer)
        {
            var native = (Control ?? Container);
            var index = (native.Layer.Sublayers?.Length > 0) ? native.Layer.Sublayers.Length - 1 : 0;
            //This is needed to get every background redrawn if the color changes on runtime
            native.Layer.InsertSublayer(layer, index);
        }

        protected override void OnElementPropertyChanged(System.ComponentModel.PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(e);

            //redraw border if any of these properties changed
            if (e.PropertyName == VisualElement.WidthProperty.PropertyName ||
                e.PropertyName == VisualElement.HeightProperty.PropertyName)
            {
                if(IsAttached && (Control != null || Container != null))
                {
                    RemoveBorderLayers();
                    UpdateBorderLayers();

                    (Control ?? Container).SetNeedsDisplay();
                }
            }   
        }


    }
}

And sample code and output:

<StackLayout Margin="20">
    <Grid x:Name="phraseGrid" BackgroundColor="Transparent"
            Margin="0,55,0,0">
        <Grid.RowDefinitions>
            <RowDefinition Height="10*" />
            <RowDefinition Height="6*" />
            <RowDefinition Height="80*" />
            <RowDefinition Height="13*" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

     <Grid x:Name="prGrid1" Grid.Row="0" Grid.Column="0" 
        Padding="5,0,0,0" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
        BackgroundColor="#EEEEEE"
        local:BorderEffect.HasBorder="true" 
        local:BorderEffect.Color="Gray" 
        local:BorderEffect.Width="0,2,0,2">
        <Label Text="grid with only top and bottom border set" Grid.Row="0" Grid.Column="0" />
    </Grid>
    <Grid x:Name="prGrid2" Grid.Row="1" Grid.Column="0" 
        Padding="5,0,0,0" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
        BackgroundColor="Gray"
        local:BorderEffect.HasBorder="true" 
        local:BorderEffect.Color="Blue" 
        local:BorderEffect.Width="2">
        <Label Text="grid with all border set" Grid.Row="0" Grid.Column="0" />
    </Grid>
    <Grid x:Name="prGrid3" Grid.Row="2" Grid.Column="0" 
        HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand"
        BackgroundColor="Silver"
        local:BorderEffect.HasBorder="true" 
        local:BorderEffect.Color="Red" 
        local:BorderEffect.Width="0,2,0,2">
        <Label Text="grid with no horizontal borders" Grid.Row="0" Grid.Column="0" />

        <Label local:BorderEffect.HasBorder="true" 
            local:BorderEffect.Color="Maroon" 
            local:BorderEffect.Width="0,2,0,2"
            Text="label with maroon border"
            HorizontalOptions="Center"
            VerticalOptions="Center" />
    </Grid>

    </Grid>
</StackLayout>

enter image description here

like image 106
Sharada Gururaj Avatar answered Sep 18 '22 01:09

Sharada Gururaj