Xamarin.Forms 自定义布局介绍

Xamarin.Forms中提供了StackLayout、AbsoluteLayout、RelativeLayout、Grid四个布局类,借助这四个类可以方便的管理程序的布局。当现有布局类不能满足我们的功能时,可以通过自定义布局实现想要的效果。 在讲解布局时提到过StackLayout等继承Layout<View>类,可以管理多个子视图。如果继承Layout类,只能管理单一子视图,如ContentView、Frame、ScrollView等。

布局概述

布局类是如何管理子视图的(确定子视图的大小和位置)?

先看一个简单示例,HorizontalOptions和VerticalOptions属性默认值为LayoutOptions.Fill。

显示效果,从红色外边框看出Frame大小为Page大小:

对Frame属性做如下修改:

对应显示效果

HorizontalOptions="Center" VerticalOptions="Center"属性用来确定子视图大小,Frame请求大小为内部Label大小,再通过指定WidthRequest="1000"明确Frame的宽度。从效果图看出布局分配给子视图的大小由子视图请求决定但又不能超出布局大小。

布局(父元素)如何才能知道子元素的大小?子元素应提供一个公共方法由父元素调用,这个方法为Layout,Layout方法定义在VisualElement中。

Layout方法接收一个Rectangle类型参数bounds。
查看Bounds属性定义

Bounds属性的Set操作声明为private,且通过Set内代码看出Bounds的值即元素的显示位置(X,Y)和大小(Width,Height)。

一个页面显示时首先会调用Page对象的Layout方法,Layout方法内在通过视图元素树(visual tree)依次调用子元素的Layout方法,这个过程称为布局的生命周期。旋转手机时会重新开始这个布局周期,这个布局周期也可以发生在元素树的一个子集内,如添加或移除子视图等操作都会调用父元素的Layout方法。

视图的X,Y,Width和Height等属性都是在布局的生命周期执行后被赋予一个有效值,视图的构造函数内无法获取有效值。X、Y的默认值为0,Width和Height的默认值为-1。

执行Layout方法会一次调用SetSize->SizeAllocated方法。SizeAllocated方法接收的width和htight参数为Bounds属性的Width和Heiht值,SizeAllocated方法内部又调用一个虚方法OnSizeAllocated。OnSizeAllocated执行完,此时视图的大小已经发生变化,同时出发SizeChanged事件。

OnSizeAllocated方法用virtual关键字声明,但是只允许Page和Layout两个类重写该方法进行特殊处理。

As an alternative to the SizeChanged event, it is possible for an application to override OnSizeAllocated in a ContentPage derivative to obtain the new size of the page. (If you do so, be sure to call the base class implementation of OnSizeAllocated.) You’ll find that OnSizeAllocated is sometimes called when the element’s size doesn’t actually change. The SizeChanged event is fired only when the size changes, and it’s better for size-specific handling on the application level.

Page类内OnSizeAllocated方法实现(Layout内相同),调用UpdateChildrenLayout:


查看UpdateChildrenLayout方法定义,UpdateChildrenLayout方法中调用LayoutChildren方法,并在方法结束前触发LayoutChanged事件。

LayoutChildren方法在Page和Layout中有了不同的处理,Page类中LayoutChildren方法定义为virtual方法,Layout类中LayoutChildren定义为abstract方法。

Page类中LayoutChildren的定义:

Layout类中LayoutChildren的定义:

LayoutChildren是一个很重要的方法。查看Page的LayoutChildren方法代码实现循环所有子元素进入LayoutChildIntoBoundingRegion方法。

LayoutChildIntoBoundingRegion是一个静态方法,方法内部判断当前元素是否为视图,传入元素不是视图(即布局)则调用子布局的Layout方法,此时再次回到Layout方法,从而完成布局生命周期的调用过程。当我们继承Layout<View>编写自定义布局时应实现该方法以管理子视图。

Layout传值到LayoutChildren时应考虑Padding属性值。如下布局所示,在360*640的设备上,Page的Layout方法接收到的X,Y,Width,Height为(0, 0, 360, 640),此时没有设置Padding 值传到LayoutChildren方法接收的值同样是(0, 0, 360, 640),LayoutChildren方法内调用ContentView的Layout方法,所以ContentView的Layout方法接收到Bound的X,Y,Width和Height值不变(0, 0, 360, 640)。在ContentView的Layout方法传值到LayoutChildren方法此时Padding值为15,所以Width和Height减少30(上下左右各减少15)并且X和Y的值增加15。LayoutChildren接收到的值为(15,15,330,610),LayoutChildren方法内调用Label的Layout,此时接收到的Bounds属性对应的X,Y,Width和Height分别为(15,15,330,610)。


