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 是已經封裝好的,實現了這個接口的一個開箱即用的集合類。所以在ItemsSource的值改變的時候,需要訂閱對數據源CollectionChanged事件,以便於在集合中元素添加或刪除的時候重新布局。

瀑布流布局

項目地址:Github


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM