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>
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>
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>
0 comments:
Post a Comment