天天看點

第二十六章:自定義布局(九)

編碼的一些規則

從上面的讨論中,您可以為自己的Layout 衍生物制定幾個規則:

規則1:如果布局類定義了諸如間距或方向等屬性,則這些屬性應由可綁定屬性支援。 在大多數情況下,這些可綁定屬性的屬性更改處理程式應調用InvalidateLayout。 調用InvalidateMeasure應該僅限于屬性更改僅影響布局大小的情況,而不是它如何安排其子級,但現實生活中的例子很難想象。

規則2:您的布局類可以為其子級定義附加的可綁定屬性,類似于Grid定義的Row,Column,RowSpan和ColumnSpan屬性。 如您所知,這些屬性由布局類定義,但它們旨在設定在布局的子級上。 在這種情況下,您的布局類應覆寫OnAdded方法,以向布局的每個子項添加PropertyChanged處理程式,并重寫OnRemoved以删除該處理程式。 PropertyChanged處理程式應檢查子項上正在更改的屬性是否是您的類已定義的附加可綁定屬性之一,如果是,您的布局通常應通過調用InvalidateLayout來響應。

規則3:如果要實作緩存(或保留其他資訊)以盡量減少對布局子項的GetSizeRequest方法的重複處理,那麼您還應該覆寫InvalidateLayout方法,以便在添加或删除子項時通知 布局和OnChildMeasureInvalidated方法,當其中一個布局的子項更改大小時,将通知該方法。 在這兩種情況下,布局類都應通過清除緩存或丢棄保留的資訊來響應。

當布局調用其InvalidateMeasure方法時,布局也可以清除緩存或丢棄保留的資訊。 但是,通常緩存是基于傳遞給OnSizeRequest和LayoutChildren覆寫的大小的字典,是以這些大小無論如何都是不同的。

所有這些技術将在前面的頁面中進行示範。

具有屬性的布局

StackLayout當然很友善,但它隻是一行或一列孩子。如果您想要多個行和列,可以使用Grid,但應用程式必須顯式設定行數和列數,這需要很好地了解子項的大小。

容納無限數量的子節點的更有用的布局将開始将子節點排成一排,就像水準StackLayout一樣,但是如果需要則轉到第二行,并且到第三行,繼續,但是需要很多行。如果預計行數超過螢幕高度,則可以将布局設定為ScrollView的子級。

這就是WrapLayout背後的想法。它将其子項水準排列在螢幕上,直到它到達邊緣,此時它将後續子項的顯示包裝到下一行,依此類推。

但是讓它變得更加通用:讓我們給它一個像StackLayout這樣的Orientation屬性。這允許使用WrapLayout的程式指定它通過在螢幕上按行排列其子項開始,然後應該在必要時轉到第二列。使用此替代方向,WrapLayout可以水準滾動。

讓我們給WrapLayout兩個屬性,名為ColumnSpacing和RowSpacing,就像Grid一樣。

如果它真的允許各種不同大小的孩子,WrapLayout有可能在算法上相當複雜。第一行可能有四個子節點,第二行可能有三個子節點,依此類推。

根據子項的最大大小為每個子項配置設定相同的空間量。 這有時稱為單元格大小,WrapLayout将為每個孩子計算足夠大的單元格大小。 小于單元格大小的子項可以根據其HorizontalOptions和VerticalOptions設定放置在該單元格内。

WrapLayout足以證明它包含在Xamarin.FormsBook.Toolkit庫中。 以下枚舉包含兩個方向選項,其中包含冗長但明确的描述:

namespace Xamarin.FormsBook.Toolkit
{
    public enum WrapOrientation
    {
        HorizontalThenVertical,
        VerticalThenHorizontal
    }
}           

WrapLayout定義了三個由可綁定屬性支援的屬性。 每個可綁定屬性的屬性更改處理程式隻是調用InvalidateLayout來觸釋出局上的新布局傳遞:

namespace Xamarin.FormsBook.Toolkit
{
    public class WrapLayout : Layout<View>
    {
        __
        public static readonly BindableProperty OrientationProperty =
            BindableProperty.Create(
                "Orientation",
                typeof(WrapOrientation),
                typeof(WrapLayout),
                WrapOrientation.HorizontalThenVertical,
                propertyChanged: (bindable, oldValue, newValue) =>
                {
                    ((WrapLayout)bindable).InvalidateLayout();
                });
        public static readonly BindableProperty ColumnSpacingProperty =
            BindableProperty.Create(
                "ColumnSpacing",
                typeof(double),
                typeof(WrapLayout),
                6.0,
                propertyChanged: (bindable, oldvalue, newvalue) =>
                {
                        ((WrapLayout)bindable).InvalidateLayout();
                });
        public static readonly BindableProperty RowSpacingProperty =
            BindableProperty.Create(
                "RowSpacing",
                typeof(double),
                typeof(WrapLayout),
                6.0,
                propertyChanged: (bindable, oldvalue, newvalue) =>
                {
                    ((WrapLayout)bindable).InvalidateLayout();
                });
        public WrapOrientation Orientation
        {
            set { SetValue(OrientationProperty, value); }
            get { return (WrapOrientation)GetValue(OrientationProperty); }
        }
        public double ColumnSpacing
        {
            set { SetValue(ColumnSpacingProperty, value); }
            get { return (double)GetValue(ColumnSpacingProperty); }
        }
        public double RowSpacing
        {
            set { SetValue(RowSpacingProperty, value); }
            get { return (double)GetValue(RowSpacingProperty); }
        }
    __
    }
}           

WrapLayout還定義了一個私有結構,用于存儲有關特定子集合的資訊。 CellSize屬性是所有子項的最大大小,但已調整為布局的大小。 Rows和Cols屬性是行數和列數。

namespace Xamarin.FormsBook.Toolkit
{
    public class WrapLayout : Layout<View>
    {
        struct LayoutInfo
        {
            public LayoutInfo(int visibleChildCount, Size cellSize, int rows, int cols) : this()
            {
                VisibleChildCount = visibleChildCount;
                CellSize = cellSize;
                Rows = rows;
                Cols = cols;
            }
            public int VisibleChildCount { private set; get; }
            public Size CellSize { private set; get; }
            public int Rows { private set; get; }
            public int Cols { private set; get; }
        }
        Dictionary<Size, LayoutInfo> layoutInfoCache = new Dictionary<Size, LayoutInfo>();
        __
    }
}           

另請注意,Dictionary的定義用于存儲多個LayoutInfo值。 Size鍵是OnSizeRequest覆寫的限制參數,或LayoutChildren覆寫的width和height參數。

如果WrapLayout在受限制的ScrollView中(通常就是這種情況),那麼其中一個限制參數将是無限的,但對于LayoutChildren的width和height參數則不是這種情況。在這種情況下,将有兩個字典條目。

如果你然後側身轉動手機,WrapLayout将獲得另一個具有無限限制的OnSizeRequest調用,以及另一個LayoutChildren調用。那是另外兩個字典條目。但是,如果您将手機恢複為縱向模式,則無需進一步計算,因為緩存已經具有該情況。

下面是WrapLayout中的GetLayoutInfo方法,它根據特定大小計算LayoutInfo結構的屬性。請注意,該方法首先檢查計算的LayoutInfo值是否已在緩存中可用。在GetLayoutInfo方法的末尾,新的LayoutInfo值存儲在緩存中:

namespace Xamarin.FormsBook.Toolkit
{
    public class WrapLayout : Layout<View>
    {
        __
        LayoutInfo GetLayoutInfo(double width, double height)
        {
            Size size = new Size(width, height);
            // Check if cached information is available.
            if (layoutInfoCache.ContainsKey(size))
            {
                return layoutInfoCache[size];
            }
            int visibleChildCount = 0;
            Size maxChildSize = new Size();
            int rows = 0;
            int cols = 0;
            LayoutInfo layoutInfo = new LayoutInfo();
            // Enumerate through all the children.
            foreach (View child in Children)
            {
                // Skip invisible children.
                if (!child.IsVisible)
                    continue;

                // Count the visible children.
                visibleChildCount++;
                // Get the child's requested size.
                SizeRequest childSizeRequest = child.GetSizeRequest(Double.PositiveInfinity,
                Double.PositiveInfinity);
                // Accumulate the maximum child size.
                maxChildSize.Width =
                Math.Max(maxChildSize.Width, childSizeRequest.Request.Width);
                maxChildSize.Height =
                Math.Max(maxChildSize.Height, childSizeRequest.Request.Height);
            }
            if (visibleChildCount != 0)
            {
                // Calculate the number of rows and columns.
                if (Orientation == WrapOrientation.HorizontalThenVertical)
                {
                    if (Double.IsPositiveInfinity(width))
                    {
                        cols = visibleChildCount;
                        rows = 1;
                    }
                    else
                    {
                        cols = (int)((width + ColumnSpacing) /
                        (maxChildSize.Width + ColumnSpacing));
                        cols = Math.Max(1, cols);
                        rows = (visibleChildCount + cols - 1) / cols;
                    }
                }
                else // WrapOrientation.VerticalThenHorizontal
                {
                    if (Double.IsPositiveInfinity(height))
                    {
                        rows = visibleChildCount;
                        cols = 1;
                    }
                    else
                    {
                        rows = (int)((height + RowSpacing) /
                        (maxChildSize.Height + RowSpacing));
                        rows = Math.Max(1, rows);
                        cols = (visibleChildCount + rows - 1) / rows;
                    }
                }
                // Now maximize the cell size based on the layout size.
                Size cellSize = new Size();
                if (Double.IsPositiveInfinity(width))
                {
                    cellSize.Width = maxChildSize.Width;
                }
                else
                {
                    cellSize.Width = (width - ColumnSpacing * (cols - 1)) / cols;
                }
                if (Double.IsPositiveInfinity(height))
                {
                    cellSize.Height = maxChildSize.Height;
                }
                else
                {
                    cellSize.Height = (height - RowSpacing * (rows - 1)) / rows;
                }
                layoutInfo = new LayoutInfo(visibleChildCount, cellSize, rows, cols);
            }
            layoutInfoCache.Add(size, layoutInfo);
            return layoutInfo;
        }
        __
    }
}            

