自定义渲染器开发指南:核心技巧与实战案例
自定义渲染器:EllipseView 属性详解(上)
Xamarin.Forms 中的 BoxView 专用于绘制矩形色块,若要绘制圆形或更通用的椭圆,则需借助 EllipseView。为提升复用性,本书将 EllipseView 纳入第 20 章“异步与文件 I/O”介绍的 Xamarin.FormsBook.Platform 库中。
EllipseView 的设计思路与 BoxView 类似:继承 Color 属性,无需额外定义宽高,直接复用 VisualElement 的 WidthRequest 与 HeightRequest。
Xamarin.FormsBook.Platform 库中的 EllipseView 实现如下:
namespace Xamarin.FormsBook.Platform
{
public class EllipseView : View
{
public static readonly BindableProperty ColorProperty =
BindableProperty.Create("Color", typeof(Color), typeof(EllipseView), Color.Default);
public Color Color
{
set { SetValue(ColorProperty, value); }
get { return (Color)GetValue(ColorProperty); }
}
protected override SizeRequest OnSizeRequest(double widthConstraint, double heightConstraint)
{
return new SizeRequest(new Size(40, 40));
}
}
}
此处 Color 属性仅声明可绑定属性,未附加 PropertyChanged 处理程序。看似定义后无实际动作,但该属性最终需与渲染器中的原生对象绑定。此外,EllipseView 重写 OnSizeRequest,设置默认尺寸为 40×40,与 BoxView 保持一致。
先从 Windows 平台切入。EllipseView 的 Windows 渲染器比 iOS 和 Android 版本更简洁。第 20 章创建的 Xamarin.FormsBook.Platform 解决方案包含共享项目 Xamarin.FormsBook.Platform.WinRT,被 UWP、Windows 和 WinPhone 库引用,EllipseViewRenderer 即存放于此。
Windows 平台上,EllipseView 由 Windows.UI.Xaml.Shapes 命名空间的 Ellipse 类呈现——此类派生自 Windows.UI.Xaml.FrameworkElement,符合 ViewRenderer 第二个泛型参数的要求。由于该文件需在所有 Windows 平台共享,需通过预处理指令处理 ExportRendererAttribute 和 ViewRenderer 类的命名空间:
using System.ComponentModel;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Shapes;
#if WINDOWS_UWP
using Xamarin.Forms.Platform.UWP;
#else
using Xamarin.Forms.Platform.WinRT;
#endif
[assembly: ExportRenderer(typeof(Xamarin.FormsBook.Platform.EllipseView),
typeof(Xamarin.FormsBook.Platform.WinRT.EllipseViewRenderer))]
namespace Xamarin.FormsBook.Platform.WinRT
{
public class EllipseViewRenderer : ViewRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs args)
{
base.OnElementChanged(args);
if (Control == null)
{
SetNativeControl(new Ellipse());
}
if (args.NewElement != null)
{
SetColor();
}
}
__
}
}
OnElementChanged 重写方法中,首先检查 Control 属性是否为空,若为空则创建原生 Ellipse 对象并调用 SetNativeControl。此后,Control 属性即指向该 Ellipse。此外,代码段处理了 ElementChangedEventArgs 参数,需要说明其用途:
每个渲染器实例(如 EllipseViewRenderer)持有一个原生对象(Ellipse)。渲染基础架构允许将渲染器实例从一个 Xamarin.Forms 元素剥离并附加到另一个元素上,用于重建或替换元素。此类变更通过 OnElementChanged 传递到渲染器。ElementChangedEventArgs 提供 OldElement 和 NewElement 两个属性(均为 EllipseView)。多数情况下无需关注元素切换,但可利用此机会释放资源。
在典型场景中,每个渲染器实例仅被调用一次 OnElementChanged——对应于绑定的 Xamarin.Forms 视图。此时创建原生元素并调用 SetNativeControl 即可。之后,ViewRenderer 定义的 Control 属性即为原生 Ellipse 对象。
OnElementChanged 被调用时,Xamarin.Forms 对象(EllipseView)可能已创建并设置部分属性。即渲染器需显示元素时,元素可能已携带初始化值。但系统不保证这一点——后续 OnElementChanged 调用可能对应新的 EllipseView。
关键在事件参数的 NewElement 属性。若不为 null(通常如此),则此属性即为当前 Xamarin.Forms 元素,需将其属性同步至原生对象。这正是 SetColor 方法的用途,下文详述其实现。
ViewRenderer 还定义 Element 属性指向 Xamarin.Forms 元素(EllipseView)。若最近一次 OnElementChanged 调用包含非 null 的 NewElement,则 Element 与 NewElement 指向同一对象。
综上,在整个渲染器类中,可依赖以下两个基本属性:
Element —— 当前 Xamarin.Forms 元素,仅在最近 OnElementChanged 调用中 NewElement 非 null 时有效。
Control —— 原生视图、控件或对象,调用 SetNativeControl 后即生效。
Xamarin.Forms 元素的属性可动态变化。例如,EllipseView 的 Color 属性可通过动画驱动。若 Color 由可绑定属性支持,任何变更都会触发 PropertyChanged 事件。
此变更会通知渲染器。渲染器上附加的 Xamarin.Forms 元素中,任何可绑定属性变化都会导致 ViewRenderer 调用受保护的虚方法 OnElementPropertyChanged。本例中,EllipseView 内任何可绑定属性变化(包括 Color)都会触发该方法。渲染器需重写 OnElementPropertyChanged,并检查具体属性:
namespace Xamarin.FormsBook.Platform.WinRT
{
public class EllipseViewRenderer : ViewRenderer
{
__
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs args)
{
base.OnElementPropertyChanged(sender, args);
if (args.PropertyName == EllipseView.ColorProperty.PropertyName)
{
SetColor();
}
}
__
}
}
若 Color 属性变更,事件参数 PropertyName 为“Color”——即创建 EllipseView.ColorProperty 可绑定属性时指定的字符串。为避免拼写错误,OnElementPropertyChanged 通过可绑定属性获取实际字符串,然后将新 Color 值同步至原生对象(Windows Ellipse)。
SetColor 方法仅在 OnElementChanged 和 OnElementPropertyChanged 两处调用。切勿因认为 OnElementChanged 之前属性不会变化而跳过该调用。通常情况下,元素初始化属性后才调用 OnElementChanged。
SetColor 假设 Xamarin.Forms 元素和原生控件均已就绪:从 OnElementChanged 调用时,原生控件已创建,NewElement 不为 null,Control 和 Element 均有效。从 OnElementPropertyChanged 调用时,Element 同样有效(正是该元素刚修改了属性)。
因此,SetColor 可简洁地将颜色从 Element(Xamarin.Forms 元素)传递至 Control(原生对象)。为避免命名冲突,所有名为 Color 的结构均使用完全限定名称:
namespace Xamarin.FormsBook.Platform.WinRT
{
public class EllipseViewRenderer : ViewRenderer
{
void SetColor()
{
if (Element.Color == Xamarin.Forms.Color.Default)
{
Control.Fill = null;
}
else
{
Xamarin.Forms.Color color = Element.Color;
global::Windows.UI.Color winColor =
global::Windows.UI.Color.FromArgb((byte)(color.A * 255),
(byte)(color.R * 255),
(byte)(color.G * 255),
(byte)(color.B * 255));
Control.Fill = new SolidColorBrush(winColor);
}
}
}
}
Windows 的 Ellipse 对象拥有 Brush 类型的 Fill 属性,默认值为 null。若 EllipseView 的 Color 属性为 Color.Default,SetColor 则设置 Fill 为 null。否则,需将 Xamarin.Forms 的 Color 转换为 Windows 的 Color,传递给 SolidColorBrush 构造函数,再赋值给 Ellipse 的 Fill 属性。
Windows 版本至此完成。但为 EllipseView 创建 iOS 和 Android 渲染器时,会面临更多挑战。回顾 ViewRenderer 第二个泛型参数的约束:
- iOS:TNativeView 必须继承自 UIKit.UIView
- Android:TNativeView 必须继承自 Android.View.Views
- Windows:TNativeElement 必须继承自 Windows.UI.Xaml.FrameworkElement
针对 iOS,需要绘制椭圆的 UIView 派生类。标准库中并无现成类,需手动创建。这正是 iOS 渲染器制作的第一步。因此,Xamarin.FormsBook.Platform.iOS 库中定义了 EllipseUIView 类,继承自 UIView,专用于绘制椭圆:
using CoreGraphics;
using UIKit;
namespace Xamarin.FormsBook.Platform.iOS
{
public class EllipseUIView : UIView
{
UIColor color = UIColor.Clear;
public EllipseUIView()
{
BackgroundColor = UIColor.Clear;
}
public override void Draw(CGRect rect)
{
base.Draw(rect);
using (CGContext graphics = UIGraphics.GetCurrentContext())
{
// 根据矩形区域创建椭圆几何路径
CGPath path = new CGPath();
path.AddEllipseInRect(rect);
path.CloseSubpath();
// 将路径添加到图形上下文并绘制
color.SetFill();
graphics.AddPath(path);
graphics.DrawPath(CGPathDrawingMode.Fill);
}
}
public void SetColor(UIColor color)
{
this.color = color;
SetNeedsDisplay();
}
}
}
该类重写 Draw 方法,创建椭圆路径后通过图形上下文的 Fill 模式绘制。颜色存储于字段中,初始为 UIColor.Clear(透明)。底部的 SetColor 方法设置新颜色并调用 SetNeedsDisplay——该方法使绘图表面失效,从而触发 Draw 再次执行。此外,构造函数将 UIView 的 BackgroundColor 设为 UIColor.Clear,避免椭圆未覆盖区域显示黑色背景。
