游乐游手机版
首页/AI教程/文章详情

第27章自定义渲染器第三部分教程

时间:2026-06-16 15:54
EllipseView自定义视图通过Color属性设置椭圆颜色。Windows渲染器利用Ellipse原生控件,重写OnElementChanged创建控件并调用SetColor同步属性值。OnElementPropertyChanged响应属性变化,将Xamarin Forms颜色转换为Windows颜色,再赋值给Fill画笔,实现颜色动态更新。

渲染器和属性(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。如果不这样做,椭圆未覆盖到的区域就会显示黑色背景。

来源:https://developer.aliyun.com/article/704749
上一篇ESXi 6.7配置SSH密码登录与磁盘直通教程 下一篇Oracle Mybatis批量插入数据实现方法
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
企业组织级AI赋能具体实施方法
AI教程 · 2026-06-30

企业组织级AI赋能具体实施方法

前段时间收到一位读者的留言,希望聊聊企业级、组织级的AI赋能究竟该怎么落地。巧的是,前几天刚看到一份咨询调研机构的数据:对近一两年所有企业级AI赋能项目的统计显示,超过90%的甲方企业认为,AI赋能在核心业务价值链上没有发挥任何实质性作用。除了AI辅助办公、企业智能知识库这类边缘应用起到了一些辅助效

Scrapy与Redis分布式架构的日本电商多平台数据聚合系统
AI教程 · 2026-06-30

Scrapy与Redis分布式架构的日本电商多平台数据聚合系统

从事日本电商数据聚合工作时,最大的难点在于要同时应对雅虎拍卖、煤炉(Mercari)、乐天和亚马逊日本站等截然不同的平台。以往使用单机爬虫,经常出现运行中崩溃的情况——单点故障、带宽利用率不足、数据存储混乱,这三大痛点令人困扰。 本文分享一套基于Scrapy + Redis的分布式爬虫方案,专门解决

详细PuTTY 0.81安装教程 SSH远程连接与自定义路径设置
AI教程 · 2026-06-30

详细PuTTY 0.81安装教程 SSH远程连接与自定义路径设置

​ PuTTY(简称PT)是一款轻量级开源SSH Telnet客户端,凭借简洁高效的特性,多年来始终是系统管理员与开发者进行远程连接的首选利器。本教程将详细介绍PuTTY 0 81版本的完整安装过程,并指导您自定义安装路径,以便更灵活地管理SSH远程连接工具。 安装准备 首先需要说明的是,整个安装流

在线教育系统必备功能:直播课堂与题库考试架构
AI教程 · 2026-06-30

在线教育系统必备功能:直播课堂与题库考试架构

很多人一想到做在线教育系统,第一反应往往是先把直播间和课程播放器搭起来,觉得“能看课”就万事大吉了。真到落地那天才发现,系统能不能顺滑跑起来,关键全藏在那些细节里——课程怎么组织、学习进度怎么记、考试怎么处理、后台怎么管得住。前端看起来就几个页面,后端其实是一整条业务链路。不管你是要做在线教育APP

ZStack源码级AI诊断套件让故障排查秒出答案
AI教程 · 2026-06-30

ZStack源码级AI诊断套件让故障排查秒出答案

一次故障排查,到底要花多少时间? 运维人员处理私有云、虚拟化平台的问题,流程大致都是这样:先翻日志看现象,再去文档里找对应机制,然后搜社区有没有类似案例,最后综合判断给出答复。简单问题半小时,复杂问题可能要跨天——而这些时间里,大部分精力耗在了“找信息”而不是“做决策”上。 类似的问题,也许每天都在