Xamarin で ItemsControl 風 AbsoluteLayout
Xamarin には ViewModel のリストを UI に表示するコントロールは ListView と TableView しか用意されてません
本家 XAML では Canvas 内に ItemsControl を配置して ViewModel の内容に応じて任意の絶対座標に UI を表示するといったことが簡単にできるのですが、ItemsControl に相当するコントロールがないわけです
というわけで下記のフォーラムで共有されたコードをもとに ItemsControl 風の AbsoluteLayout を作ってみました!
XForms needs an ItemsControl - Xamarin Forums
/// <summary> /// ItemsControl 風 AbsoluteLayout /// </summary> public class AbsoluteItemsControl<T> : AbsoluteLayout { /// <summary> /// コンストラクタ /// </summary> public AbsoluteItemsControl() { } #region ItemsSource /// <summary> /// ItemsSource BindableProperty /// </summary> public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create<AbsoluteItemsControl<T>, ObservableCollection<T>>( p => p.ItemsSource, new ObservableCollection<T>(), BindingMode.OneWay, null, OnItemsChanged); /// <summary> /// ItemsSource CLR プロパティ /// </summary> public ObservableCollection<T> ItemsSource { get { return (ObservableCollection<T>)this.GetValue(ItemsSourceProperty); } set { this.SetValue(ItemsSourceProperty, value); } } /// <summary> /// ItemsSource 変更イベントハンドラ /// </summary> /// <param name="bindable">BindableObject</param> /// <param name="oldValue">古い値</param> /// <param name="newValue">新しい値</param> private static void OnItemsChanged(BindableObject bindable, ObservableCollection<T> oldValue, ObservableCollection<T> newValue) { var control = bindable as AbsoluteItemsControl<T>; if (control == null) { return; } control.ItemsSource.CollectionChanged += control.OnCollectionChanged; control.Children.Clear(); foreach (var item in newValue) { var content = control.ItemTemplate.CreateContent(); View view; var cell = content as ViewCell; if (cell != null) { view = cell.View; } else { view = (View)content; } view.BindingContext = item; control.Children.Add(view); } control.UpdateChildrenLayout(); control.InvalidateLayout(); } #endregion //ItemsSource #region ItemTemplate /// <summary> /// ItemTemplate BindableProperty /// </summary> public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create<AbsoluteItemsControl<T>, DataTemplate>( p => p.ItemTemplate, default(DataTemplate)); /// <summary> /// ItemTemplate CLR プロパティ /// </summary> public DataTemplate ItemTemplate { get { return (DataTemplate)this.GetValue(ItemTemplateProperty); } set { this.SetValue(ItemTemplateProperty, value); } } #endregion //ItemTemplate /// <summary> /// Items の変更イベントハンドラ /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { if (e.OldItems != null) { this.Children.RemoveAt(e.OldStartingIndex); this.UpdateChildrenLayout(); this.InvalidateLayout(); } if (e.NewItems == null) { return; } foreach (T item in e.NewItems) { var content = this.ItemTemplate.CreateContent(); View view; var cell = content as ViewCell; if (cell != null) { view = cell.View; } else { view = (View)content; } view.BindingContext = item; this.Children.Insert(ItemsSource.IndexOf(item), view); } this.UpdateChildrenLayout(); this.InvalidateLayout(); } }
AbsoluteLayout を継承し、内部に ObservableColection を持たせて BindingContext に ViewModel をバインドして表示しているだけです
?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:prismmvvm="clr-namespace:Prism.Mvvm;assembly=Prism.Mvvm.Xamarin" xmlns:cv="clr-namespace:XamarinUnityInjection.Converters;assembly=XamarinUnityInjection" xmlns:vm="clr-namespace:XamarinUnityInjection.ViewModels;assembly=XamarinUnityInjection" prismmvvm:ViewModelLocator.AutoWireViewModel="true" x:Class="XamarinUnityInjection.Views.TopPage"> <ContentPage.Resources> <ResourceDictionary> <cv:RectangleConverter x:Key="RectangleConverter"/> </ResourceDictionary> </ContentPage.Resources> <local:AbsoluteItemsControl HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" x:TypeArguments="vm:TickItemViewModel" ItemsSource="{Binding TickItems}"> <local:AbsoluteItemsControl.ItemTemplate> <DataTemplate> <BoxView Color="#7F4234" AbsoluteLayout.LayoutBounds="{Binding LayoutBounds, Converter={StaticResource RectangleConverter}}" AbsoluteLayout.LayoutFlags="None" AnchorX="0.5" AnchorY="0.5" Rotation="{Binding Rotation}"/> </DataTemplate> </local:AbsoluteItemsControl.ItemTemplate> </local:AbsoluteItemsControl> </ContentPage>
XAML への記述例はこんな感じ
AbsoluteItemsControl は ItemsSource と DataTemplate を内包でき、AbsoluteLayout.LayoutBounds の添付プロパティなどで座標や範囲を指定できます
RectangleConverter はカンマ区切りの数字文字列を Rectangle に変換するコンバーターです
ジェネリックの型として TypeArguments に ViewModel を指定する必要がありますが、今後改善したいところ
上記の Page の ViewModel にはこんな感じに記述します
/// <summary> /// 最初の画面の ViewModel /// </summary> public class TopPageViewModel : BindableBase { ~ 中略 ~ /// <summary> /// コンストラクタ /// </summary> public TopPageViewModel() { this.tickItems.Clear(); for (var i = 0; i < 60; i++) { this.tickItems.Add(App.Container.Resolve<TickItemViewModel>()); } this.width = 320; this.height = 480; this.CenterX = this.width / 2; this.CenterY = this.height / 2; var radius = Math.Min(this.width, this.height); var index = 0; foreach (var tickItem in this.tickItems) { double size = 0.45 * radius / (index % 5 == 0 ? 15 : 30); double radians = index * 2 * Math.PI / this.tickItems.Count; tickItem.Left = this.centerX + 0.45 * radius * Math.Sin(radians) - size / 2; tickItem.Top = this.centerY - 0.45 * radius * Math.Cos(radians) - size / 2; tickItem.Width = size; tickItem.Height = size; tickItem.Rotation = 180 * radians / Math.PI; index++; } } }
むにゃむにゃと座標位置を計算して ItemViewModel に設定しているだけ
これを実行すると
円状に矩形が並びました!