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

简介: 编码的一些规则从上面的讨论中,您可以为自己的Layout 衍生物制定几个规则:规则1:如果布局类定义了诸如间距或方向等属性,则这些属性应由可绑定属性支持。 在大多数情况下,这些可绑定属性的属性更改处理程序应调用InvalidateLayout。

编码的一些规则
从上面的讨论中,您可以为自己的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)
        {
        }
    }
}

每行中的列数取决于位图的大小,屏幕宽度以及每个与设备无关的单位的像素数:
2019_05_20_145210
将手机侧身转动,你会看到一些不同的东西:
2019_05_20_145302
ScrollView允许布局垂直滚动。 如果要检查WrapPanel的不同方向,则还需要更改ScrollView的方向:

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

现在屏幕水平滚动:
2019_05_20_145539
Image元素在后台加载位图,因此WrapLayout类将获得对其Layout方法的大量调用,因为每个Image元素都会根据加载的位图获得新的大小。 因此,在加载位图时,您可能会看到行和列的某些移位。

目录
相关文章
|
2月前
|
XML 编解码 Android开发
安卓开发中的自定义视图控件
【9月更文挑战第14天】在安卓开发中,自定义视图控件是一种高级技巧,它可以让开发者根据项目需求创建出独特的用户界面元素。本文将通过一个简单示例,引导你了解如何在安卓项目中实现自定义视图控件,包括创建自定义控件类、处理绘制逻辑以及响应用户交互。无论你是初学者还是有经验的开发者,这篇文章都会为你提供有价值的见解和技巧。
46 3
|
JavaScript Android开发
第二十六章:自定义布局(十)
不允许无约束的尺寸!有时您希望在屏幕上看到所有内容,可能是一系列大小统一的行和列。您可以使用带有星号定义的所有行和列定义的Grid执行类似的操作,以使它们具有相同的大小。唯一的问题是您可能还希望行数和列数基于子节点数,并针对屏幕空间的最佳使用进行了优化。
835 0
|
JavaScript Android开发
第二十六章:自定义布局(八)
失效假设您已在页面上组装了一些布局和视图,并且由于某种原因,代码隐藏文件(或者可能是触发器或行为)会更改Button的文本,或者可能只是字体大小或属性。 该更改可能会影响按钮的大小,这可能会对页面其余部分的布局更改产生连锁反应。
3421 0
|
JavaScript Android开发
第二十六章:自定义布局(七)
垂直和水平定位简化在VerticalStack中,LayoutChildren覆盖的末尾是一个switch语句,它有助于根据子级的HorizontalOptions属性设置水平定位每个子级。 这是整个方法: public class VerticalStack : Layout<View> { ...
872 0
|
JavaScript Android开发
第二十六章:自定义布局(六)
从Layout派生 我们现在拥有足够的知识来创建我们自己的布局类。布局中涉及的大多数公共和受保护方法都是由非泛型布局类定义的。 Layout 类派生自Layout,并将泛型类型约束为View及其派生类。
764 0
|
JavaScript Android开发
第二十六章:自定义布局(十二)
更多附加的可绑定属性附加的可绑定属性也可以在XAML中设置并使用Style设置。 为了了解它是如何工作的,让我们检查一个名为CartesianLayout的类,它模仿一个二维的,四象限的笛卡尔坐标系。
539 0
|
JavaScript Android开发 iOS开发
第二十六章:自定义布局(五)
内视过程中本章到目前为止提供的大部分信息都是从包含派生自各种元素(如StackLayout,ScrollView和Label)的类的测试程序汇编而来,覆盖虚拟方法(如GetSizeRequest,OnSizeRequest,OnSizeAllocated和LayoutChildren) ,并使用System.Diagnostics命名空间中的Debug.WriteLine方法在Visual Studio或Xamarin Studio的“输出”窗口中显示信息。
762 0
|
JavaScript Android开发
第二十六章:自定义布局(十一)
重叠的子项Layout 类可以在其子项上调用Layout方法,以便子项重叠吗?是的,但这可能会在你的脑海中提出另一个问题:什么决定孩子们的呈现顺序?哪些孩子看似坐在前台,可能部分或完全掩盖了背景中显示的其他孩子?在某些图形环境中,程序员可以访问名为Z-index的值。
643 0
|
Android开发
第二十六章:自定义布局(四)
无限约束现在这里有一些标记,起初看起来与前面的例子非常相似,但有很大的不同: <ContentPage __ Padding="20"> <StackLayout> <Label Text="Sample text" /> __ </StackLayout> </ContentPage> ContentPage仍然使用参数(0,0,360,640)进行初始布局调用,而LayoutChildren覆盖的参数是(20,20,320,600)。
703 0