zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    git config (21)
    狗狗染头,一举天下成名
    K2使用总结K2简介
    K2使用总结流程设计
    如何组建开发团队谈面试
    如何组建开发团队谈谈团队组成
    如何组建开发团队谈招聘中的牛人
    作业题有感
    js对象学习笔记Function类型和对象
    非常认同的《SEO优化大全》
  • 原文地址:https://www.cnblogs.com/cjw1115/p/6544544.html
Copyright © 2011-2022 走看看