Xamarin.Forms提供了一種跨平臺的開發手段,使我們用一套代碼就可以運行在不同的平臺上。但Xamarin.Forms所提供的基本控件比較有限,一些比較復雜的效果不好實現,這時我們也許會想,如果可以在Xamarin.Forms上使用原生的控件就好了。那么,Xamarin.Forms可以這么做嗎?答案當然是可以啦,難道我是吃飽了撐來寫這篇文章嗎?
這里我以歲寒輸入法開發中的一個需求為例:歲寒輸入法是支持主題功能的,我需要在主程序中實現對主題包效果的展示和切換的功能。在這里交代一下,iOS版的歲寒輸入法是由Xamarin.iOS和Xamarin.Forms聯合開發的,其中主程序的部分用的是Xamarin.Forms,鍵盤的部分是用Xamarin.iOS。輸入法的主題,自然也就是鍵盤的主題,因此主題包的顯示是與原生控件緊密關聯的。如果我不能在Xamarin.Forms上調用原生的鍵盤控件,那我就只能基于Xamarin.Forms把主題功能重做一遍,那就真是完犢子了。幸好Xamarin.Forms提供了ViewRenderer類,我可以用這個類在Xamarin.Forms中實現對原生控件的渲染。
第一步
我在Form項目中新建一個類,取名叫BoardView,是Xamarin.Forms.View的子類,并聲明了一個可綁定的Theme屬性。
代碼如下:
using System;
using Xamarin.Forms;
using System.Diagnostics;
namespace SuiHanIME {
public class BoardView : View {
public static readonly BindableProperty ThemeProperty = BindableProperty.Create(
"Theme",
typeof(IThemePath),
typeof(BoardView),
defaultBindingMode: BindingMode.TwoWay
);
public IThemePath Theme {
get {
return (IThemePath)GetValue(ThemeProperty);
}
set {
SetValue(ThemeProperty, value);
}
}
public BoardView() {
}
}
}
這里,IThemePath是我自己定義的用于查找主題文件的接口,其具體細節這里不做贅述。如何實現一個可綁定的屬性,可參閱我的另一篇文章Xamarin.From中的Data binding(數據綁定)(一)
注意,這里BoardView必須聲明為public,因為我們屆時要在iOS項目和Android項目中訪問這個類。
可以看到,BoardView中其實基本是空的,而這個時候我們已經可以在Form項目中使用它了,在代碼中使用也行,在XAML使用也行。
我用的是XAML;
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:SuiHanIME"
x:Class="SuiHanIME.SelfDefindUIPage">
......
<local:BoardView
Margin="25,0,25,0"
x:Name="board"
HeightRequest="200"
HorizontalOptions="FillAndExpand" />
......
</ContentPage>
xmlns:local="clr-namespace:SuiHanIME"的聲明是必須的,這樣IDE才能找到BoardView所在的位置。
不過這個時候,如果我們運行程序的話,會發現BoardView是空的。它這會兒當然是空的,因為我們還沒有將鍵盤通過ViewRenderer渲染到BoardView上。
第二步
我們要建立BoardView與鍵盤界面的關系,這里以iOS平臺為例。
我先在iOS項目下新建一個BoardViewRender,是ViewRenderer<TView,TNativeView>的子類,并重寫他的OnElementChanged方法。
代碼如下:
using Xamarin.Forms.Platform.iOS;
using UIKit;
using Xamarin.Forms;
using SuiHanIME.iOS;
using IOSLib;
using System.Diagnostics;
using Masonry;
[assembly: ExportRenderer(typeof(SuiHanIME.BoardView), typeof(BoardViewRender))]
namespace SuiHanIME.iOS {
public class BoardViewRender : ViewRenderer<SuiHanIME.BoardView, UIView> {
iOS_IME_CoreHandler handler = new iOS_IME_CoreHandler();
public BoardViewRender() {
}
protected override void OnElementChanged(ElementChangedEventArgs<BoardView> e) {
base.OnElementChanged(e);
if (this.Control == null) {
var holdView = handler.HoldView;
holdView.UserInteractionEnabled = false;
SetNativeControl(holdView);
handler.setTheme(Element.Theme);
}
}
......
}
iOS_IME_CoreHandler是我用于生成鍵盤的類,這里我們只需知道它的HoldView屬性會返回裝有鍵盤的UIView,setTheme方法用于設置新的主題。
我們現在回過頭再看代碼的開頭,我們需要在命名空間SuiHanIME.iOS之上寫上[assembly: ExportRenderer(typeof(SuiHanIME.BoardView), typeof(BoardViewRender))]
由于上述代碼在SuiHanIME.iOS空間之外引用了BoardViewRender,所以我們得using SuiHanIME.iOS;
或者使用[assembly: ExportRenderer(typeof(SuiHanIME.BoardView), typeof(SuiHanIME.iOS.BoardViewRender))]
ExportRenderer
聲明了BoardView類的渲染可由一個BoardViewRender類型的對象支持。
ViewRenderer<TView,TNativeView>是一個泛型類,兩個泛型參數,顧名思義的,TView是指Form項目下的控件的類型,TNativeView是指原生的控件的類型;
當然,他們要滿足一定的約束:
這也就是為什么BoardView得是Xamarin.Forms.View的子類。
這里,我對BoardViewRender的聲明為public class BoardViewRender : ViewRenderer<SuiHanIME.BoardView, UIView>
,其中BoardViewRender也必須聲明為public,因為在運行時,Xamarin.Forms也需要訪問這個類。這句話的意思就是:將SuiHanIME.BoardView作為一個UIView進行渲染。
接下來的事情是實現原生控件與Forms控件關聯的關鍵:在OnElementChanged方法中通過SetNativeControl方法設置原生控件。
protected override void OnElementChanged(ElementChangedEventArgs<BoardView> e) {
base.OnElementChanged(e);
if (this.Control == null) {
var holdView = handler.HoldView;
holdView.UserInteractionEnabled = false;
SetNativeControl(holdView);
handler.setTheme(Element.Theme);
}
}
在運行過程中,OnElementChanged被第一次調用時,this.Control必然為null,我們必須在這時通過SetNativeControl設置好原生控件。
檢查this.Control是否為null是必須的,這是為了避免設置原生控件。
handler.setTheme(Element.Theme);
是我初始化鍵盤主題的操作。
第三步
這會兒如果我們運行程序的話,就可以在Forms中看見原生控件的效果了,但,橋都馬代!我們還沒有設置與之交互的方法呢。
這里,我需要動態地改變鍵盤的主題,比如在Forms使用如下代碼切換主題:
board.Theme = themePath;
Theme就是我在最前面聲明的那個可綁定屬性。
為了將這個動作傳遞給原生控件,我需要重寫BoardViewRender的OnElementPropertyChanged:
protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) {
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName.Equals(BoardView.ThemeProperty.PropertyName)) {
handler.setTheme(Element.Theme);
}
}
可以想見,如果Theme被定義普通的CLR屬性的話,是不會觸發這個方法的。
最后
我們一起看一下效果:
這兩處使用的原生控件是完全一樣的,我也因此避免了用Forms重新做一次輪子的命運。
Android平臺上的實現與iOS基本上別無二致,但是由于我還沒有寫出代碼,這里就暫時不講。但是只要我把Android平臺上的鍵盤原生控件做好,剩下的事情就是編寫一個Android項目下的BoardViewRender類而已,Forms項目中的代碼不需要做絲毫的改動。