1. 程式人生 > >Xamarin自定義佈局系列——瀑布流佈局

Xamarin自定義佈局系列——瀑布流佈局

原文: Xamarin自定義佈局系列——瀑布流佈局

Xamarin.Forms以Xamarin.Android和Xamarin.iOS等為基礎,自己實現了一整套比較完整的UI框架,包含了絕大多數常用的控制元件,如下圖
來源不詳,感謝作者

雖然XF(Xamarin.Forms簡稱XF,下同)為我們提供大這麼多的控制元件,但在實際使用中,會發現這些控制元件的可定製性特別差,基本上都需要裡利用Renderer來做一些修改。為了實現我們的需求,有兩種辦法:

  1. Renderer
  2. 自定義控制元件/佈局

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座標和大小四個引數。

現在考慮瀑布流佈局的特點:
  1. 父元素大小確定,至少寬度和高度中有一個值確定(通常表現為整個頁面大小)
  2. 子元素排列表現為按行排列或者按列排列
  • 按行排列時:子元素的高是一個定值,寬度跟具具體情況可變

  • 按列排列時:子元素的寬是一個定值,高度跟具具體情況可變

瀑布流的常用場景
  1. 圖片展示

下面以的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