如何在Xamarin的iOS网格的顶部和底部添加边框?

时间:2017-08-11 21:04:10

标签: xamarin xamarin.forms

我有这个XAML。我想要做的是使用iOS渲染器在网格的顶部和底部放置1px线。有人能告诉我有没有一种特殊的方法可以使用渲染器将边框线放在网格的顶部和底部?

<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 个答案:

答案 0 :(得分:10)

从可维护性和复杂性的角度来看,我建议您创建一些可绑定属性并使用它来渲染边框。

有三种方法可以实现这一点:

<强> 1。平台渲染器:使用属性扩展Grid并在平台级别绘制边框。

<强> 2。表单控件:使用PaddingBackgroundColor来显示边框。

第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>

enter image description here

选项-2:仅表单

如果您不想为每个平台实现渲染器的麻烦 - 您还可以创建一个自定义控件BorderView作为包装,用于在表单层自身渲染边框(使用简单的PaddingBackgroundColor 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>

enter image description here

答案 1 :(得分:0)

这是2行3列网格的示例。正如杰森建议在顶部和底部再添加2行,所以网格现在是4行,并将BoxView添加到第一行和最后一行

<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