UICollectionView 重复和乱序显示项目
UICollectionView repeating and showing items out of order
我已经为 UICollectionView 构建了一个自定义渲染器。
我一直遇到一个问题,我 运行 没有可能的修复方法,就在这里。
每当用户滚动 UICollectionView 时,屏幕中显示的下一个项目将乱序显示并重复。
您可以在 GitHub 中找到我的代码:https://github.com/DanielCauser/XamarinHorizontalList
这张 gif 显示出了什么问题,一些列表项出现不止一次而且顺序不对。
我认为这是由于竞争条件造成的,其中 OS 只是将数据加载到它在该帧可用的任何视单元中。
这是我在 Xamarin.Forms 中的观点:
<local:HorizontalViewNative ItemsSource="{Binding Monkeys}"
Grid.Row="5"
VerticalOptions="Start"
ItemHeight="100"
ItemWidth="100">
<local:HorizontalViewNative.ItemTemplate>
<DataTemplate>
<ViewCell>
<ContentView>
<StackLayout WidthRequest="100"
HeightRequest="100">
<Image Source="{Binding Image}" />
<Label Text="{Binding Name}"
LineBreakMode="MiddleTruncation"
HorizontalTextAlignment="Center"
VerticalTextAlignment="Center"/>
</StackLayout>
</ContentView>
</ViewCell>
</DataTemplate>
</local:HorizontalViewNative.ItemTemplate>
</local:HorizontalViewNative>
这是我在 Xamarin.Forms 项目中的自定义控件:
public class HorizontalViewNative : View
{
public static readonly BindableProperty ItemsSourceProperty =
BindableProperty.Create("ItemsSource", typeof(IEnumerable), typeof(HorizontalViewNative), default(IEnumerable<object>), BindingMode.TwoWay, propertyChanged: ItemsSourceChanged);
public static readonly BindableProperty ItemTemplateProperty =
BindableProperty.Create("ItemTemplate", typeof(DataTemplate), typeof(HVScrollGridView), default(DataTemplate));
public static readonly BindableProperty ItemHeightProperty =
BindableProperty.Create("ItemHeight", typeof(int), typeof(HVScrollGridView), default(int));
public static readonly BindableProperty ItemWidthProperty =
BindableProperty.Create("ItemWidth", typeof(int), typeof(HVScrollGridView), default(int));
public IEnumerable ItemsSource
{
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public DataTemplate ItemTemplate
{
get { return (DataTemplate)GetValue(ItemTemplateProperty); }
set { SetValue(ItemTemplateProperty, value); }
}
public int ItemHeight
{
get { return (int)GetValue(ItemHeightProperty); }
set { SetValue(ItemHeightProperty, value); }
}
public int ItemWidth
{
get { return (int)GetValue(ItemWidthProperty); }
set { SetValue(ItemWidthProperty, value); }
}
private static void ItemsSourceChanged(BindableObject bindable, object oldValue, object newValue)
{
var itemsLayout = (HorizontalViewNative)bindable;
}
}
这是我在 iOS 项目中的自定义渲染(使用 UICollectionView、UICollectionViewSource 和 UICollectionViewCell)。
[assembly: ExportRenderer(typeof(HorizontalViewNative), typeof(iOSHorizontalViewRenderer))]
namespace XamarinHorizontalList.iOS
{
public class iOSHorizontalViewRenderer : ViewRenderer<HorizontalViewNative, UICollectionView>
{
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(HorizontalViewNative.ItemsSource))
{
Control.Source = new iOSViewSource(Element as HorizontalViewNative);
Control.RegisterClassForCell(typeof(iOSViewCell), nameof(iOSViewCell));
}
}
protected override void OnElementChanged(ElementChangedEventArgs<HorizontalViewNative> e)
{
base.OnElementChanged(e);
if (Control == null)
{
var layout = new UICollectionViewFlowLayout();
layout.ScrollDirection = UICollectionViewScrollDirection.Horizontal;
if (e.NewElement != null)
{
layout.ItemSize = new CGSize(e.NewElement.ItemWidth, e.NewElement.ItemHeight);
layout.MinimumInteritemSpacing = 0;
layout.MinimumLineSpacing = 0;
var rect = new CGRect(0, 0, 100, 100);
SetNativeControl(new UICollectionView(rect, layout));
Control.BackgroundColor = e.NewElement?.BackgroundColor.ToUIColor();
}
}
}
}
public class iOSViewSource : UICollectionViewSource
{
private readonly HorizontalViewNative _view;
private readonly IList _dataSource;
public iOSViewSource(HorizontalViewNative view)
{
_view = view;
_dataSource = view.ItemsSource?.Cast<object>()?.ToList();
}
public override nint NumberOfSections(UICollectionView collectionView)
{
return 1;
}
public override nint GetItemsCount(UICollectionView collectionView, nint section)
{
return _dataSource != null ? _dataSource.Count : 0;
}
public override UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath)
{
iOSViewCell cell = (iOSViewCell)collectionView.DequeueReusableCell(nameof(iOSViewCell), indexPath);
var dataContext = _dataSource[indexPath.Row];
Debug.WriteLine(((Monkey)dataContext).Name);
if (dataContext != null)
{
var dataTemplate = _view.ItemTemplate;
ViewCell viewCell;
var selector = dataTemplate as DataTemplateSelector;
if (selector != null)
{
var template = selector.SelectTemplate(_dataSource[indexPath.Row], _view.Parent);
viewCell = template.CreateContent() as ViewCell;
}
else
{
viewCell = dataTemplate?.CreateContent() as ViewCell;
}
cell.UpdateUi(viewCell, dataContext, _view);
}
return cell;
}
}
public class iOSViewCell : UICollectionViewCell
{
private UIView _view;
public iOSViewCell(IntPtr p) : base(p)
{
}
public void UpdateUi(ViewCell viewCell, object dataContext, HorizontalViewNative view)
{
viewCell.BindingContext = dataContext;
viewCell.Parent = view;
var height = (int)((view.ItemHeight + viewCell.View.Margin.Top + viewCell.View.Margin.Bottom));
var width = (int)((view.ItemWidth + viewCell.View.Margin.Left + viewCell.View.Margin.Right));
viewCell.View.Layout(new Rectangle(0, 0, width, height));
if (Platform.GetRenderer(viewCell.View) == null)
{
Platform.SetRenderer(viewCell.View, Platform.CreateRenderer(viewCell.View));
}
var renderer = Platform.GetRenderer(viewCell.View).NativeView;
if (_view == null)
{
renderer.ContentMode = UIViewContentMode.ScaleAspectFit;
ContentView.AddSubview(renderer);
}
_view = renderer;
}
}
}
On iOS UICollectionView
将重用单元格,因为您调用了 DequeueReusableCell(nameof(iOSViewCell), indexPath);
。
这意味着当集合视图第一次加载它的内容时,它要求它的数据源为每个可见项提供一个视图。为了简化代码的创建过程,集合视图要求您始终使视图出队,而不是在代码中显式创建它们。有关详细信息,请参阅 here。
所以您的 UICollectionView
一开始似乎在最初的四个单元格中运行良好。但是在你滚动它之后,单元格的内容变得混乱,因为这个语句 不正确 :
if (_view == null)
{
renderer.ContentMode = UIViewContentMode.ScaleAspectFit;
ContentView.AddSubview(renderer);
}
当UICollectionView
尝试重用队列中的 Cell 时,属性 _view
不会为空,因此 ContentView
不会添加新的子视图。然后您的单元格会显示旧图像和文本。您可以试试这个来修复它:
//if (_view == null)
//{
//Remove all subViews from contentView when the cell being reused.
foreach(UIView subView in ContentView.Subviews)
{
subView.RemoveFromSuperview();
}
renderer.ContentMode = UIViewContentMode.ScaleAspectFit;
ContentView.AddSubview(renderer);
//}
//_view = renderer;
但我想推荐你做的是:既然你尝试使用渲染器,为什么不尝试使用本地方法来创建你的单元格、数据源?
首先,修改您的单元格结构,例如:
UIImageView imgView;
UILabel label;
public iOSViewCell(IntPtr p) : base(p)
{
imgView = new UIImageView(new CGRect(0, 0, 100, 80));
ContentView.AddSubview(imgView);
imgView.ContentMode = UIViewContentMode.ScaleAspectFit;
label = new UILabel(new CGRect(0, 80, 100, 20));
label.TextAlignment = UITextAlignment.Center;
label.LineBreakMode = UILineBreakMode.MiddleTruncation;
ContentView.AddSubview(label);
}
同时更改UpdateUi()
方法:
public void UpdateUi(object dataContext)
{
Monkey monkey = dataContext as Monkey;
imgView.Image = UIImage.FromBundle(monkey.Image);
label.Text = monkey.Name;
}
最后一步是修改GetCell()
事件:
public override UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath)
{
iOSViewCell cell = (iOSViewCell)collectionView.DequeueReusableCell(nameof(iOSViewCell), indexPath);
var dataContext = _dataSource[indexPath.Row];
if (dataContext != null)
{
cell.UpdateUi(dataContext);
}
return cell;
}
这样scrollView滚动起来会更流畅
我已经为 UICollectionView 构建了一个自定义渲染器。
我一直遇到一个问题,我 运行 没有可能的修复方法,就在这里。
每当用户滚动 UICollectionView 时,屏幕中显示的下一个项目将乱序显示并重复。
您可以在 GitHub 中找到我的代码:https://github.com/DanielCauser/XamarinHorizontalList
这张 gif 显示出了什么问题,一些列表项出现不止一次而且顺序不对。
我认为这是由于竞争条件造成的,其中 OS 只是将数据加载到它在该帧可用的任何视单元中。
这是我在 Xamarin.Forms 中的观点:
<local:HorizontalViewNative ItemsSource="{Binding Monkeys}"
Grid.Row="5"
VerticalOptions="Start"
ItemHeight="100"
ItemWidth="100">
<local:HorizontalViewNative.ItemTemplate>
<DataTemplate>
<ViewCell>
<ContentView>
<StackLayout WidthRequest="100"
HeightRequest="100">
<Image Source="{Binding Image}" />
<Label Text="{Binding Name}"
LineBreakMode="MiddleTruncation"
HorizontalTextAlignment="Center"
VerticalTextAlignment="Center"/>
</StackLayout>
</ContentView>
</ViewCell>
</DataTemplate>
</local:HorizontalViewNative.ItemTemplate>
</local:HorizontalViewNative>
这是我在 Xamarin.Forms 项目中的自定义控件:
public class HorizontalViewNative : View
{
public static readonly BindableProperty ItemsSourceProperty =
BindableProperty.Create("ItemsSource", typeof(IEnumerable), typeof(HorizontalViewNative), default(IEnumerable<object>), BindingMode.TwoWay, propertyChanged: ItemsSourceChanged);
public static readonly BindableProperty ItemTemplateProperty =
BindableProperty.Create("ItemTemplate", typeof(DataTemplate), typeof(HVScrollGridView), default(DataTemplate));
public static readonly BindableProperty ItemHeightProperty =
BindableProperty.Create("ItemHeight", typeof(int), typeof(HVScrollGridView), default(int));
public static readonly BindableProperty ItemWidthProperty =
BindableProperty.Create("ItemWidth", typeof(int), typeof(HVScrollGridView), default(int));
public IEnumerable ItemsSource
{
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public DataTemplate ItemTemplate
{
get { return (DataTemplate)GetValue(ItemTemplateProperty); }
set { SetValue(ItemTemplateProperty, value); }
}
public int ItemHeight
{
get { return (int)GetValue(ItemHeightProperty); }
set { SetValue(ItemHeightProperty, value); }
}
public int ItemWidth
{
get { return (int)GetValue(ItemWidthProperty); }
set { SetValue(ItemWidthProperty, value); }
}
private static void ItemsSourceChanged(BindableObject bindable, object oldValue, object newValue)
{
var itemsLayout = (HorizontalViewNative)bindable;
}
}
这是我在 iOS 项目中的自定义渲染(使用 UICollectionView、UICollectionViewSource 和 UICollectionViewCell)。
[assembly: ExportRenderer(typeof(HorizontalViewNative), typeof(iOSHorizontalViewRenderer))]
namespace XamarinHorizontalList.iOS
{
public class iOSHorizontalViewRenderer : ViewRenderer<HorizontalViewNative, UICollectionView>
{
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(HorizontalViewNative.ItemsSource))
{
Control.Source = new iOSViewSource(Element as HorizontalViewNative);
Control.RegisterClassForCell(typeof(iOSViewCell), nameof(iOSViewCell));
}
}
protected override void OnElementChanged(ElementChangedEventArgs<HorizontalViewNative> e)
{
base.OnElementChanged(e);
if (Control == null)
{
var layout = new UICollectionViewFlowLayout();
layout.ScrollDirection = UICollectionViewScrollDirection.Horizontal;
if (e.NewElement != null)
{
layout.ItemSize = new CGSize(e.NewElement.ItemWidth, e.NewElement.ItemHeight);
layout.MinimumInteritemSpacing = 0;
layout.MinimumLineSpacing = 0;
var rect = new CGRect(0, 0, 100, 100);
SetNativeControl(new UICollectionView(rect, layout));
Control.BackgroundColor = e.NewElement?.BackgroundColor.ToUIColor();
}
}
}
}
public class iOSViewSource : UICollectionViewSource
{
private readonly HorizontalViewNative _view;
private readonly IList _dataSource;
public iOSViewSource(HorizontalViewNative view)
{
_view = view;
_dataSource = view.ItemsSource?.Cast<object>()?.ToList();
}
public override nint NumberOfSections(UICollectionView collectionView)
{
return 1;
}
public override nint GetItemsCount(UICollectionView collectionView, nint section)
{
return _dataSource != null ? _dataSource.Count : 0;
}
public override UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath)
{
iOSViewCell cell = (iOSViewCell)collectionView.DequeueReusableCell(nameof(iOSViewCell), indexPath);
var dataContext = _dataSource[indexPath.Row];
Debug.WriteLine(((Monkey)dataContext).Name);
if (dataContext != null)
{
var dataTemplate = _view.ItemTemplate;
ViewCell viewCell;
var selector = dataTemplate as DataTemplateSelector;
if (selector != null)
{
var template = selector.SelectTemplate(_dataSource[indexPath.Row], _view.Parent);
viewCell = template.CreateContent() as ViewCell;
}
else
{
viewCell = dataTemplate?.CreateContent() as ViewCell;
}
cell.UpdateUi(viewCell, dataContext, _view);
}
return cell;
}
}
public class iOSViewCell : UICollectionViewCell
{
private UIView _view;
public iOSViewCell(IntPtr p) : base(p)
{
}
public void UpdateUi(ViewCell viewCell, object dataContext, HorizontalViewNative view)
{
viewCell.BindingContext = dataContext;
viewCell.Parent = view;
var height = (int)((view.ItemHeight + viewCell.View.Margin.Top + viewCell.View.Margin.Bottom));
var width = (int)((view.ItemWidth + viewCell.View.Margin.Left + viewCell.View.Margin.Right));
viewCell.View.Layout(new Rectangle(0, 0, width, height));
if (Platform.GetRenderer(viewCell.View) == null)
{
Platform.SetRenderer(viewCell.View, Platform.CreateRenderer(viewCell.View));
}
var renderer = Platform.GetRenderer(viewCell.View).NativeView;
if (_view == null)
{
renderer.ContentMode = UIViewContentMode.ScaleAspectFit;
ContentView.AddSubview(renderer);
}
_view = renderer;
}
}
}
On iOS UICollectionView
将重用单元格,因为您调用了 DequeueReusableCell(nameof(iOSViewCell), indexPath);
。
这意味着当集合视图第一次加载它的内容时,它要求它的数据源为每个可见项提供一个视图。为了简化代码的创建过程,集合视图要求您始终使视图出队,而不是在代码中显式创建它们。有关详细信息,请参阅 here。
所以您的 UICollectionView
一开始似乎在最初的四个单元格中运行良好。但是在你滚动它之后,单元格的内容变得混乱,因为这个语句 不正确 :
if (_view == null)
{
renderer.ContentMode = UIViewContentMode.ScaleAspectFit;
ContentView.AddSubview(renderer);
}
当UICollectionView
尝试重用队列中的 Cell 时,属性 _view
不会为空,因此 ContentView
不会添加新的子视图。然后您的单元格会显示旧图像和文本。您可以试试这个来修复它:
//if (_view == null)
//{
//Remove all subViews from contentView when the cell being reused.
foreach(UIView subView in ContentView.Subviews)
{
subView.RemoveFromSuperview();
}
renderer.ContentMode = UIViewContentMode.ScaleAspectFit;
ContentView.AddSubview(renderer);
//}
//_view = renderer;
但我想推荐你做的是:既然你尝试使用渲染器,为什么不尝试使用本地方法来创建你的单元格、数据源?
首先,修改您的单元格结构,例如:
UIImageView imgView;
UILabel label;
public iOSViewCell(IntPtr p) : base(p)
{
imgView = new UIImageView(new CGRect(0, 0, 100, 80));
ContentView.AddSubview(imgView);
imgView.ContentMode = UIViewContentMode.ScaleAspectFit;
label = new UILabel(new CGRect(0, 80, 100, 20));
label.TextAlignment = UITextAlignment.Center;
label.LineBreakMode = UILineBreakMode.MiddleTruncation;
ContentView.AddSubview(label);
}
同时更改UpdateUi()
方法:
public void UpdateUi(object dataContext)
{
Monkey monkey = dataContext as Monkey;
imgView.Image = UIImage.FromBundle(monkey.Image);
label.Text = monkey.Name;
}
最后一步是修改GetCell()
事件:
public override UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath)
{
iOSViewCell cell = (iOSViewCell)collectionView.DequeueReusableCell(nameof(iOSViewCell), indexPath);
var dataContext = _dataSource[indexPath.Row];
if (dataContext != null)
{
cell.UpdateUi(dataContext);
}
return cell;
}
这样scrollView滚动起来会更流畅