Xamarin自定义布局系列——瀑布流布局

简介: 原文:Xamarin自定义布局系列——瀑布流布局Xamarin.Forms以Xamarin.Android和Xamarin.iOS等为基础,自己实现了一整套比较完整的UI框架,包含了绝大多数常用的控件,如下图 虽然XF(Xamarin.Forms简称XF,下同)为我们提供大这么多的控件,但在实际使用中,会发现这些控件的可定制性特别差,基本上都需要里利用Renderer来做一些修改。
原文: 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

目录
相关文章
|
Android开发 iOS开发 容器
Xamarin自定义布局系列——PivotPage(多页面切换控件)
原文:Xamarin自定义布局系列——PivotPage(多页面切换控件) PivotPage ———— 多页面切换控件 PivotPage是一个多页面切换控件,类似安卓中的ViewPager和UWP中的Pivot枢轴控件。
1167 0
|
Android开发 iOS开发 索引
Xamarin自定义布局系列——支持无限滚动的自动轮播视图CarouselView
原文:Xamarin自定义布局系列——支持无限滚动的自动轮播视图CarouselView 背景简述 自动轮播视图(CarouselView)在现在App中的地位不言而喻,绝大多数的App中都有类似的视图,无论是WebApp还是Native App。
1831 0
|
虚拟化 容器
Xamarin自定义布局系列——ListView的一个自定义实现ItemsControl(横向列表)
原文:Xamarin自定义布局系列——ListView的一个自定义实现ItemsControl(横向列表) 在以前写UWP程序的时候,了解到在ListView或者ListBox这类的列表空间中,有一个叫做ItemsPannel的属性,它是所有列表中子元素实际的容器,如果要让列表进行横向排列,只需要在...
1074 0
|
6月前
|
开发工具 Android开发 iOS开发
使用xamarin开发Android、iOS报错failed to open directory: 系统找不到指定的文件
使用vs2019学习xamarin时,创建新程序。使用模拟器真机等测试都报错如下图错误: ![请在此添加图片描述](https://developer-private-1258344699.cos.ap-guangzhou.myqcloud.com/column/article/5877188/20231030-de8ce5fd.png?x-cos-security-token=r4KyZDEowPT0kGTL0LqE8EnwfN1Nzexadb05dcffed3939ff8d7591c528c01706nvpGSE93QwHpZM8NwhJNTZctNRQa0l3KDhEnqj8P7d8t
65 0
使用xamarin开发Android、iOS报错failed to open directory: 系统找不到指定的文件
|
Java C# Android开发
.NET(WinCE、WM)开发转Android开发 ——Xamarin和Smobiler对比
WinCE从1995年诞生至今,已有20多年的发展历史,行业成熟方案覆盖范围广,从车载、工控、手持机都有涉及,且方案成熟。 近些年,Android以后来居上的态势,逐渐渗透至各行业领域,硬件手持大厂也把产品线重心向Android手持迁移,基于Android的行业解决方案越来越成熟,WinCE的开发人才流失,在WinCE解决方案上吃老本的企业寻求转型。
|
Web App开发 测试技术 Android开发
xamarin开发android收集的一些工具
原文:xamarin开发android收集的一些工具 xamarin开发android收集的一些工具 工欲善其事,必先利其器,从16年下半年开始做xamarin相关的开发,平时使用的一些工具和google插件给大家分享一下,都有下载地址,持续更新。
1487 0
|
前端开发 Android开发
Xamarin android中使用signalr实现即时通讯
前面几天也写了一些signalr的例子,不过都是在Web端,今天我就来实践一下如何在xamarin android中使用signalr,刚好工作中也用到了这个,也算是总结一下学到的东西吧,希望能帮助你们,更快地熟悉使用xamarin android进行即时通讯。
1389 0
|
XML Android开发 数据格式
Xamarin android spinner的使用方法
xamarin  android spinner的如何使用呢,大多数web开发人员经常会听到DropDownList 和Combobox 这种下拉选择框,spinner 的意思差不多,有道词典一下意思是“下拉列表”、“台湾斯普”,“下拉列表组件”,“微调控件”。
1519 0
|
XML Java Android开发
Xamarin android如何反编译apk文件
Xamarin android 如何反编译 apk文件 这里推荐一款XamarinAndroid开发的小游戏,撸棍英雄,游戏很简单,的确的是有点大。等一下我们来翻翻译这个Xamarin Android 开发的小游戏 下载链接:http://shouji.
1336 0
|
定位技术 开发工具 Android开发
Xamarin android如何调用百度地图入门示例(一)
在Xamarin android如何调用百度地图呢? 首先我们要区分清楚,百度地图这是一个广泛的概念,很多刚刚接触这个名词”百度地图api”,的确是泛泛而谈,我们来看一下百度地图的官网: android上使用百度地图的有Android地图SDK,定位SDK,导航SDK,全景SDK.
1421 0