GetLayoutInfo的邏輯分為三個主要部分:

第一部分是一個foreach循環,它枚舉所有子節點,調用具有無限寬度和高度的GetSizeRequest,并确定最大子節點大小。

僅當存在至少一個可見子項時才執行第二和第三部分。第二部分基于Orientation屬性進行不同的處理,并計算行數和列數。通常情況下,具有預設Orientation設定(Horizo​​ntalThenVertical)的WrapPanel将是垂直ScrollView的子項,在這種情況下,OnSizeRequest覆寫的heightConstraint參數将是無限的。也可能是OnSizeRequest(和GetLayoutInfo)的widthConstraint參數也是無限的,這導緻所有子節點都顯示在一行中。但這不尋常。

然後,第三部分根據WrapLayout的尺寸計算子項的單元格大小。對于Horizo​​ntalThenVertical的方向,此單元格大小通常比最大子大小略寬,但如果WrapLayout對于最寬的孩子不夠寬或者對于最高的孩子而言足夠高則可能更小。

當布局接收到對InvalidateLayout的調用時(當将子集添加到集合或從集合中删除時,或者當WrapLayout的某個屬性更改值時)或OnChildMeasureInvalidated時,必須完全銷毀緩存。這隻是清除字典的問題:

namespace Xamarin.FormsBook.Toolkit
{
    public class WrapLayout : Layout<View>
    {
        __
        protected override void InvalidateLayout()
        {
            base.InvalidateLayout();
            // Discard all layout information for children added or removed.
            layoutInfoCache.Clear();
        }
        protected override void OnChildMeasureInvalidated()
        {
            base.OnChildMeasureInvalidated();
            // Discard all layout information for child size changed.
            layoutInfoCache.Clear();
        }
    }
}           

最後,我們準備檢視兩種必需的方法。 OnSizeRequest覆寫隻調用GetLayoutInfo并從傳回的資訊以及RowSpacing和ColumnSpacing屬性構造SizeRequest值:

namespace Xamarin.FormsBook.Toolkit
{
    public class WrapLayout : Layout<View>
    {
        __
        protected override SizeRequest OnSizeRequest(double widthConstraint,
        double heightConstraint)
        {
            LayoutInfo layoutInfo = GetLayoutInfo(widthConstraint, heightConstraint);
            if (layoutInfo.VisibleChildCount == 0)
            {
                return new SizeRequest();
            }
            Size totalSize = new Size(layoutInfo.CellSize.Width * layoutInfo.Cols +
            ColumnSpacing * (layoutInfo.Cols - 1),
            layoutInfo.CellSize.Height * layoutInfo.Rows +
            RowSpacing * (layoutInfo.Rows - 1));
            return new SizeRequest(totalSize);
        }
    __
    }
}            

LayoutChildren覆寫以對GetLayoutInfo的調用開始,然後枚舉所有要調整大小的子節點并将它們放置在每個子節點的單元格中。 此邏輯還需要基于Orientation屬性進行單獨處理:

