渲染器和属性(1)
在 Xamarin.Forms 中,BoxView 专门用于绘制矩形色块,但如果您需要绘制圆形或更通用的椭圆,该怎么办呢?
这时,EllipseView 就是为您量身定制的解决方案。考虑到其通用性,本书将其收录在第 20 章“异步和文件 I/O”中介绍的 Xamarin.FormsBook.Platform 库里,方便您在多个项目中复用。
BoxView 自身定义了 Color 类型的 Color 属性。EllipseView 也采用了相同的设计——它无需额外定义宽度和高度,因为从 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 中的 Color 属性最终必须与渲染器中的原生对象关联起来才能生效。
除了属性定义外,EllipseView 还重写了 OnSizeRequest 方法,为椭圆设置了一个默认尺寸——与 BoxView 一样,都是 40。
我们先从 Windows 平台说起。实践证明,EllipseView 的 Windows 渲染器比 iOS 和 Android 的实现要简单得多。
您可能还记得,第 20 章创建的 Xamarin.FormsBook.Platform 解决方案专门提供了一套工具,让 Windows 各平台共享代码:Xamarin.FormsBook.Platform.UWP 库、Windows 库和 WinPhone 库,它们都引用了一个共享项目——Xamarin.FormsBook.Platform.WinRT。这个共享项目正是放置 EllipseViewRenderer 的地方。
在 Windows 平台上,EllipseView 可以由 Windows.UI.Xaml.Shapes 命名空间中的 Ellipse 来呈现——因为它满足从 Windows.UI.Xaml.FrameworkElement 派生的条件。
Ellipse 被指定为 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 属性是否为 null,如果是,则创建原生对象(这里是 Ellipse),然后调用 SetNativeControl。此后,Control 属性就指向这个 Ellipse 对象。
不过,这次的 OnElementChanged 还额外处理了 ElementChangedEventArgs 参数,需要特别说明一下:
每个渲染器实例(例如这里的 EllipseViewRenderer)都持有一个唯一的原生对象(一个 Ellipse)。但是,渲染基础架构允许将渲染器实例从某个 Xamarin.Forms 元素上剥离,再附加到另一个元素上。Xamarin.Forms 可能会为了重建元素或用另一个元素替换当前元素而执行此类操作。
这种变化通过调用 OnElementChanged 通知给渲染器。ElementChangedEventArgs 参数提供了两个属性:OldElement 和 NewElement,类型均为 EllipseView。大多数情况下,您无需担心从一个渲染器实例上附加和剥离不同 Xamarin.Forms 元素的问题。但有时,您可以借此机会清理或释放渲染器占用的资源。
在最简单也最常见的情况下,每个渲染器实例只会被调用一次 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 就是同一个对象。
总结一下,在整个渲染器类中,您可以依赖以下两个基本属性:
Element - Xamarin.Forms 元素,只要最近的 OnElementChanged 调用有非 null 的 NewElement,它就有效。
Control - 原生视图、控件或对象,调用 SetNativeView 后即有效。
如您所知,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 就将其设为 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 制作 EllipseView 渲染器,您需要一个能绘制椭圆的 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 方法,创建椭圆图形路径,然后在图形上下文中将其绘制出来。它使用的颜色存储在一个字段中,初始设为 UIColor.Clear(透明)。注意底部的 SetColor 方法,它为新颜色赋值并调用 SetNeedsDisplay——该方法会使绘图表面失效,从而触发对 Draw 的另一次调用。
另外,构造方法中将 UIView 的 BackgroundColor 也设为了 UIColor.Clear。如果不这样做,椭圆未覆盖到的区域就会显示黑色背景。
