从可维护性和复杂性的角度来看,我建议您创建几个可绑定属性并使用它来呈现边框。
有三个选项可以实现这一点:
1.平台渲染器:使用属性扩展Grid,并在平台级别绘制边框。
2。表单控件:使用Padding 和BackgroundColor 赋予边框外观。
3。平台效果:创建一个PlatformEffect 来渲染边框(在这种情况下,我们定义附加的可绑定属性),并附加到任何视觉元素。
选项 1:平台渲染器方法
您可以扩展Grid 以创建自定义控件并实现其相应的渲染器。此代码示例说明了如何使用自定义控件方法来实现这一点。
自定义控件实现:
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;
}
}
}
并且,渲染器可以实现为:
[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
}
示例用法和输出:
<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>
选项 2:仅表单方法
如果您不想麻烦为每个平台实现渲染器 - 您还可以创建一个自定义控件 BorderView 作为在表单级别本身渲染边框的包装器(使用简单的 Padding 和 @987654335 @ hack),它应该适用于所有平台。缺点是它引入了一个额外的包装视图来添加边框,并且子视图不能有透明背景。
BorderView 实施:
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;
}
}
}
以及示例用法(输出与上图相同):
<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>
选项3:平台效应方法
另一个选项是创建一个自定义PlatformEffect 和几个附加的可绑定属性来实现任何可视控件的边框。
附加属性和效果(便携/共享代码):
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);
}
}
}
}
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();
}
}
}
}
}
以及示例代码和输出:
<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>