Xamarin自定义布局系列——ListView的一个自定义实现ItemsControl(横向列表)

简介: 原文:Xamarin自定义布局系列——ListView的一个自定义实现ItemsControl(横向列表)在以前写UWP程序的时候,了解到在ListView或者ListBox这类的列表空间中,有一个叫做ItemsPannel的属性,它是所有列表中子元素实际的容器,如果要让列表进行横向排列,只需要在...
原文: Xamarin自定义布局系列——ListView的一个自定义实现ItemsControl(横向列表)

在以前写UWP程序的时候,了解到在ListView或者ListBox这类的列表空间中,有一个叫做ItemsPannel的属性,它是所有列表中子元素实际的容器,如果要让列表进行横向排列,只需要在Xaml中如下编辑即可

    //UWP中用XAML大致实现如下
    ···
    <ListView.ItemsPannel>
        <StackPannel Orientation="Horizental"/>
    </ListView.ItemsPannel>
    ···

这种让列表元素横向排列实际是一个很常见的场景,但是在Xamarin.Forms中,并没有提供直接的实现方法,如果想要这种效果,有两种解决办法

  • Renderer:利用Renderer在各平台实现,适用于对性能有较高要求的场景,比如大量数据展示
  • 自定义布局:实现比较简单,但是适用于数据量比较小的场景
    实际在使用的时候,利用自定义布局会比较简单,并且横向的列表展示并不适合大量数据的场景。

怎么实现呢?

Xamarin.Forms的列表控件是直接利用Renderer实现的,没有提供类似ItemsPannel之类的属性,所以考虑直接自己实现一个列表控件。有以下几个点:

  • 列表控件要支持滚动:所以在控件最外层需要一个ScrollView
  • 实现类似ItemsPannel的效果:所以需要实现一个ItemsPannel属性,类型是StackLayout,并且它应该是ScrollView的Content
  • ItemsControl控件的基类型是View,便于使用,直接让它继承自ContentView,这样就可以直接设置其Content为ScrollView

至此,先来给出这部分的代码,我们直接在构造函数中完成绝大多数操作

    ···
    private ScrollView _scrollView;
    private StackLayout itemsPanel = null;
    public StackLayout ItemsPanel
    {
        get { return this.itemsPanel; }
        set { this.itemsPanel = value; }
    }
    public ItemsControl()
    {
        this._scrollView = new ScrollView();
        this._scrollView.Orientation = Orientation;

        this.itemsPanel = new StackLayout() { Orientation = StackOrientation.Horizontal };//子元素水平排布的关键

        this.Content = this._scrollView;
        this._scrollView.Content = this.itemsPanel;
    }
    ···

子元素的容器是ItemsPannel,它实际是一个水平排布的StackLayout。想要在列表控件添加子元素,实际就是对该StackLayout的Children添加子元素。

考虑到列表控件中子元素的添加,就必须实现一个属性ItemsSource,是集合类型,并且为了支持数据绑定等,还需要让他是一个依赖属性,针对ItemsSource属性值自身的改变或者其集合中元素的添加删除等,都需要监听,并且将具体变化表现在ItemsControl中。实现该属性如下:

    ···
    public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create("ItemsSource", typeof(IEnumerable), typeof(ItemsControl), defaultBindingMode: BindingMode.OneWay, defaultValue: null, propertyChanged: OnItemsSourceChanged);
    
    public IEnumerable ItemsSource
    {
        get { return (IEnumerable)this.GetValue(ItemsSourceProperty); }
        set { this.SetValue(ItemsSourceProperty, value); }
    }
    ···
    Static vid OnItemsSourceChanged(BindableObject sender,object oldValue,object newValue)
    {
        ···
    }
    ···

当为ItemsSource属性赋值之后,OnItemsSourceChanged方法被调用,在该方法中,需要干这么几件事儿:

  • 为ItemsSource中的每一个元素,根据ItemTemplate创建相应的View,设置View的数据绑定上想问BindingContext为该元素,并且将此View添加到ItemsPannel中(ItemsPannel实际是StackLayout,他的子元素必须继承自View或者是View)
  • 检测ItemsSource的数据源是否实现了接口INotifyCollectionChanged,如果实现了,需要订阅其CollectionChanged事件,注册一个方法,便于在集合元素变动后调用我们注册的方法,来通知ItemsControl控件,把具体的变动表现在UI层面(通常就是元素的添加和删除)

OnItemsSourceChanged方法实现如下:

    public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create("ItemTemplate", typeof(DataTemplate), typeof(ItemsControl), defaultValue: default(DataTemplate));
    public DataTemplate ItemTemplate
    {
        get { return (DataTemplate)this.GetValue(ItemTemplateProperty); }
        set { this.SetValue(ItemTemplateProperty, value); }
    }

    static void OnItemsSourceChanged(BindableObject bindable, object oldValue, object newValue)
    {
        var control = bindable as ItemsControl;
        if (control == null)
        {
            return;
        }
        //检测是否实现该接口,如果实现,就订阅该事件
        var oldCollection = oldValue as INotifyCollectionChanged;
        if (oldCollection != null)
        {
            oldCollection.CollectionChanged -= control.OnCollectionChanged;
        }

        if (newValue == null)
        {
            return;
        }

        control.ItemsPanel.Children.Clear();

        //遍历数据源中每个元素,为它创建View,并设置其BindingContext
        foreach (var item in (IEnumerable)newValue)
        {
            object content;
            content = control.ItemTemplate.CreateContent();
            View view;
            var cell = content as ViewCell;
            if (cell != null)
            {
                view = cell.View;
            }
            else
            {
                view = (View)content;
            }

            //元素点击相关事件
            view.GestureRecognizers.Add(control._tapGestureRecognizer);
            view.BindingContext = item;
            control.ItemsPanel.Children.Add(view);
        }



        var newCollection = newValue as INotifyCollectionChanged;
        if (newCollection != null)
        {
            newCollection.CollectionChanged += control.OnCollectionChanged;
        }
        control.SelectedItem = control.ItemsPanel.Children[control.SelectedIndex].BindingContext;

        //更新布局
        control.UpdateChildrenLayout();
        control.InvalidateLayout();
        
    }

CollectionChanged实现方法如下:

    private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.OldItems != null)
        {
            this.ItemsPanel.Children.RemoveAt(e.OldStartingIndex);
            this.UpdateChildrenLayout();
            this.InvalidateLayout();
        }

        if (e.NewItems == null)
        {
            return;
        }
        foreach (var item in e.NewItems)
        {
            var content = this.ItemTemplate.CreateContent();

            View view;
            var cell = content as ViewCell;
            if (cell != null)
            {
                view = cell.View;
            }
            else
            {
                view = (View)content;
            }
            if (!view.GestureRecognizers.Contains(this._tapGestureRecognizer))
            {
                view.GestureRecognizers.Add(this._tapGestureRecognizer);
            }
            view.BindingContext = item;
            this.ItemsPanel.Children.Insert(e.NewItems.IndexOf(item), view);
        }
        

        this.UpdateChildrenLayout();
        this.InvalidateLayout();
        
    }

到目前为止,已经实现ItemsControl控件大部分的内容了,还需要实现的有

  • SelectedItem,SelectedIndex:当前列表选定项
  • ItemSelected:列表中元素被选定时触发

怎么判断元素被选定呢?

当一个元素被点击后,认为它被选中了,所以需要监听列表中每一个元素的点击事件。

列表中每一个View被点击后,触发OnTapped事件,事件的发送者是该View本身

        //只定义一个TapGestureRecognizer,不需要为每一个元素都创建,只需要为每一个元素的GestureRecognizers集合添加该实例即可。
        TapGestureRecognizer _tapGestureRecognizer;

        //在构造函数中创建一个Tap事件的GestureRecognizer,并且订阅其Tapped事件
        public ItemsControl()
        {
            _tapGestureRecognizer = new TapGestureRecognizer();
            _tapGestureRecognizer.Tapped += OnTapped;
        }
        ···
        private void OnTapped(object sender, EventArgs e)
        {
            var view = (BindableObject)sender;
            this.SelectedItem = view.BindingContext;

        }
        ···
        static void OnItemsSourceChanged(BindableObject bindable, object oldValue, object newValue)
        {
            ···
            if (!view.GestureRecognizers.Contains(this._tapGestureRecognizer))
            {
                    view.GestureRecognizers.Add(this._tapGestureRecognizer);
            }
            ···
        }
        ···

一个基本的ItemsControl列表控件就完成了,至此,它的已经具备Xamarin.Forms提供的ListView的大致功能。不过还是有几点

  • 它不支持虚拟化技术,所以在列表数据量比较大的时候,会有明显的卡顿

具体代码和Demo看我的Github:

ItemsControl源码

目录
相关文章
|
Android开发 iOS开发 容器
Xamarin自定义布局系列——PivotPage(多页面切换控件)
原文:Xamarin自定义布局系列——PivotPage(多页面切换控件) PivotPage ———— 多页面切换控件 PivotPage是一个多页面切换控件,类似安卓中的ViewPager和UWP中的Pivot枢轴控件。
1209 0
|
Android开发 iOS开发 索引
Xamarin自定义布局系列——支持无限滚动的自动轮播视图CarouselView
原文:Xamarin自定义布局系列——支持无限滚动的自动轮播视图CarouselView 背景简述 自动轮播视图(CarouselView)在现在App中的地位不言而喻,绝大多数的App中都有类似的视图,无论是WebApp还是Native App。
1892 0
|
Android开发 iOS开发
Xamarin自定义布局系列——瀑布流布局
原文:Xamarin自定义布局系列——瀑布流布局 Xamarin.Forms以Xamarin.Android和Xamarin.iOS等为基础,自己实现了一整套比较完整的UI框架,包含了绝大多数常用的控件,如下图 虽然XF(Xamarin.Forms简称XF,下同)为我们提供大这么多的控件,但在实际使用中,会发现这些控件的可定制性特别差,基本上都需要里利用Renderer来做一些修改。
1133 0
|
C#
C#使用Xamarin开发可移植移动应用(2.Xamarin.Forms布局,本篇很长,注意)附源码
原文:C#使用Xamarin开发可移植移动应用(2.Xamarin.Forms布局,本篇很长,注意)附源码 前言 系列目录 C#使用Xamarin开发可移植移动应用目录 源码地址:https://github.
1183 0
|
C# Android开发
C#使用Xamarin开发可移植移动应用进阶篇(7.使用布局渲染器,修改默认布局),附源码
原文:C#使用Xamarin开发可移植移动应用进阶篇(7.使用布局渲染器,修改默认布局),附源码 前言 系列目录 C#使用Xamarin开发可移植移动应用目录 源码地址:https://github.
1006 0
|
开发工具 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
127 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插件给大家分享一下,都有下载地址,持续更新。
1545 0
|
XML Java Android开发
Xamarin android如何反编译apk文件
Xamarin android 如何反编译 apk文件 这里推荐一款XamarinAndroid开发的小游戏,撸棍英雄,游戏很简单,的确的是有点大。等一下我们来翻翻译这个Xamarin Android 开发的小游戏 下载链接:http://shouji.
1387 0
|
定位技术 开发工具 Android开发
Xamarin android如何调用百度地图入门示例(一)
在Xamarin android如何调用百度地图呢? 首先我们要区分清楚,百度地图这是一个广泛的概念,很多刚刚接触这个名词”百度地图api”,的确是泛泛而谈,我们来看一下百度地图的官网: android上使用百度地图的有Android地图SDK,定位SDK,导航SDK,全景SDK.
1522 0

热门文章

最新文章