ListView的一个自定义实现ItemsControl(横向列表)


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

Xamarin


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

  1. //UWP中用XAML大致实现如下
  2. ···
  3. <ListView.ItemsPannel>
  4. <StackPannel Orientation="Horizental"/>
  5. </ListView.ItemsPannel>
  6. ···

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

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

怎么实现呢?

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

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

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

  1. private ScrollView _scrollView;
  2. private StackLayout itemsPanel = null;
  3. public StackLayout ItemsPanel
  4. {
  5. get { return this.itemsPanel; }
  6. set { this.itemsPanel = value; }
  7. }
  8. public ItemsControl()
  9. {
  10. this._scrollView = new ScrollView();
  11. this._scrollView.Orientation = Orientation;
  12. this.itemsPanel = new StackLayout() { Orientation = StackOrientation.Horizontal };//子元素水平排布的关键
  13. this.Content = this._scrollView;
  14. this._scrollView.Content = this.itemsPanel;
  15. }

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

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

  1. public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create("ItemsSource", typeof(IEnumerable), typeof(ItemsControl), defaultBindingMode: BindingMode.OneWay, defaultValue: null, propertyChanged: OnItemsSourceChanged);
  2. public IEnumerable ItemsSource
  3. {
  4. get { return (IEnumerable)this.GetValue(ItemsSourceProperty); }
  5. set { this.SetValue(ItemsSourceProperty, value); }
  6. }
  7. ···
  8. Static vid OnItemsSourceChanged(BindableObject sender,object oldValue,object newValue)
  9. {
  10. ···
  11. }

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

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

OnItemsSourceChanged方法实现如下:

  1. public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create("ItemTemplate", typeof(DataTemplate), typeof(ItemsControl), defaultValue: default(DataTemplate));
  2. public DataTemplate ItemTemplate
  3. {
  4. get { return (DataTemplate)this.GetValue(ItemTemplateProperty); }
  5. set { this.SetValue(ItemTemplateProperty, value); }
  6. }
  7. static void OnItemsSourceChanged(BindableObject bindable, object oldValue, object newValue)
  8. {
  9. var control = bindable as ItemsControl;
  10. if (control == null)
  11. {
  12. return;
  13. }
  14. //检测是否实现该接口,如果实现,就订阅该事件
  15. var oldCollection = oldValue as INotifyCollectionChanged;
  16. if (oldCollection != null)
  17. {
  18. oldCollection.CollectionChanged -= control.OnCollectionChanged;
  19. }
  20. if (newValue == null)
  21. {
  22. return;
  23. }
  24. control.ItemsPanel.Children.Clear();
  25. //遍历数据源中每个元素,为它创建View,并设置其BindingContext
  26. foreach (var item in (IEnumerable)newValue)
  27. {
  28. object content;
  29. content = control.ItemTemplate.CreateContent();
  30. View view;
  31. var cell = content as ViewCell;
  32. if (cell != null)
  33. {
  34. view = cell.View;
  35. }
  36. else
  37. {
  38. view = (View)content;
  39. }
  40. //元素点击相关事件
  41. view.GestureRecognizers.Add(control._tapGestureRecognizer);
  42. view.BindingContext = item;
  43. control.ItemsPanel.Children.Add(view);
  44. }
  45. var newCollection = newValue as INotifyCollectionChanged;
  46. if (newCollection != null)
  47. {
  48. newCollection.CollectionChanged += control.OnCollectionChanged;
  49. }
  50. control.SelectedItem = control.ItemsPanel.Children[control.SelectedIndex].BindingContext;
  51. //更新布局
  52. control.UpdateChildrenLayout();
  53. control.InvalidateLayout();
  54. }

CollectionChanged实现方法如下:

  1. private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
  2. {
  3. if (e.OldItems != null)
  4. {
  5. this.ItemsPanel.Children.RemoveAt(e.OldStartingIndex);
  6. this.UpdateChildrenLayout();
  7. this.InvalidateLayout();
  8. }
  9. if (e.NewItems == null)
  10. {
  11. return;
  12. }
  13. foreach (var item in e.NewItems)
  14. {
  15. var content = this.ItemTemplate.CreateContent();
  16. View view;
  17. var cell = content as ViewCell;
  18. if (cell != null)
  19. {
  20. view = cell.View;
  21. }
  22. else
  23. {
  24. view = (View)content;
  25. }
  26. if (!view.GestureRecognizers.Contains(this._tapGestureRecognizer))
  27. {
  28. view.GestureRecognizers.Add(this._tapGestureRecognizer);
  29. }
  30. view.BindingContext = item;
  31. this.ItemsPanel.Children.Insert(e.NewItems.IndexOf(item), view);
  32. }
  33. this.UpdateChildrenLayout();
  34. this.InvalidateLayout();
  35. }

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

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

怎么判断元素被选定呢?

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

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

  1. //只定义一个TapGestureRecognizer,不需要为每一个元素都创建,只需要为每一个元素的GestureRecognizers集合添加该实例即可。
  2. TapGestureRecognizer _tapGestureRecognizer;
  3. //在构造函数中创建一个Tap事件的GestureRecognizer,并且订阅其Tapped事件
  4. public ItemsControl()
  5. {
  6. _tapGestureRecognizer = new TapGestureRecognizer();
  7. _tapGestureRecognizer.Tapped += OnTapped;
  8. }
  9. ···
  10. private void OnTapped(object sender, EventArgs e)
  11. {
  12. var view = (BindableObject)sender;
  13. this.SelectedItem = view.BindingContext;
  14. }
  15. ···
  16. static void OnItemsSourceChanged(BindableObject bindable, object oldValue, object newValue)
  17. {
  18. ···
  19. if (!view.GestureRecognizers.Contains(this._tapGestureRecognizer))
  20. {
  21. view.GestureRecognizers.Add(this._tapGestureRecognizer);
  22. }
  23. ···
  24. }
  25. ···

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

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

具体代码和Demo看我的Github:

ItemsControl源码


作者:cjw1115
原文地址:http://www.cnblogs.com/cjw1115/p/6553161.html

分享到