自訂 ViewCell
Xamarin.Forms ViewCell 是可新增至 ListView 或 TableView 的儲存格,其中包含開發人員定義的檢視。 本文示範如何為裝載於 ListView 控件內的 Xamarin.Forms ViewCell 建立自定義轉譯器。 這會停止 Xamarin.Forms 在 ListView 捲動期間重複呼叫版面配置計算。
每個 Xamarin.Forms 數據格都有每個平臺的隨附轉譯器,可建立原生控件的實例。 ViewCell
當應用程式轉Xamarin.Forms譯 時,會在iOS ViewCellRenderer
中具現化 類別,進而具現化原生UITableViewCell
控件。 在 Android 平台上,ViewCellRenderer
類別會具現化原生 View
控制項。 在通用 Windows 平台 (UWP) 上,ViewCellRenderer
類別會具現化原生的 DataTemplate
。 如需控件對應之轉譯器和原生控件類別 Xamarin.Forms 的詳細資訊,請參閱 轉譯器基類和原生控件。
下圖說明 ViewCell
和實作它之對應原生控制項間的關聯性:
您可在每個平台上建立 ViewCell
的自訂轉譯器,利用轉譯程序實作平台特定的自訂。 執行這項作業的程序如下:
- 建立Xamarin.Forms自定義儲存格。
- 從Xamarin.Forms取用自定義儲存格。
- 在每個平台上建立資料格的自訂轉譯器。
現在將討論每個專案,以實 NativeCell
作轉譯器,以利用控件內 Xamarin.FormsListView
裝載之每個數據格的平臺特定配置。 這樣會 Xamarin.Forms 停止在捲動期間 ListView
重複呼叫版面配置計算。
建立自訂資料格
您可以子類別化 ViewCell
類別,建立自訂的資料格控制項,如下列程式碼範例所示:
public class NativeCell : ViewCell
{
public static readonly BindableProperty NameProperty =
BindableProperty.Create ("Name", typeof(string), typeof(NativeCell), "");
public string Name {
get { return (string)GetValue (NameProperty); }
set { SetValue (NameProperty, value); }
}
public static readonly BindableProperty CategoryProperty =
BindableProperty.Create ("Category", typeof(string), typeof(NativeCell), "");
public string Category {
get { return (string)GetValue (CategoryProperty); }
set { SetValue (CategoryProperty, value); }
}
public static readonly BindableProperty ImageFilenameProperty =
BindableProperty.Create ("ImageFilename", typeof(string), typeof(NativeCell), "");
public string ImageFilename {
get { return (string)GetValue (ImageFilenameProperty); }
set { SetValue (ImageFilenameProperty, value); }
}
}
在 .NET Standard 程式庫專案中建立的 NativeCell
類別,會為自訂資料格定義 API。 自訂資料格會公開可透過資料繫結顯示的 Name
、Category
和 ImageFilename
屬性。 如需有關資料繫結的詳細資訊,請參閱資料繫結基本概念。
使用自訂的資料格
您可以宣告自訂資料格的位置命名空間並使用自訂資料格項目上的命名空間前置詞,在 .NET Standard 程式庫專案的 XAML 中參考 NativeCell
自訂資料格。 下列程式碼範例示範 XAML 頁面可如何使用 NativeCell
自訂資料格:
<ContentPage ...
xmlns:local="clr-namespace:CustomRenderer;assembly=CustomRenderer"
...>
...
<ContentPage.Content>
<StackLayout>
<Label Text="Xamarin.Forms native cell" HorizontalTextAlignment="Center" />
<ListView x:Name="listView" CachingStrategy="RecycleElement" ItemSelected="OnItemSelected">
<ListView.ItemTemplate>
<DataTemplate>
<local:NativeCell Name="{Binding Name}" Category="{Binding Category}" ImageFilename="{Binding ImageFilename}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage.Content>
</ContentPage>
local
命名空間前置詞沒有命名限制。 不過,clr-namespace
和 assembly
值必須符合自訂控制項的詳細資料。 一旦宣告命名空間,即會使用前置詞來參考自訂資料格。
下列程式碼範例示範 C# 頁面如何使用 NativeCell
自訂資料格:
public class NativeCellPageCS : ContentPage
{
ListView listView;
public NativeCellPageCS()
{
listView = new ListView(ListViewCachingStrategy.RecycleElement)
{
ItemsSource = DataSource.GetList(),
ItemTemplate = new DataTemplate(() =>
{
var nativeCell = new NativeCell();
nativeCell.SetBinding(NativeCell.NameProperty, "Name");
nativeCell.SetBinding(NativeCell.CategoryProperty, "Category");
nativeCell.SetBinding(NativeCell.ImageFilenameProperty, "ImageFilename");
return nativeCell;
})
};
switch (Device.RuntimePlatform)
{
case Device.iOS:
Padding = new Thickness(0, 20, 0, 0);
break;
case Device.Android:
case Device.UWP:
Padding = new Thickness(0);
break;
}
Content = new StackLayout
{
Children = {
new Label { Text = "Xamarin.Forms native cell", HorizontalTextAlignment = TextAlignment.Center },
listView
}
};
listView.ItemSelected += OnItemSelected;
}
...
}
Xamarin.FormsListView
控制項可用來顯示透過 ItemSource
屬性填入的數據清單。 RecycleElement
快取策略嘗試透過回收清單資料格,將 ListView
磁碟使用量和執行速度降至最低。 如需詳細資訊,請參閱快取資料。
清單中的每個資料列都包含三個資料項目 – 名稱、類別和影像檔案名稱。 清單中每個資料列的配置都是由 DataTemplate
所定義,它是透過 ListView.ItemTemplate
可繫結屬性所參考。 DataTemplate
將清單中的每個資料列定義為 NativeCell
,透過資料繫結顯示其 Name
、Category
和 ImageFilename
屬性。 如需 ListView
控制項的詳細資訊,請參閱 ListView。
自訂轉譯器現在可以新增至每個應用程式專案,為每個資料格自訂平台特定的配置。
在每個平台上建立自訂轉譯器
建立自訂轉譯器類別的流程如下:
- 建立轉譯自訂資料格之
ViewCellRenderer
類別的子類別。 - 覆寫轉譯自訂資料格的平台特定方法,並撰寫自訂方法的邏輯。
ExportRenderer
將屬性新增至自定義轉譯器類別,以指定將用來轉Xamarin.Forms譯自定義儲存格。 這個屬性是用來向 Xamarin.Forms註冊自定義轉譯器。
注意
對於大多數 Xamarin.Forms 元素,您可以選擇在每個平台專案中提供自定義轉譯器。 如果自訂轉譯器未註冊,則會使用控制項基底類別的預設轉譯器。 不過,轉譯 ViewCell 項目時,每個平台專案都必須要有自訂轉譯器。
下圖說明應用程式範例中每個專案的責任,以及這些專案之間的關聯性:
NativeCell
自訂資料格是由平台特定轉譯器類別轉譯,其全部衍生自各平台的 ViewCellRenderer
類別。 這會導致每個 NativeCell
自訂資料格都使用平台特定配置轉譯,如下列螢幕擷取畫面所示:
ViewCellRenderer
類別會公開平台轉譯自訂資料格的特定方法。 這在 iOS 平台為 GetCell
方法、在 Android 平台為 GetCellCore
方法,在 UWP 為 GetTemplate
方法。
每個自定義轉譯器類別都會以 ExportRenderer
向 註冊轉譯器 Xamarin.Forms的屬性裝飾。 屬性會採用兩個參數 : 要轉譯之儲存格的類型 Xamarin.Forms 名稱,以及自定義轉譯器的型別名稱。 屬性的 assembly
前置詞會指定套用至整個組件的屬性。
下列各節會討論每個平台特定自訂轉譯器類別的實作。
在 iOS 上建立自訂轉譯器
下列程式碼範例示範適用於 iOS 平台的自訂轉譯器:
[assembly: ExportRenderer(typeof(NativeCell), typeof(NativeiOSCellRenderer))]
namespace CustomRenderer.iOS
{
public class NativeiOSCellRenderer : ViewCellRenderer
{
NativeiOSCell cell;
public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, UITableView tv)
{
var nativeCell = (NativeCell)item;
cell = reusableCell as NativeiOSCell;
if (cell == null)
cell = new NativeiOSCell(item.GetType().FullName, nativeCell);
else
cell.NativeCell.PropertyChanged -= OnNativeCellPropertyChanged;
nativeCell.PropertyChanged += OnNativeCellPropertyChanged;
cell.UpdateCell(nativeCell);
return cell;
}
...
}
}
呼叫 GetCell
方法來建置要顯示的每個資料格。 每個資料格都是 NativeiOSCell
執行個體,其定義資料格的配置及其資料。 GetCell
方法的操作取決於 ListView
快取策略:
當
ListView
快取策略為RetainElement
時,每個資料格都需要叫用GetCell
方法。 畫面最初顯示的每個NativeCell
執行個體都需要建立NativeiOSCell
執行個體。 當使用者捲動ListView
時,會重複使用NativeiOSCell
執行個體。 如需 iOS 資料格重複使用的詳細資訊,請參閱資料格重複使用。注意
即使當
ListView
設為保留資料格時,此自訂轉譯器程式碼仍會執行一些資料格重複使用。UpdateCell
方法會使用每個NativeCell
執行個體的資料來更新按每個NativeiOSCell
執行個體顯示的資料,無論其為新建或為重複使用。注意
當
ListView
快取策略設為保留資料格時,絕不叫用OnNativeCellPropertyChanged
方法。當
ListView
快取策略為RecycleElement
時,畫面最初顯示的每個資料格都需要叫用GetCell
方法。 畫面最初顯示的每個NativeCell
執行個體都需要建立NativeiOSCell
執行個體。UpdateCell
方法會使用NativeCell
執行個體的資料來更新按每個NativeiOSCell
執行個體顯示的資料。 不過,當使用者捲動ListView
時,不會叫用GetCell
方法。 而是重複使用NativeiOSCell
執行個體。 當資料變更時,NativeCell
執行個體上會引發PropertyChanged
事件,而OnNativeCellPropertyChanged
事件處理常式會更新每個重複使用的NativeiOSCell
執行個體資料。
下列程式碼範例示範 PropertyChanged
事件引發時叫用的 OnNativeCellPropertyChanged
方法:
namespace CustomRenderer.iOS
{
public class NativeiOSCellRenderer : ViewCellRenderer
{
...
void OnNativeCellPropertyChanged(object sender, PropertyChangedEventArgs e)
{
var nativeCell = (NativeCell)sender;
if (e.PropertyName == NativeCell.NameProperty.PropertyName)
{
cell.HeadingLabel.Text = nativeCell.Name;
}
else if (e.PropertyName == NativeCell.CategoryProperty.PropertyName)
{
cell.SubheadingLabel.Text = nativeCell.Category;
}
else if (e.PropertyName == NativeCell.ImageFilenameProperty.PropertyName)
{
cell.CellImageView.Image = cell.GetImage(nativeCell.ImageFilename);
}
}
}
}
這個方法會更新按重複使用之 NativeiOSCell
執行個體顯示的資料。 因為此方法會呼叫多次,所以會檢查變更的屬性。
NativeiOSCell
類別定義每個資料格的配置,如下列程式碼範例所示:
internal class NativeiOSCell : UITableViewCell, INativeElementView
{
public UILabel HeadingLabel { get; set; }
public UILabel SubheadingLabel { get; set; }
public UIImageView CellImageView { get; set; }
public NativeCell NativeCell { get; private set; }
public Element Element => NativeCell;
public NativeiOSCell(string cellId, NativeCell cell) : base(UITableViewCellStyle.Default, cellId)
{
NativeCell = cell;
SelectionStyle = UITableViewCellSelectionStyle.Gray;
ContentView.BackgroundColor = UIColor.FromRGB(255, 255, 224);
CellImageView = new UIImageView();
HeadingLabel = new UILabel()
{
Font = UIFont.FromName("Cochin-BoldItalic", 22f),
TextColor = UIColor.FromRGB(127, 51, 0),
BackgroundColor = UIColor.Clear
};
SubheadingLabel = new UILabel()
{
Font = UIFont.FromName("AmericanTypewriter", 12f),
TextColor = UIColor.FromRGB(38, 127, 0),
TextAlignment = UITextAlignment.Center,
BackgroundColor = UIColor.Clear
};
ContentView.Add(HeadingLabel);
ContentView.Add(SubheadingLabel);
ContentView.Add(CellImageView);
}
public void UpdateCell(NativeCell cell)
{
HeadingLabel.Text = cell.Name;
SubheadingLabel.Text = cell.Category;
CellImageView.Image = GetImage(cell.ImageFilename);
}
public UIImage GetImage(string filename)
{
return (!string.IsNullOrWhiteSpace(filename)) ? UIImage.FromFile("Images/" + filename + ".jpg") : null;
}
public override void LayoutSubviews()
{
base.LayoutSubviews();
HeadingLabel.Frame = new CGRect(5, 4, ContentView.Bounds.Width - 63, 25);
SubheadingLabel.Frame = new CGRect(100, 18, 100, 20);
CellImageView.Frame = new CGRect(ContentView.Bounds.Width - 63, 5, 33, 33);
}
}
這個類別會定義用來轉譯資料格內容的控制項及其配置。 此類別會實作 INativeElementView
介面,當 ListView
使用 RecycleElement
快取策略時為必要。 此介面會指定類別必須實作 Element
屬性,這應會傳回回收資料格的自訂資料格資料。
NativeiOSCell
建構函式會初始化 HeadingLabel
、SubheadingLabel
和 CellImageView
屬性的外觀。 這些屬性用以顯示儲存在 NativeCell
執行個體的資料,使用正在呼叫的 UpdateCell
方法來設定每個屬性的值。 此外,當 ListView
使用 RecycleElement
快取策略時,您可在自訂轉譯器中使用 OnNativeCellPropertyChanged
方法來更新按 HeadingLabel
、SubheadingLabel
和 CellImageView
屬性顯示的資料。
資料格配置是由 LayoutSubviews
覆寫所執行,這會在資料格中設定 HeadingLabel
、SubheadingLabel
和 CellImageView
的座標。
在 Android 上建立自訂轉譯器
下列程式碼範例示範適用於 Android 平台的自訂轉譯器:
[assembly: ExportRenderer(typeof(NativeCell), typeof(NativeAndroidCellRenderer))]
namespace CustomRenderer.Droid
{
public class NativeAndroidCellRenderer : ViewCellRenderer
{
NativeAndroidCell cell;
protected override Android.Views.View GetCellCore(Cell item, Android.Views.View convertView, ViewGroup parent, Context context)
{
var nativeCell = (NativeCell)item;
Console.WriteLine("\t\t" + nativeCell.Name);
cell = convertView as NativeAndroidCell;
if (cell == null)
{
cell = new NativeAndroidCell(context, nativeCell);
}
else
{
cell.NativeCell.PropertyChanged -= OnNativeCellPropertyChanged;
}
nativeCell.PropertyChanged += OnNativeCellPropertyChanged;
cell.UpdateCell(nativeCell);
return cell;
}
...
}
}
呼叫 GetCellCore
方法來建置要顯示的每個資料格。 每個資料格都是 NativeAndroidCell
執行個體,其定義資料格的配置及其資料。 GetCellCore
方法的操作取決於 ListView
快取策略:
當
ListView
快取策略為RetainElement
時,每個資料格都需要叫用GetCellCore
方法。 畫面最初顯示的每個NativeCell
執行個體都需要建立NativeAndroidCell
。 當使用者捲動ListView
時,會重複使用NativeAndroidCell
執行個體。 如需 Android 資料格重複使用的詳細資訊,請參閱資料列檢視重複使用。注意
請注意,當
ListView
設為保留資料格時,此自訂轉譯器程式碼會執行一些資料格重複使用。UpdateCell
方法會使用每個NativeCell
執行個體的資料來更新按每個NativeAndroidCell
執行個體顯示的資料,無論其為新建或為重複使用。注意
請注意,雖然
ListView
設為保留資料格時會叫用OnNativeCellPropertyChanged
方法,但它不會更新NativeAndroidCell
屬性值。當
ListView
快取策略為RecycleElement
時,畫面最初顯示的每個資料格都需要叫用GetCellCore
方法。 畫面最初顯示的每個NativeCell
執行個體都需要建立NativeAndroidCell
執行個體。UpdateCell
方法會使用NativeCell
執行個體的資料來更新按每個NativeAndroidCell
執行個體顯示的資料。 不過,當使用者捲動ListView
時,不會叫用GetCellCore
方法。 而是重複使用NativeAndroidCell
執行個體。 當資料變更時,NativeCell
執行個體上會引發PropertyChanged
事件,而OnNativeCellPropertyChanged
事件處理常式會更新每個重複使用的NativeAndroidCell
執行個體資料。
下列程式碼範例示範 PropertyChanged
事件引發時叫用的 OnNativeCellPropertyChanged
方法:
namespace CustomRenderer.Droid
{
public class NativeAndroidCellRenderer : ViewCellRenderer
{
...
void OnNativeCellPropertyChanged(object sender, PropertyChangedEventArgs e)
{
var nativeCell = (NativeCell)sender;
if (e.PropertyName == NativeCell.NameProperty.PropertyName)
{
cell.HeadingTextView.Text = nativeCell.Name;
}
else if (e.PropertyName == NativeCell.CategoryProperty.PropertyName)
{
cell.SubheadingTextView.Text = nativeCell.Category;
}
else if (e.PropertyName == NativeCell.ImageFilenameProperty.PropertyName)
{
cell.SetImage(nativeCell.ImageFilename);
}
}
}
}
這個方法會更新按重複使用之 NativeAndroidCell
執行個體顯示的資料。 因為此方法會呼叫多次,所以會檢查變更的屬性。
NativeAndroidCell
類別定義每個資料格的配置,如下列程式碼範例所示:
internal class NativeAndroidCell : LinearLayout, INativeElementView
{
public TextView HeadingTextView { get; set; }
public TextView SubheadingTextView { get; set; }
public ImageView ImageView { get; set; }
public NativeCell NativeCell { get; private set; }
public Element Element => NativeCell;
public NativeAndroidCell(Context context, NativeCell cell) : base(context)
{
NativeCell = cell;
var view = (context as Activity).LayoutInflater.Inflate(Resource.Layout.NativeAndroidCell, null);
HeadingTextView = view.FindViewById<TextView>(Resource.Id.HeadingText);
SubheadingTextView = view.FindViewById<TextView>(Resource.Id.SubheadingText);
ImageView = view.FindViewById<ImageView>(Resource.Id.Image);
AddView(view);
}
public void UpdateCell(NativeCell cell)
{
HeadingTextView.Text = cell.Name;
SubheadingTextView.Text = cell.Category;
// Dispose of the old image
if (ImageView.Drawable != null)
{
using (var image = ImageView.Drawable as BitmapDrawable)
{
if (image != null)
{
if (image.Bitmap != null)
{
image.Bitmap.Dispose();
}
}
}
}
SetImage(cell.ImageFilename);
}
public void SetImage(string filename)
{
if (!string.IsNullOrWhiteSpace(filename))
{
// Display new image
Context.Resources.GetBitmapAsync(filename).ContinueWith((t) =>
{
var bitmap = t.Result;
if (bitmap != null)
{
ImageView.SetImageBitmap(bitmap);
bitmap.Dispose();
}
}, TaskScheduler.FromCurrentSynchronizationContext());
}
else
{
// Clear the image
ImageView.SetImageBitmap(null);
}
}
}
這個類別會定義用來轉譯資料格內容的控制項及其配置。 此類別會實作 INativeElementView
介面,當 ListView
使用 RecycleElement
快取策略時為必要。 此介面會指定類別必須實作 Element
屬性,這應會傳回回收資料格的自訂資料格資料。
NativeAndroidCell
建構函式會擴大 NativeAndroidCell
配置,並初始化擴大配置中的控制項 HeadingTextView
、SubheadingTextView
和 ImageView
屬性。 這些屬性用以顯示儲存在 NativeCell
執行個體的資料,使用正在呼叫的 UpdateCell
方法來設定每個屬性的值。 此外,當 ListView
使用 RecycleElement
快取策略時,您可在自訂轉譯器中使用 OnNativeCellPropertyChanged
方法來更新按 HeadingTextView
、SubheadingTextView
和 ImageView
屬性顯示的資料。
下列程式碼範例示範 NativeAndroidCell.axml
配置檔案的配置定義:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:background="@drawable/CustomSelector">
<LinearLayout
android:id="@+id/Text"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="10dip">
<TextView
android:id="@+id/HeadingText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#FF7F3300"
android:textSize="20dip"
android:textStyle="italic" />
<TextView
android:id="@+id/SubheadingText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14dip"
android:textColor="#FF267F00"
android:paddingLeft="100dip" />
</LinearLayout>
<ImageView
android:id="@+id/Image"
android:layout_width="48dp"
android:layout_height="48dp"
android:padding="5dp"
android:src="@drawable/icon"
android:layout_alignParentRight="true" />
</RelativeLayout>
此配置指定顯示資料格內容所用的兩個 TextView
控制項和一個 ImageView
控制項。 兩個 TextView
控制項在 LinearLayout
控制項內是垂直方向,而且所有控制項都包含在 RelativeLayout
內。
在 UWP 上建立自訂轉譯器
下列程式碼範例示範適用於 UWP 的自訂轉譯器:
[assembly: ExportRenderer(typeof(NativeCell), typeof(NativeUWPCellRenderer))]
namespace CustomRenderer.UWP
{
public class NativeUWPCellRenderer : ViewCellRenderer
{
public override Windows.UI.Xaml.DataTemplate GetTemplate(Cell cell)
{
return App.Current.Resources["ListViewItemTemplate"] as Windows.UI.Xaml.DataTemplate;
}
}
}
呼叫 GetTemplate
方法以傳回清單中每個資料列要轉譯的資料格。 它會為將顯示在畫面中的每個 NativeCell
執行個體建立 DataTemplate
,以 DataTemplate
定義資料格的外觀和內容。
DataTemplate
儲存在應用程式層級的資源字典中,如下列程式碼範例所示:
<DataTemplate x:Key="ListViewItemTemplate">
<Grid Background="LightYellow">
<Grid.Resources>
<local:ConcatImageExtensionConverter x:Name="ConcatImageExtensionConverter" />
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.40*" />
<ColumnDefinition Width="0.40*"/>
<ColumnDefinition Width="0.20*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.ColumnSpan="2" Foreground="#7F3300" FontStyle="Italic" FontSize="22" VerticalAlignment="Top" Text="{Binding Name}" />
<TextBlock Grid.RowSpan="2" Grid.Column="1" Foreground="#267F00" FontWeight="Bold" FontSize="12" VerticalAlignment="Bottom" Text="{Binding Category}" />
<Image Grid.RowSpan="2" Grid.Column="2" HorizontalAlignment="Left" VerticalAlignment="Center" Source="{Binding ImageFilename, Converter={StaticResource ConcatImageExtensionConverter}}" Width="50" Height="50" />
<Line Grid.Row="1" Grid.ColumnSpan="3" X1="0" X2="1" Margin="30,20,0,0" StrokeThickness="1" Stroke="LightGray" Stretch="Fill" VerticalAlignment="Bottom" />
</Grid>
</DataTemplate>
DataTemplate
指定顯示資料格內容及其配置和外觀所用的控制項。 透過資料繫結使用兩個 TextBlock
控制項和一個 Image
控制項來顯示資料格的內容。 此外,使用 ConcatImageExtensionConverter
的執行個體將 .jpg
副檔名串連到每個影像檔案名稱。 這可確保 Image
控制項在設定 Source
屬性後,可以載入及轉譯影像。
摘要
本文示範如何為 ViewCell
控件內裝載的 Xamarin.FormsListView
建立自定義轉譯器。 這樣會 Xamarin.Forms 停止在捲動期間 ListView
重複呼叫版面配置計算。