Xamarin自定義佈局系列——瀑布流佈局
Xamarin.Forms以Xamarin.Android和Xamarin.iOS等為基礎,自己實現了一整套比較完整的UI框架,包含了絕大多數常用的控制元件,如下圖
雖然XF(Xamarin.Forms簡稱XF,下同)為我們提供大這麼多的控制元件,但在實際使用中,會發現這些控制元件的可定製性特別差,基本上都需要裡利用Renderer來做一些修改。為了實現我們的需求,有兩種辦法:
- Renderer
- 自定義控制元件/佈局
1.Renderer
XF中的所有控制元件,實際都是通過Renderer來實現的,利用Renderer,直接例項化相應的原生控制元件,每一個XF控制元件在各個平臺都對應一個原生控制元件,具體可以檢視這兒:
利用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