自定义渲染器入门:从HelloRenderers开始
先来介绍一下HelloRenderers这个示例程序。它的核心目标非常明确——就是让你直观地了解,编写一个最简单的自定义渲染器究竟需要完成哪些步骤。在这个示例中,我们定义了一个名为HelloView的新视图,它继承自View,唯一的功能就是在屏幕上显示一段固定的文本字符串。
首先来看PCL(可移植类库)中的完整代码,整个HelloView.cs文件的内容极其简洁:
using Xamarin.Forms;
namespace HelloRenderers
{
public class HelloView : View
{
}
}
是的,仅仅只有一行类定义。不过有一个关键细节需要注意:这个类被声明为public。你可能会疑惑,它只用在PCL项目中,为什么必须公开?实际上,它必须对各个平台组件可见,否则后续的渲染器将无法找到它。
HelloRenderers的PCL项目甚至没有单独定义页面类,而是直接在App.cs中将一个HelloView对象居中显示:
namespace HelloRenderers
{
public class App : Application
{
public App()
{
MainPage = new ContentPage
{
Content = new HelloView
{
VerticalOptions = LayoutOptions.Center,
HorizontalOptions = LayoutOptions.Center
}
};
}
}
}
这段代码运行起来不会产生任何错误,但你将在屏幕上看到一片空白。原因很简单:此时的HelloView只是一个完全透明且没有任何绘制内容的视图,缺少相应的渲染器来告诉它应该以何种方式呈现。
关键点就在这里:当Xamarin.Forms应用启动时,它会通过.NET反射扫描所有程序集,寻找标注了ExportRenderer特性的位置。这个特性就像信标一样,告诉框架:“嘿,这里有一个自定义渲染器,可以为特定的Xamarin.Forms元素提供渲染支持”。
接下来看看iOS项目中的HelloViewRenderer.cs文件。请注意在using语句之后,紧接着就是ExportRenderer特性。由于它是程序集级别的特性,因此必须放在命名空间声明的外部。它的含义非常直接:“HelloView这个类,由HelloViewRenderer这个渲染器负责渲染”:
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
using UIKit;
using HelloRenderers;
using HelloRenderers.iOS;
[assembly: ExportRenderer(typeof(HelloView), typeof(HelloViewRenderer))]
namespace HelloRenderers.iOS
{
public class HelloViewRenderer : ViewRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs args)
{
base.OnElementChanged(args);
if (Control == null)
{
UILabel label = new UILabel
{
Text = "Hello from iOS!",
Font = UIFont.SystemFontOfSize(24)
};
SetNativeControl(label);
}
}
}
}
这个HelloViewRenderer类必须公开,它继承自泛型类ViewRenderer。两个泛型参数中,第一个TView对应Xamarin.Forms中的类(也就是HelloView),第二个TNativeView对应iOS平台的原生控件类型。在iOS上,显示文本的标准组件是UIKit命名空间下的UILabel,这里正是选用了它。泛型参数相当于在声明:“一个HelloView对象,实际上会被渲染成一个iOS的UILabel对象”。
渲染器的核心工作在于重写OnElementChanged方法。当HelloView对象被创建时,该方法就会被调用,它的职责就是创建那个真正用于显示内容的原生控件。
在OnElementChanged的实现中,首先检查Control属性是否为null。这个Control属性由ViewRenderer定义,类型就是泛型参数中的TNativeView,因此在iOS项目中它是UILabel类型。第一次调用时,Control肯定为null,此时需要创建一个新的UILabel对象,设置好文本和字体大小,然后通过SetNativeControl方法将其注册进去。此后,Control属性就指向这个已创建的UILabel。
文件顶部的using指令分为三组:Xamarin.Forms命名空间用于编写ExportRenderer特性;Xamarin.Forms.Platform.iOS用于引用ViewRenderer;UIKit则用于使用UILabel。最后还有HelloRenderers和HelloRenderers.iOS,仅仅是因为ExportRenderer特性中的两个类型引用需要它们。这种为了一个引用而专门引入一个命名空间的做法确实有些繁琐。不过有一个小技巧:你可以直接在特性中使用完全限定名,从而省去这两个using语句。
接下来看看Android版本。Android项目中的HelloViewRenderer.cs文件就采用了这种完全限定名的写法:
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using Android.Util;
using Android.Widget;
[assembly: ExportRenderer(typeof(HelloRenderers.HelloView), typeof(HelloRenderers.Droid.HelloViewRenderer))]
namespace HelloRenderers.Droid
{
public class HelloViewRenderer : ViewRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs args)
{
base.OnElementChanged(args);
if (Control == null)
{
SetNativeControl(new TextView(Context)
{
Text = "Hello from Android!"
});
Control.SetTextSize(ComplexUnitType.Sp, 24);
}
}
}
}
这个渲染器同样继承自Android版本的ViewRenderer。泛型参数表明,HelloView在Android上由TextView来呈现。同样,在第一次调用OnElementChanged时Control为null。这里直接创建TextView并调用SetNativeControl,一气呵成。注意TextView的构造函数需要一个Android的Context对象,该对象可以通过OnElementChanged方法的参数获取。
调用完SetNativeControl之后,Control属性就获得了实际的TextView对象。然后通过这个Control调用SetTextSize方法设置字号。在Android中,文本尺寸有多种缩放单位,ComplexUnitType.Sp代表“缩放像素”,这也是Xamarin.Forms处理Label字体大小时的兼容方式。
最后是UWP平台的实现:
using Xamarin.Forms.Platform.UWP;
using Windows.UI.Xaml.Controls;
[assembly: ExportRenderer (typeof(HelloRenderers.HelloView), typeof(HelloRenderers.UWP.HelloViewRenderer))]
namespace HelloRenderers.UWP
{
public class HelloViewRenderer : ViewRenderer
{
protected override void OnElementChanged(ElementChangedEventArgs args)
{
base.OnElementChanged(args);
if (Control == null)
{
SetNativeControl(new TextBlock
{
Text = "Hello from the UWP!",
FontSize = 24,
});
}
}
}
}
在所有Windows平台上,HelloView最终由Windows.UI.Xaml.Controls命名空间中的TextBlock来渲染。Windows Phone(WinPhone)和Windows标准项目中的写法大同小异,区别只在于命名空间以及显示的文本内容。

运行效果如上图所示。可以看到,通过HelloView对象上设置的HorizontalOptions和VerticalOptions属性,文本被完美居中在屏幕中央。但请注意,你无法在这个HelloView上直接设置HorizontalTextAlignment或VerticalTextAlignment,因为这些属性是Label类特有的,并非View基类提供的。如果希望将HelloView升级为一个功能完善的文本视图,下一步就需要开始向HelloView类中添加自定义属性了。下一篇内容,我们将深入探讨如何实现这一目标。
