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:
Xamarin自定义布局系列——ListView的一个自定义实现ItemsControl(横向列表)的更多相关文章
- 【Android基础】listview控件的使用(4)-----自定义布局的listview的使用
前面我介绍了listview控件的不同用法,但是这些用法在实际的开发项目中是不足以满足需求的,因为前面的几种用法只能简单的显示文本信息,而且布局都比较单一,很难做出复杂的结果,在实际的开发项目中,90 ...
- 【SpringBoot 基础系列】实现一个自定义配置加载器(应用篇)
[SpringBoot 基础系列]实现一个自定义配置加载器(应用篇) Spring 中提供了@Value注解,用来绑定配置,可以实现从配置文件中,读取对应的配置并赋值给成员变量:某些时候,我们的配置可 ...
- Android自定义组件系列【4】——自定义ViewGroup实现双侧滑动
在上一篇文章<Android自定义组件系列[3]--自定义ViewGroup实现侧滑>中实现了仿Facebook和人人网的侧滑效果,这一篇我们将接着上一篇来实现双面滑动的效果. 1.布局示 ...
- Xamarin自定义布局系列——瀑布流布局
Xamarin.Forms以Xamarin.Android和Xamarin.iOS等为基础,自己实现了一整套比较完整的UI框架,包含了绝大多数常用的控件,如下图 虽然XF(Xamarin.Forms简 ...
- Xamarin自定义布局系列——支持无限滚动的自动轮播视图CarouselView
背景简述 自动轮播视图(CarouselView)在现在App中的地位不言而喻,绝大多数的App中都有类似的视图,无论是WebApp还是Native App.在安卓.iOS以及Windows(UWP) ...
- Xamarin自定义布局系列——PivotPage,多页面切换控件
PivotPage ---- 多页面切换控件 PivotPage是一个多页面切换控件,类似安卓中的ViewPager和UWP中的Pivot枢轴控件. 起初打算直接通过ScrollView+StackL ...
- Android自定义组件系列【3】——自定义ViewGroup实现侧滑
有关自定义ViewGroup的文章已经很多了,我为什么写这篇文章,对于初学者或者对自定义组件比较生疏的朋友虽然可以拿来主义的用了,但是要一步一步的实现和了解其中的过程和原理才能真真脱离别人的代码,举一 ...
- Android自定义组件系列【1】——自定义View及ViewGroup
View类是ViewGroup的父类,ViewGroup具有View的所有特性,ViewGroup主要用来充当View的容器,将其中的View作为自己孩子,并对其进行管理,当然孩子也可以是ViewGr ...
- 在JS中,一个自定义函数如何调用另一个自定义函数中的变量
function aa1511() { var chengshi="马鞍山"; var shengfen="安徽省"; return shengfen+&quo ...
随机推荐
- IM比较SipDroid/IMSDroid/CSipsimple/Linphone/Webrtc
一) sipdroid1)架构sip协议栈使用JAVA实现,音频Codec使用skype的silk(Silk编解码是Skype向第三方开发人员和硬件制造商提供免版税认证(RF)的Silk宽带音频编码器 ...
- 干货!手把手教你如何使用第三方通讯服务实现LayIM Socket组件开发。
前言 之前写了一系列的文章,是关于使用ASP.NET SignalR技术实现LayIM的功能对接,有兴趣的同学移步:http://www.cnblogs.com/panzi/p/5767095.htm ...
- iOS 图片压缩方法
iOS 图片压缩方法 两种图片压缩方法 两种压缩图片的方法:压缩图片质量(Quality),压缩图片尺寸(Size). 压缩图片质量 NSData *data = UIImageJPEGReprese ...
- window下redis的安装
1.使用phpinfo()函数查看PHP的版本信息,这会决定扩展文件版本2.根据PHP版本号,编译器版本号和CPU架构,选择php_redis-2.2.5-5.5-ts-vc11-x86.zip和ph ...
- Linux 用键盘操作窗口
以下是我从各处搜集来的关于用键盘操作窗口信息,操作可能不是最简或者最好的,当然也可能不是最全的,以后遇到新的操作,我会即使添加,如果你有我没有列出的操作,希望你能提出,我可以加上! 我实验的操作系统是 ...
- Windows 10 IoT Serials 5 - 如何为树莓派应用程序添加语音识别与交互功能
都说语音是人机交互的重要手段,虽然个人觉得在大庭广众之下,对着手机发号施令会显得有些尴尬.但是在资源受限的物联网应用场景下(无法外接鼠标键盘显示器),如果能够通过语音来控制设备,与设备进行交互,那还是 ...
- js阻止表单提交的两种方法
下面直接看代码. <!DOCTYPE html> <html lang="en"> <head> <meta charset=" ...
- 为什么Java可以跨平台,而其他语言不行
你好 我是大福 你现在看的是大福笔记 今天复习了Java语言的概述 内容包括Java 语言的历史.语言特点及平台版本 JRE和JDK的区别 这篇文章的主题是总结下对Java语言特点中的跨平台原理. 在 ...
- iOS 创建OpenGL 环境的思考
关于如何从头开始创建环境,可以参考大神的博文OpenGL ES 3.0 数据可视化 0:Hello world,本文只是补充一些我在实践中的一些思考. CAEAGLLayer If you plan ...
- 读书笔记 effective c++ Item 12 拷贝对象的所有部分
1.默认构造函数介绍 在设计良好的面向对象系统中,会将对象的内部进行封装,只有两个函数可以拷贝对象:这两个函数分别叫做拷贝构造函数和拷贝赋值运算符.我们把这两个函数统一叫做拷贝函数.从Item5中,我 ...