渲染器和屬性(2)
現在,對于iOS,EllipseUIView類是存在的,可以使用EllipseUIView作為本機控件來編寫EllipseViewRenderer。 從結構上講,這個類幾乎與Windows渲染器相同:
using System.ComponentModel;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(Xamarin.FormsBook.Platform.EllipseView),
typeof(Xamarin.FormsBook.Platform.iOS.EllipseViewRenderer))]
namespace Xamarin.FormsBook.Platform.iOS
{
public class EllipseViewRenderer : ViewRenderer<EllipseView, EllipseUIView>
{
protected override void OnElementChanged(ElementChangedEventArgs<EllipseView> args)
{
base.OnElementChanged(args);
if (Control == null)
{
SetNativeControl(new EllipseUIView());
}
if (args.NewElement != null)
{
SetColor();
}
}
protected override void OnElementPropertyChanged(object sender,
PropertyChangedEventArgs args)
{
base.OnElementPropertyChanged(sender, args);
if (args.PropertyName == EllipseView.ColorProperty.PropertyName)
{
SetColor();
}
}
void SetColor()
{
if (Element.Color != Color.Default)
{
Control.SetColor(Element.Color.ToUIColor());
}
else
{
Control.SetColor(UIColor.Clear);
}
}
}
}
此渲染器和Windows版本之間唯一的真正差別是Control屬性設定為ColorUIView的執行個體,底部的SetColor方法的主體是不同的。 它現在調用ColorUIView中的SetColor方法。 這個SetColor方法也可以在名為ToUIColor的Xamarin.Forms.Platform.iOS庫中使用公共擴充方法
将Xamarin.Forms顔色轉換為iOS顔色。
您可能已經注意到,Windows渲染器和iOS渲染器都不必擔心調整大小。 您很快就會看到,EllipseView可以設定為各種大小,并且在Xamarin.Forms布局系統中計算的大小将變為本機控件的大小。
遺憾的是,這不是Android渲染器的情況。 Android渲染器需要一些大小調整邏輯。 與iOS一樣,Android也缺少呈現橢圓的本機控件。 是以,Xamarin.FormsBook.Platform.Android庫包含一個名為EllipseDrawableView的類,它從View派生并繪制一個橢圓:
using Android.Content;
using Android.Views;
using Android.Graphics.Drawables;
using Android.Graphics.Drawables.Shapes;
using Android.Graphics;
namespace Xamarin.FormsBook.Platform.Android
{
public class EllipseDrawableView : View
{
ShapeDrawable drawable;
public EllipseDrawableView(Context context) : base(context)
{
drawable = new ShapeDrawable(new OvalShape());
}
protected override void OnDraw(Canvas canvas)
{
base.OnDraw(canvas);
drawable.Draw(canvas);
}
public void SetColor(Xamarin.Forms.Color color)
{
drawable.Paint.SetARGB((int)(255 * color.A),
(int)(255 * color.R),
(int)(255 * color.G),
(int)(255 * color.B));
Invalidate();
}
public void SetSize(double width, double height)
{
float pixelsPerDip = Resources.DisplayMetrics.Density;
drawable.SetBounds(0, 0, (int)(width * pixelsPerDip),
(int)(height * pixelsPerDip));
Invalidate();
}
}
}
在結構上,這類似于為iOS定義的EllipseUIView類,除了構造函數為橢圓建立一個ShapeDrawable對象,并且OnDraw覆寫渲染它。
此類有兩種方法來設定此橢圓的屬性。 SetColor方法轉換Xamarin.Forms顔色以設定ShapeDrawable對象的Paint屬性,SetSize方法将裝置無關機關的大小轉換為像素,用于設定ShapeDrawable對象的邊界。 SetColor和SetSize都以對Invalidate的調用結束,以使繪圖表面無效并生成對OnDraw的另一個調用。
Android渲染器使用EllipseDrawableView類作為其本機對象:
using System.ComponentModel;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
[assembly: ExportRenderer(typeof(Xamarin.FormsBook.Platform.EllipseView),
typeof(Xamarin.FormsBook.Platform.Android.EllipseViewRenderer))]
namespace Xamarin.FormsBook.Platform.Android
{
public class EllipseViewRenderer : ViewRenderer<EllipseView, EllipseDrawableView>
{
double width, height;
protected override void OnElementChanged(ElementChangedEventArgs<EllipseView> args)
{
base.OnElementChanged(args);
if (Control == null)
{
SetNativeControl(new EllipseDrawableView(Context));
}
if (args.NewElement != null)
{
SetColor();
SetSize();
}
}
protected override void OnElementPropertyChanged(object sender,
PropertyChangedEventArgs args)
{
base.OnElementPropertyChanged(sender, args);
if (args.PropertyName == VisualElement.WidthProperty.PropertyName)
{
width = Element.Width;
SetSize();
}
else if (args.PropertyName == VisualElement.HeightProperty.PropertyName)
{
height = Element.Height;
SetSize();
}
else if (args.PropertyName == EllipseView.ColorProperty.PropertyName)
{
SetColor();
}
}
void SetColor()
{
Control.SetColor(Element.Color);
}
void SetSize()
{
Control.SetSize(width, height);
}
}
}
請注意,OnElementPropertyChanged方法需要檢查Width和Height屬性的更改并将它們儲存在字段中,以便可以将它們組合到SetSize調用EllipseDrawableView的單個Bounds設定中。
所有渲染器到位後,是時候看它是否有效。 EllipseDemo解決方案還包含指向Xamarin.FormsBook.Platform解決方案的各個項目的連結,EllipseDemo中的每個項目都包含對Xamarin.FormsBook.Platform中相應庫項目的引用。
EllipseDemo中的每個項目還包含對相應庫項目中的Toolkit.Init方法的調用。這并不總是必要的。但請記住,各種渲染器不會被任何項目中的任何代碼直接引用,并且某些優化可能導緻代碼在運作時無法使用。對Toolkit.Init的調用避免了這種情況。
EllipseDemo中的XAML檔案建立了幾個具有不同顔色和大小的EllipseView對象,其中一些受到大小限制,而其他對象則允許填充其容器:
<?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:platform=
"clr-namespace:Xamarin.FormsBook.Platform;assembly=Xamarin.FormsBook.Platform"
x:Class="EllipseDemo.EllipseDemoPage">
<Grid>
<platform:EllipseView Color="Aqua" />
<StackLayout>
<StackLayout.Padding>
<OnPlatform x:TypeArguments="Thickness"
iOS="0, 20, 0, 0" />
</StackLayout.Padding>
<platform:EllipseView Color="Red"
WidthRequest="40"
HeightRequest="80"
HorizontalOptions="Center" />
<platform:EllipseView Color="Green"
WidthRequest="160"
HeightRequest="80"
HorizontalOptions="Start" />
<platform:EllipseView Color="Blue"
WidthRequest="160"
HeightRequest="80"
HorizontalOptions="End" />
<platform:EllipseView Color="#80FF0000"
HorizontalOptions="Center" />
<ContentView Padding="50"
VerticalOptions="FillAndExpand">
<platform:EllipseView Color="Red"
BackgroundColor="#80FF0000" />
</ContentView>
</StackLayout>
</Grid>
</ContentPage>
請特别注意倒數第二個EllipseView,它給自己一個半透明的紅色。 對于填充頁面的大橢圓的Aqua,這應該呈現為中灰色。
最後一個EllipseView為自己提供半透明紅色的BackgroundColor設定。 同樣,這應該在大的Aqua橢圓上呈灰色,但在白色背景下呈淺紅色,在黑色背景下呈暗紅色。 他們來了:

一旦你有一個EllipseView,當然你會想要寫一個彈跳球程式。 BouncingBall解決方案還包含指向Xamarin.FormsBook.Platform解決方案中所有項目的連結,并且所有應用程式項目都引用了相應的庫項目。 BouncingBall PCL還引用了一個名為Vector2的結構的Xamarin.FormsBook.Toolkit庫,這是一個二維向量。
XAML檔案将EllipseView定位在頁面的中心:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:platform=
"clr-namespace:Xamarin.FormsBook.Platform;assembly=Xamarin.FormsBook.Platform"
x:Class="BouncingBall.BouncingBallPage">
<platform:EllipseView x:Name="ball"
WidthRequest="100"
HeightRequest="100"
HorizontalOptions="Center"
VerticalOptions="Center" />
</ContentPage>
代碼隐藏檔案啟動了兩個“永遠”運作的動畫。第一個動畫在構造函數中定義,并動畫彈跳球的Color屬性,每隔10秒通過彩虹的顔色。
第二個動畫在螢幕的四個“牆壁”上彈回球。 對于通過while循環的每個循環,代碼首先确定它将首先擊中哪個牆以及以與裝置無關的機關到該牆的距離。 朝向while循環結束的中心的新計算是球撞擊牆壁時的位置。 新的矢量計算基于現有矢量和垂直于其擊中的表面的矢量(稱為法向矢量)确定偏轉矢量:
public partial class BouncingBallPage : ContentPage
{
public BouncingBallPage()
{
InitializeComponent();
// Color animation: cycle through rainbow every 10 seconds.
new Animation(callback: v => ball.Color = Color.FromHsla(v, 1, 0.5),
start: 0,
end: 1
).Commit(owner: this,
name: "ColorAnimation",
length: 10000,
repeat: () => true);
BounceAnimationLoop();
}
async void BounceAnimationLoop()
{
// Wait until the dimensions are good.
while (Width == -1 && Height == -1)
{
await Task.Delay(100);
}
// Initialize points and vectors.
Point center = new Point();
Random rand = new Random();
Vector2 vector = new Vector2(rand.NextDouble(), rand.NextDouble());
vector = vector.Normalized;
Vector2[] walls = { new Vector2(1, 0), new Vector2(0, 1), // left, top
new Vector2(-1, 0), new Vector2(0, -1) }; // right, bottom
while (true)
{
// The locations of the four "walls" (taking ball size into account).
double right = Width / 2 - ball.Width / 2;
double left = -right;
double bottom = Height / 2 - ball.Height / 2;
double top = -bottom;
// Find the number of steps till a wall is hit.
double nX = Math.Abs(((vector.X > 0 ? right : left) - center.X) / vector.X);
double nY = Math.Abs(((vector.Y > 0 ? bottom : top) - center.Y) / vector.Y);
double n = Math.Min(nX, nY);
// Find the wall that's being hit.
Vector2 wall = walls[nX < nY ? (vector.X > 0 ? 2 : 0) : (vector.Y > 0 ? 3 : 1)];
// New center and vector after animation.
center += n * vector;
vector -= 2 * Vector2.DotProduct(vector, wall) * wall;
// Animate at 3 msec per unit.
await ball.TranslateTo(center.X, center.Y, (uint)(3 * n));
}
}
}
當然,靜态照片無法捕捉到動畫的激動人心的動作: