Как добавить границу в верхнюю и нижнюю часть сетки iOS в Xamarin?

У меня есть этот XAML. То, что я хотел бы сделать, это разместить 1px-линию в верхней и нижней частях сетки с помощью рендеринга iOS. Может ли кто-нибудь сказать мне, есть ли специальный способ поместить границу линии сверху и снизу сетки с помощью средства визуализации?

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

Ответ 1

С точки зрения ремонтопригодности и сложности я бы рекомендовал вам создать пару связующих свойств и использовать их для визуализации границ.

Для реализации этого параметра доступны три варианта:

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 и BackgroundColor 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 и пару прикрепленных свойств bindable для реализации границы для любого визуального контроля.

Прикрепленные свойства и эффект (переносимый/общий код):

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>

введите описание изображения здесь

Ответ 2

Вот пример сетки 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>

введите описание изображения здесь