具有状态的自定义视图单元格

Custom View Cell with Status

我正在构建一个 Xamarin Forms 应用程序,它将有一个 ListView,其中包含具有以下布局的自定义单元格:

红色部分旨在作为状态指示器 - 而不是披露按钮。我很高兴我需要做些什么来将值绑定到它来改变颜色,但是我不确定实际绘制形状的最佳方法是什么?我不想使用图像,我宁愿用代码创建形状。

像这样的简单形状不应该保证 SkiaSharp canvas。您可以使用平台渲染器和本机绘图 API 作为路径来绘制此形状。

表单控件

public class StatusIndicator : View
{
    public static readonly BindableProperty BorderWidthProperty =
        BindableProperty.Create(
            "BorderWidth", typeof(int), typeof(StatusIndicator),
            defaultValue: 2);

    public int BorderWidth
    {
        get { return (int)GetValue(BorderWidthProperty); }
        set { SetValue(BorderWidthProperty, value); }
    }

    public static readonly BindableProperty BorderColorProperty =
        BindableProperty.Create(
            "BorderColor", typeof(Color), typeof(StatusIndicator),
            defaultValue: Color.Black);

    public Color BorderColor
    {
        get { return (Color)GetValue(BorderColorProperty); }
        set { SetValue(BorderColorProperty, value); }
    }

    public static readonly BindableProperty FillColorProperty =
        BindableProperty.Create(
            "FillColor", typeof(Color), typeof(StatusIndicator),
            defaultValue: Color.Gray);

    public Color FillColor
    {
        get { return (Color)GetValue(FillColorProperty); }
        set { SetValue(FillColorProperty, value); }
    }
}

Android 渲染器

public class StatusIndicatorRenderer : ViewRenderer<StatusIndicator, AView>
{
    protected override void OnElementPropertyChanged(
        object sender,
        PropertyChangedEventArgs e)
    {
        base.OnElementPropertyChanged(sender, e);

        if (e.PropertyName == nameof(StatusIndicator.FillColor) 
            || e.PropertyName == nameof(StatusIndicator.BorderColor)
            || e.PropertyName == nameof(StatusIndicator.BorderWidth))
        Invalidate();
    }

    protected override void DispatchDraw(global::Android.Graphics.Canvas canvas)
    {
        var indicator = this.Element;
        if (indicator == null)
        {
            base.DispatchDraw(canvas);
            return;
        }

        var borderColor = indicator.BorderColor;
        var fillColor = indicator.FillColor;

        var widthInDp = (int)TypedValue.ApplyDimension(ComplexUnitType.Dip, indicator.BorderWidth, Context.Resources.DisplayMetrics);
        var clipBounds = canvas.ClipBounds;
        var bounds = new Rect(clipBounds.Left, clipBounds.Top, clipBounds.Right, clipBounds.Bottom);
        bounds.Inset(widthInDp / 2, widthInDp / 2);

        var midPoint = new float[] { (bounds.Right - bounds.Left) / 2, (bounds.Bottom - bounds.Top) / 2 };
        var points = new float[,] {
            { bounds.Left, bounds.Top },
            { midPoint[0], bounds.Top },
            { bounds.Right, midPoint[1] },
            { midPoint[0], bounds.Bottom },
            { bounds.Left, bounds.Bottom },
            { bounds.Left, bounds.Top },
        };

        var paint = new Paint();
        paint.AntiAlias = true;

        var path = new Path();
        SetPath(path, points);
        path.SetFillType(Path.FillType.EvenOdd);
        path.Close();

        paint.Color = fillColor.ToAndroid();
        paint.SetStyle(Paint.Style.Fill);
        canvas.DrawPath(path, paint);

        paint.StrokeWidth = widthInDp;
        paint.Color = borderColor.ToAndroid();
        paint.SetStyle(Paint.Style.Stroke);
        canvas.DrawPath(path, paint);

        base.DispatchDraw(canvas);
    }