namespace Xamarin.FormsBook.Toolkit
{
    public class WrapLayout : Layout<View>
    {
        __
        protected override void LayoutChildren(double x, double y, double width, double height)
        {
            LayoutInfo layoutInfo = GetLayoutInfo(width, height);
            if (layoutInfo.VisibleChildCount == 0)
                return;
            double xChild = x;
            double yChild = y;
            int row = 0;
            int col = 0;
            foreach (View child in Children)
            {
                if (!child.IsVisible)
                    continue;
                LayoutChildIntoBoundingRegion(child,
                new Rectangle(new Point(xChild, yChild), layoutInfo.CellSize));
                if (Orientation == WrapOrientation.HorizontalThenVertical)
                {
                    if (++col == layoutInfo.Cols)
                    {
                        col = 0;
                        row++;
                        xChild = x;
                        yChild += RowSpacing + layoutInfo.CellSize.Height;
                    }
                    else
                    {
                        xChild += ColumnSpacing + layoutInfo.CellSize.Width;
                    }
                }
                else // Orientation == WrapOrientation.VerticalThenHorizontal
                {
                    if (++row == layoutInfo.Rows)
                    {
                        col++;
                        row = 0;
                        xChild += ColumnSpacing + layoutInfo.CellSize.Width;
                        yChild = y;
                    }
                    else
                    {
                        yChild += RowSpacing + layoutInfo.CellSize.Height;
                    } 
                }
            }
        }
    __
    }
}           

我們來試試吧! PhotoWrap程式的XAML檔案隻包含一個在ScrollView中具有預設屬性設定的WrapPanel:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:toolkit=
                 "clr-namespace:Xamarin.FormsBook.Toolkit;assembly=Xamarin.FormsBook.Toolkit"
             x:Class="PhotoWrap.PhotoWrapPage">
    <ContentPage.Padding>
        <OnPlatform x:TypeArguments="Thickness"
                    iOS="0, 20, 0, 0" />
    </ContentPage.Padding>

    <ScrollView>
        <toolkit:WrapLayout x:Name="wrapLayout" />
    </ScrollView>
</ContentPage>           

代碼隐藏檔案通路JSON檔案,該檔案包含以前在本書的幾個示例程式中使用的庫存照片清單。 構造函數為清單中的每個位圖建立一個Image元素,并将其添加到WrapLayout:

public partial class PhotoWrapPage : ContentPage
{
    [DataContract]
    class ImageList
    {
        [DataMember(Name = "photos")]
        public List<string> Photos = null;
    }
    WebRequest request;
    static readonly int imageDimension = Device.OnPlatform(240, 240, 120);
    static readonly string urlSuffix =
        String.Format("?width={0}&height={0}&mode=max", imageDimension);
    public PhotoWrapPage()
    {
        InitializeComponent();
        // Get list of stock photos.
        Uri uri = new Uri("http://docs.xamarin.com/demo/stock.json");
        request = WebRequest.Create(uri);
        request.BeginGetResponse(WebRequestCallback, null);
    }
    void WebRequestCallback(IAsyncResult result)
    {
        try
        {
            Stream stream = request.EndGetResponse(result).GetResponseStream();
            // Deserialize the JSON into imageList.
            var jsonSerializer = new DataContractJsonSerializer(typeof(ImageList));
            ImageList imageList = (ImageList)jsonSerializer.ReadObject(stream);
            Device.BeginInvokeOnMainThread(() =>
         {
             foreach (string filepath in imageList.Photos)
             {
                 Image image = new Image
                 {
                     Source = ImageSource.FromUri(new Uri(filepath + urlSuffix))
                 };
                 wrapLayout.Children.Add(image);
             }
         });
        }
        catch (Exception)
        {
        }
    }
}           

每行中的列數取決于位圖的大小,螢幕寬度以及每個與裝置無關的機關的像素數:

第二十六章:自定義布局(九)

将手機側身轉動,你會看到一些不同的東西:

第二十六章:自定義布局(九)

ScrollView允許布局垂直滾動。 如果要檢查WrapPanel的不同方向,則還需要更改ScrollView的方向:

<ScrollView Orientation="Horizontal">
    <toolkit:WrapLayout x:Name="wrapLayout"
                        Orientation="VerticalThenHorizontal" />
</ScrollView>           

現在螢幕水準滾動:

第二十六章:自定義布局(九)

Image元素在背景加載位圖,是以WrapLayout類将獲得對其Layout方法的大量調用,因為每個Image元素都會根據加載的位圖獲得新的大小。 是以,在加載位圖時,您可能會看到行和列的某些移位。

繼續閱讀