在Page的LayoutChildIntoBoundingRegion方法中,传入child是一个View则调用子视图的Measure(过时方法为GetSizeRequest)方法计算子视图需要的大小,(此时child的Bounds属性或Width和Height属性还没有赋予有效值)。

调用子元素的Layout方法前会调用Measure方法,Layout方法的参数取决Measure返回的结果。

widthConstraint 和 heightConstraint表示宽高的约束值,表示父元素可以提供多少可用空间用来布局子元素。Measure方法内计算出所有子元素所需的大小与widthConstraint和heightConstraint比较,取较小值创建SizeRequest类型实例返回,SizeRequest包含Minimum和Request两个Size类型属性。Request表示元素需要的大小,Minimum表示元素的最小大小。

通常Minimum和Request两个属性赋予相同值,Xamarin.Forms中ListView和TabeView两个元素对应的Minimum和Request值不同,Minimum的值为(40,40)。然而Minimum的值并没什么用。

Measure内部调用已经过时的GetSizeRequest方法。

GetSizeRequest会调用OnMeasure方法,OnMeasure定义为虚方法。OnMeasure方法也是我们自定义布局时必须要重写的方法。


布局重绘

当元素的某些属性改变时会影响元素的大小,此时会引起元素的重绘。VisualElement定义一个InvalidateMeasure方法重绘元素,InvalidateMeasure是一个受保护的方法。自定义View时有某个BindableProperty类型属性可能改变元素大小,应该在该属性的propertyChanged内调用InvalidateMeasure方法。

AnchorX, AnchorY, Rotation, RotationX, RotationY, Scale, TranslationX, TranslationY等属性影响的是元素的渲染大小并不能改变元素的布局大小,值的改变不会触发MeasureInvalidated事件。Bounds, X, Y, Width,Height等属性在Layout方法内被调用,这几个属性改变时同样不会调用InvalidateMeasure方法。

InvalidateMeasure方法会触发MeasureInvalidated事件,以通知外部元素布局发生改变,通常是父元素处理MeasureInvalidated事件。

Layout类提供了一个类似的方法InvalidateLayout,当布局内属性发生改变影响子元素的大小和位置时应调用InvalidateLayout方法,如Layout<T>中Children属性的added和removed操作会调用InvalidateLayout方法。

如果想阻止增加或移除子元素时InvalidateLayout方法的调用可以重写ShouldInvalidateOnChildAdded和ShouldInvalidateOnChildRemoved方法,返回false即可。代码片段截图看出,还将OnChildMeasureInvalidated方法作为参数实例化EventHandler监听view的MeasureInvalidated事件,实际编码时可以重写OnChildMeasureInvalidated方法接收MeasureInvalidated触发的通知。

Layout<T>类定义了OnAdded和OnRemoved两个虚方法,分别在重写Element类的OnChildAdded和OnChildRemoved方法时调用,自定义布局时可以根据自己需要重写这两个方法。

Grid类重写OnAdded方法,添加子元素PropertyChanged监听示例(重写OnRemoved卸载监听,附加属性改变时调用InvalidateLayout方法,有兴趣可查看OnItemPropertyChanged内部实现):

当我们必须重新绘制布局时,该调用InvalidateLayout还是InvalidateMeasure?调用InvalidateLayout重新开始一个布局周期!!!InvalidateMeasure方法适合改变了布局的大小且不影响子布局排列时调用。

In most cases, the layout should call InvalidateLayout. This guarantees that the layout gets a call to its LayoutChildren method even if the layout is fully constrained in size. If the layout calls InvalidateMeasure, then a new layout pass will be generated only if the layout is not fully constrained in size. If the layout is constrained in size, then a call to InvalidateMeasure will do nothing.


自定义布局示例

通过继承Layout<T>实现瀑布流布局(只是简单示例)。如果想限制布局内子元素的类型可以T来指定,如Layout<Label>。但更多时候是继承Layout<View>。

重写LayoutChildren和OnMeasure方法,方法内循环所有子元素(忽略IsVisible属性为false的子元素)。LayoutChildren方法内调用Xamarin.Forms提供的LayoutChildIntoBoundingRegion(代替子元素的Layout 方法)方法简化编码,不必考虑LayoutOptions的值。

public class FlowLayout : Layout<View>
    {

        public static readonly BindableProperty ColumnSpacingProperty =
                BindableProperty.Create(
                    "ColumnSpacing",
                    typeof(double),
                    typeof(FlowLayout),
                    6.0,
                    propertyChanged: (bindable, oldvalue, newvalue) =>
                    {
                        ((FlowLayout)bindable).InvalidateLayout();
                    });

        public static readonly BindableProperty RowSpacingProperty =
                BindableProperty.Create(
                    "RowSpacing",
                    typeof(double),
                    typeof(FlowLayout),
                    6.0,
                    propertyChanged: (bindable, oldvalue, newvalue) =>
                    {
                        ((FlowLayout)bindable).InvalidateLayout();
                    });


        public double ColumnSpacing
        {
            set { SetValue(ColumnSpacingProperty, value); }
            get { return (double)GetValue(ColumnSpacingProperty); }
        }

        public double RowSpacing
        {
            set { SetValue(RowSpacingProperty, value); }
            get { return (double)GetValue(RowSpacingProperty); }
        }


        protected override void LayoutChildren(double x, double y, double width, double height)
        {
            double xChild = 0, yChild = 0;
            //循环子视图
            foreach (View child in Children)
            {
                if (!child.IsVisible)
                {
                    continue;
                }

                SizeRequest childSizeRequest = child.Measure(width, height);

                // Initialize child position and size.
                var childWidth = childSizeRequest.Request.Width;
                var childHeight = childSizeRequest.Request.Height;

                if (xChild + childWidth > width)
                {
                    xChild = 0;
                    yChild += childHeight + RowSpacing;
                }

                //判断HorizontalOptions和VerticalOptions的值计算子视图 x,y,width,height的值
                //switch (child.HorizontalOptions.Alignment)
                //{
                //  case LayoutAlignment.Start:
                //      break;
                //  case LayoutAlignment.Center:
                //      //xChild += (width - childWidth) / 2;
                //      break;
                //  case LayoutAlignment.End:
                //      //xChild += (width - childWidth);
                //      break;
                //  case LayoutAlignment.Fill:
                //      //childWidth = width;
                //      break;
                //}

                // Layout the child.
                LayoutChildIntoBoundingRegion(child, new Rectangle(xChild, yChild, width, childHeight));

                xChild = xChild + childWidth + ColumnSpacing;
            }
        }

        /// <summary>
        /// 计算出显示子元素所需大小 
        /// </summary>
        /// <returns>The measure.</returns>
        /// <param name="widthConstraint">Width constraint.</param>
        /// <param name="heightConstraint">Height constraint.</param>
        protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint)
        {
            var size = new Size();
            var width = 0.0;

            //循环子视图
            foreach (View child in Children)
            {
                if (!child.IsVisible)
                {
                    continue;
                }

                // Get the child's requested size.
                SizeRequest childSizeRequest = child.Measure(widthConstraint, Double.PositiveInfinity);
                // Initialize child position and size.
                var childWidth = childSizeRequest.Request.Width;
                var childHeight = childSizeRequest.Request.Height;

                if (width + childWidth >= widthConstraint)
                {
                    size.Height = size.Height + childHeight + RowSpacing;
                }
                else
                {
                    width = width + childWidth + ColumnSpacing;
                    size.Width = Math.Max(size.Width, width);
                }

            }
            //widthConstraint或heightConstraint的值可能为Double.PositiveInfinity,
            //但是OnMeasure不能返回Double.PositiveInfinity所以不能有如下代码
            //return new SizeRequest(new Size(widthConstraint, heightConstraint));

            return new SizeRequest(size);
        }

        #region 测试时编写的无关代码

        protected override bool ShouldInvalidateOnChildAdded(View child)
        {
            return false;
        }

        protected override bool ShouldInvalidateOnChildRemoved(View child)
        {
            return base.ShouldInvalidateOnChildRemoved(child);
        }

        protected override void OnAdded(View view)
        {
            base.OnAdded(view);
            //根据自己需要编码
        }

        protected override void OnRemoved(View view)
        {
            base.OnRemoved(view);
            //根据自己需要编码
        }


        protected override void OnChildMeasureInvalidated()
        {
            base.OnChildMeasureInvalidated();
        }

        #endregion

    }

XAML使用示例:

运行效果:

更多自定义布局介绍情查看《Creating Mobile Apps with Xamarin.Forms》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容