我正在构建一个 Xamarin Forms 应用程序,该应用程序将具有一个 ListView,其中包含一个具有以下布局的自定义单元格:
红色部分旨在作为状态指示器 - 而不是披露按钮。我很高兴我需要做些什么来将值绑定到它以改变颜色,但是我不确定实际绘制形状的最佳方法?我不想使用图像,我宁愿在代码中创建形状。
我正在构建一个 Xamarin Forms 应用程序,该应用程序将具有一个 ListView,其中包含一个具有以下布局的自定义单元格:
红色部分旨在作为状态指示器 - 而不是披露按钮。我很高兴我需要做些什么来将值绑定到它以改变颜色,但是我不确定实际绘制形状的最佳方法?我不想使用图像,我宁愿在代码中创建形状。
像这样的简单形状不应该保证使用 SkiaSharp 画布。您可以使用平台渲染器和本机绘图 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); }
}
}
安卓渲染器
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>