    private static void SetPath(Path path, float[,] points)
    {
        path.MoveTo(points[0, 0], points[0, 1]);
        path.LineTo(points[1, 0], points[1, 1]);
        path.LineTo(points[2, 0], points[2, 1]);
        path.LineTo(points[3, 0], points[3, 1]);
        path.LineTo(points[4, 0], points[4, 1]);
        path.LineTo(points[5, 0], points[5, 1]);
    }
}

iOS 渲染器

public class StatusIndicatorRenderer
    : ViewRenderer<StatusIndicator, UIView>
{
    protected override void OnElementChanged(
        ElementChangedEventArgs<StatusIndicator> e)
    {
        base.OnElementChanged(e);
    }

    protected override void OnElementPropertyChanged(
        object sender,
        PropertyChangedEventArgs e)
    {
        base.OnElementPropertyChanged(sender, e);

        if (e.PropertyName == nameof(StatusIndicator.FillColor)
            || e.PropertyName == nameof(StatusIndicator.BorderColor)
            || e.PropertyName == nameof(StatusIndicator.BorderWidth))
            SetNeedsDisplay();
    }

    public override void Draw(CoreGraphics.CGRect rect)
    {
        base.Draw(rect);
        RemoveLayer<IndicatorLayer>();

        if (Element == null)
            return;

        var strokeColor = Element.BorderColor.ToCGColor();
        var fillColor = Element.FillColor.ToCGColor();

        var offset = Element.BorderWidth / 2;
        var bounds = new CoreGraphics.CGRect(rect.Left + offset, 
                                             rect.Top + offset, 
                                             rect.Width + Element.BorderWidth, 
                                             rect.Height + Element.BorderWidth);

        var midPoint = new CoreGraphics.CGPoint((bounds.Right - bounds.Left) / 2, (bounds.Bottom - bounds.Top) / 2);
        var points = new List<CoreGraphics.CGPoint>() {
            new CoreGraphics.CGPoint(bounds.Left, bounds.Top),
            new CoreGraphics.CGPoint(midPoint.X, bounds.Top),
            new CoreGraphics.CGPoint(bounds.Right, midPoint.Y),
            new CoreGraphics.CGPoint(midPoint.X, bounds.Bottom),
            new CoreGraphics.CGPoint(bounds.Left, bounds.Bottom),
            new CoreGraphics.CGPoint(bounds.Left, bounds.Top),
        };

        var path = new CoreGraphics.CGPath();
        path.AddLines(points.ToArray());
        path.CloseSubpath();

        var shapeLayer = new IndicatorLayer()
        {
            Path = path,
            StrokeColor = strokeColor,
            LineWidth = Element.BorderWidth,
            FillColor = fillColor,
        };

        ReplaceOrInsertLayer(shapeLayer);
    }

    void RemoveLayer<T>() where T : CAShapeLayer
    {
        var existingLayer = NativeView.Layer.Sublayers?.OfType<T>().FirstOrDefault();
        //This is needed to get every background redrawn if the color changes on runtime
        if (existingLayer != null)
        {
            existingLayer.RemoveFromSuperLayer();
        }
    }

    void ReplaceOrInsertLayer<T>(T layer) where T : CAShapeLayer
    {
        var existingLayer = NativeView.Layer.Sublayers?.OfType<T>().FirstOrDefault();
        //This is needed to get every background redrawn if the color changes on runtime
        if (existingLayer != null)
        {
            NativeView.Layer.ReplaceSublayer(existingLayer, layer);
        }
        else
        {
            NativeView.Layer.InsertSublayer(layer, 0);
        }
    }

    public class IndicatorLayer : CAShapeLayer { }
}

示例用法

<ViewCell ..>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition Width="50" />
        </Grid.ColumnDefinitions>
        <Label Text="Random Text" HorizontalOptions="Fill" />
        <local:StatusIndicator Grid.Column="1" FillColor="{Binding}" BorderColor="Black" />
    </Grid>
</ViewCell>