Monday, August 21, 2017

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

Leave a Comment

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> 

2 Answers

Answers 1

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

Answers 2

Here is example of 2 rows 3 colums grid. As Jason suggested add 2 more rows at top and bottom, so grid is 4 rows now and add BoxView to first and last row

<Grid   BackgroundColor="Green" ColumnSpacing="0" RowSpacing="0" Padding="0" Margin="0" VerticalOptions="Center" >     <Grid.RowDefinitions>         <RowDefinition  />         <RowDefinition  />         <RowDefinition Height="30"/>         <RowDefinition  />     </Grid.RowDefinitions>      <Grid.ColumnDefinitions>         <ColumnDefinition  />         <ColumnDefinition  />         <ColumnDefinition />     </Grid.ColumnDefinitions>      <BoxView Grid.Row="0" Grid.ColumnSpan="3" BackgroundColor="#CDCDCD" HeightRequest="5" VerticalOptions="End"/>     <BoxView Grid.Row="3" Grid.ColumnSpan="3" BackgroundColor="#CDCDCD" HeightRequest="5" VerticalOptions="Start"/>   2 more rows      </Grid> 

enter image description here

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment