Xamarin.Forms以Xamarin.Android和Xamarin.iOS等為基礎,自己實現了一整套比較完整的UI框架,包含了絕大多數常用的控件,如下圖
雖然XF(Xamarin.Forms簡稱XF,下同)為我們提供大這么多的控件,但在實際使用中,會發現這些控件的可定制性特別差,基本上都需要里利用Renderer來做一些修改。為了實現我們的需求,有兩種辦法:
- Renderer
- 自定義控件/布局
1.Renderer
XF中的所有控件,實際都是通過Renderer來實現的,利用Renderer,直接實例化相應的原生控件,每一個XF控件在各個平台都對應一個原生控件,具體可以查看這兒:RendererBase
利用Renderer,需要你了解原生控件的使用,所以引用一句話就是:
跨平台不代表不用學各個平台
筆者也是對安卓和iOS了解不多,正在摸索學習中
2.自定義控件/布局
這種相對來說比較簡單,卻比較繁瑣,並且最終效果不會太好,包括性能和UI兩方面。但是還是能適應一些常用場景。
關於布局基礎知識方面可以查看這位作者的一片文章:Xamarin.Forms自定義布局基礎
在使用中會發現XF的自定義布局和UWP的非常相似,常用的方法有兩個
public SizeRequest Measure(double widthConstraint, double heightConstraint, MeasureFlags flags = MeasureFlags.None); //計算元素大小
public void Layout(Rectangle bounds);//為元素實際布局,確定其位置和大小
Measure方法的兩個參數,表示父元素能為子元素提供的空間大小,返回值則表示子元素計算出自己實際需要的空間大小。
Layout方法的參數表示父元素給子元素提供的布局位置,包含XY坐標和大小四個參數。
現在考慮瀑布流布局的特點:
- 父元素大小確定,至少寬度和高度中有一個值確定(通常表現為整個頁面大小)
- 子元素排列表現為按行排列或者按列排列
-
按行排列時:子元素的高是一個定值,寬度跟具具體情況可變
-
按列排列時:子元素的寬是一個定值,高度跟具具體情況可變
瀑布流的常用場景
- 圖片展示
下面以的Demo展示一個按列布局的圖片展示瀑布流布局
主要有兩個方法
private double _maxHeight;
/// <summary>
/// 計算父元素需要的空間大小
/// </summary>
/// <param name="widthConstraint">可供布局的寬度</param>
/// <param name="heightConstraint">可供布局的高度</param>
/// <returns>實際需要的布局大小</returns>
protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
{
double[] colHeights = new double[Column];
double allColumnSpacing = ColumnSpacing * (Column - 1);
columnWidth = (widthConstraint - allColumnSpacing) / Column;
foreach (var item in this.Children)
{
var measuredSize = item.Measure(columnWidth, heightConstraint, MeasureFlags.IncludeMargins);
int col = 0;
for (int i = 1; i < Column; i++)
{
if (colHeights[i] < colHeights[col])
{
col = i;
}
}
colHeights[col] += measuredSize.Request.Height + RowSpacing;
}
_maxHeight = colHeights.OrderByDescending(m => m).First();
return new SizeRequest(new Size(widthConstraint, _maxHeight));
}
OnMeasured方法在布局開始前被調用,在這個方法中,我們遍歷所有的子元素,通過調用子元素的Measure方法,計算出所有子元素需要的布局大小,然后按列累加所有的高度,最后選取高度的最大值,這個最大值就是父元素的布局高度,在按列布局中,寬度是確定的。
protected override void LayoutChildren(double x, double y, double width, double height)
{
double[] colHeights = new double[Column];
double allColumnSpacing = ColumnSpacing * (Column - 1);
columnWidth = (width- allColumnSpacing )/ Column;
foreach (var item in this.Children)
{
var measuredSize=item.Measure(columnWidth, height, MeasureFlags.IncludeMargins);
int col = 0;
for (int i = 1; i < Column; i++)
{
if (colHeights[i] < colHeights[col])
{
col = i;
}
}
item.Layout(new Rectangle(col * (columnWidth + ColumnSpacing), colHeights[col], columnWidth, measuredSize.Request.Height));
colHeights[col] += measuredSize.Request.Height+RowSpacing;
}
}
LayoutChildren方法在OnMeasured方法后調用,通過調用子元素的Layou方法,用於對所有子元素布局。
至此,瀑布流和的新邏輯基本完成了,實際很簡單。接下來就是讓瀑布流支持數據綁定,實現動態添加刪除子元素。
為了支持數據綁定,實現一個依賴屬性ItemsSource,當ItemsSource被賦值或者值發生變化的時候,重新布局,根據ItemsSource的內容重新布局
public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create("ItemsSource", typeof(IList), typeof(FlowLayout), null,propertyChanged: ItemsSource_PropertyChanged);
public IList ItemsSource
{
get { return (IList)this.GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
private static void ItemsSource_PropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
var flowLayout = (FlowLayout)bindable;
var newItems = newValue as IList;
var oldItems = oldValue as IList;
var oldCollection = oldValue as INotifyCollectionChanged;
if (oldCollection != null)
{
oldCollection.CollectionChanged -= flowLayout.OnCollectionChanged;
}
if (newValue == null)
{
return;
}
if (newItems == null)
return;
if(oldItems == null||newItems.Count!= oldItems.Count)
{
flowLayout.Children.Clear();
for (int i = 0; i < newItems.Count; i++)
{
var child = flowLayout.ItemTemplate.CreateContent();
((BindableObject)child).BindingContext = newItems[i];
flowLayout.Children.Add((View)child);
}
}
var newCollection = newValue as INotifyCollectionChanged;
if (newCollection != null)
{
newCollection.CollectionChanged += flowLayout.OnCollectionChanged;
}
flowLayout.UpdateChildrenLayout();
flowLayout.InvalidateLayout();
}
protected override void OnBindingContextChanged()
{
base.OnBindingContextChanged();
}
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;
}
for (int i = 0; i < e.NewItems.Count; i++)
{
var child = this.ItemTemplate.CreateContent();
((BindableObject)child).BindingContext = e.NewItems[i];
this.Children.Add((View)child);
}
this.UpdateChildrenLayout();
this.InvalidateLayout();
}
}
ItemsSource_PropertyChanged方法在ItemsSource屬性被賦值的時候調用,在此方法中,根據自定義的DataTemplate,創建一個視圖(View),設置其數據綁定上下文為對應的Item,然后添加到瀑布流布局的Children中。
var child = this.ItemTemplate.CreateContent(); ((BindableObject)child).BindingContext = e.NewItems[i]; this.Children.Add((View)child);
注意到,在數據綁定中,更加常見的場景是:ItemsSource只賦值一次,以后ItemsSource中的值修改,直接能在布局中表現出來。
這就要求ItemsSource的數據源必須實現INotifyCollectionChanged這個接口,在.Net中,ObservableCollection
項目地址:Github