RecyclerView复用异常求助:自定义渲染器滚动时条目乱序
Hey there, let's break down why your RecyclerView is showing scrambled items when scrolling—this all boils down to how you're handling ViewHolder reuse, which is the backbone of RecyclerView's performance and consistency. Right now, you're creating a brand new ViewCell and its native renderer every time OnBindViewHolder runs, which completely bypasses RecyclerView's core reuse mechanism. That's exactly why items are getting mixed up as you scroll.
Here's how to fix it step by step:
1. Rewrite the ViewHolder to Cache Reusable Views
Your RecycleViewHolder should store existing ViewCell instances and their renderers instead of creating new ones on every bind. This lets RecyclerView reuse views properly:
public class RecycleViewHolder : RecyclerView.ViewHolder { private ViewCell _cachedViewCell; private VisualElementRenderer<View> _cachedRenderer; private readonly FrameLayout _contentFrame; private readonly HorizontalViewNative _parentView; public RecycleViewHolder(FrameLayout itemView, HorizontalViewNative parentView) : base(itemView) { _contentFrame = itemView; _parentView = parentView; } public void UpdateUi(object dataContext) { // Create a ViewCell only if we don't have one cached if (_cachedViewCell == null) { var dataTemplate = _parentView.ItemTemplate; ViewCell viewCell; if (dataTemplate is DataTemplateSelector selector) { var template = selector.SelectTemplate(dataContext, _parentView.Parent); viewCell = template.CreateContent() as ViewCell; } else { viewCell = dataTemplate?.CreateContent() as ViewCell; } if (viewCell != null) { _cachedViewCell = viewCell; _cachedViewCell.Parent = _parentView; // Create and cache the native renderer _cachedRenderer = Platform.CreateRenderer(viewCell.View) as VisualElementRenderer<View>; Platform.SetRenderer(viewCell.View, _cachedRenderer); _contentFrame.AddView(_cachedRenderer.View); } } // Only update the data context for reused views if (_cachedViewCell != null) { _cachedViewCell.BindingContext = dataContext; // Adjust layout dimensions to match your control's properties var metrics = Resources.System.DisplayMetrics; var height = (int)((_parentView.ItemHeight + _cachedViewCell.View.Margin.Top + _cachedViewCell.View.Margin.Bottom) * metrics.Density); var width = (int)((_parentView.ItemWidth + _cachedViewCell.View.Margin.Left + _cachedViewCell.View.Margin.Right) * metrics.Density); _cachedRenderer.View.LayoutParameters = new ViewGroup.LayoutParams(width, height); _cachedViewCell.View.Layout(new Rectangle(0, 0, _parentView.ItemWidth, _parentView.ItemHeight)); _cachedRenderer.View.Layout(0, 0, width, height); } } }
2. Adjust the Adapter to Work with Cached ViewHolders
Update the adapter to pass the parent control to the ViewHolder, and simplify OnBindViewHolder to only update the data context:
public class RecycleViewAdapter : RecyclerView.Adapter { private readonly Activity _context; private readonly HorizontalViewNative _view; private readonly IList _dataSource; public override long GetItemId(int position) { // Use your model's unique ID here if available for better stability return position; } public override int ItemCount => (_dataSource != null ? _dataSource.Count : 0); public RecycleViewAdapter(Activity context, HorizontalViewNative view) { _context = context; _view = view; _dataSource = view.ItemsSource?.Cast<object>()?.ToList(); HasStableIds = true; } public override void OnBindViewHolder(RecyclerView.ViewHolder holder, int position) { var itemHolder = (RecycleViewHolder)holder; var dataContext = _dataSource[position]; itemHolder.UpdateUi(dataContext); } public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int viewType) { var metrics = Resources.System.DisplayMetrics; var contentFrame = new FrameLayout(parent.Context) { LayoutParameters = new FrameLayout.LayoutParams( (int)(_view.ItemWidth * metrics.Density), (int)(_view.ItemHeight * metrics.Density)) }; contentFrame.DescendantFocusability = DescendantFocusability.AfterDescendants; return new RecycleViewHolder(contentFrame, _view); } }
3. Fix Typed Errors in Your Custom Control
You had a typo in your HorizontalViewNative class—some bindable properties referenced HVScrollGridView instead of HorizontalViewNative, which could break bindings:
public class HorizontalViewNative : View { public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create( nameof(ItemsSource), typeof(IEnumerable), typeof(HorizontalViewNative), default(IEnumerable<object>), BindingMode.TwoWay, propertyChanged: ItemsSourceChanged); // Fixed target type from HVScrollGridView to HorizontalViewNative public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create( nameof(ItemTemplate), typeof(DataTemplate), typeof(HorizontalViewNative), default(DataTemplate)); // Fixed target type here too public static readonly BindableProperty ItemHeightProperty = BindableProperty.Create( nameof(ItemHeight), typeof(int), typeof(HorizontalViewNative), default(int)); // And here public static readonly BindableProperty ItemWidthProperty = BindableProperty.Create( nameof(ItemWidth), typeof(int), typeof(HorizontalViewNative), default(int)); // ... keep your existing properties and ItemsSourceChanged method }
4. Improve Data Source Handling in the Renderer
Instead of creating a new adapter every time the ItemsSource changes, update the existing adapter or replace it cleanly:
public class AndroidHorizontalViewRenderer : ViewRenderer<HorizontalViewNative, RecyclerView> { private LinearLayoutManager _horizontalLayoutManager; private RecycleViewAdapter _adapter; protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) { base.OnElementPropertyChanged(sender, e); if (e.PropertyName == nameof(Element.ItemsSource)) { _adapter = new RecycleViewAdapter(Context as Android.App.Activity, Element); Control.SetAdapter(_adapter); _adapter.NotifyDataSetChanged(); } } protected override void OnElementChanged(ElementChangedEventArgs<HorizontalViewNative> e) { base.OnElementChanged(e); if (e.NewElement != null) { var recyclerView = new RecyclerView(Context); SetNativeControl(recyclerView); _horizontalLayoutManager = new LinearLayoutManager(Context, OrientationHelper.Horizontal, false); recyclerView.SetLayoutManager(_horizontalLayoutManager); _adapter = new RecycleViewAdapter(Context as Android.App.Activity, e.NewElement); Control.SetAdapter(_adapter); } } }
Why This Fixes the Issue
By caching ViewCell instances and their native renderers inside the ViewHolder, we let RecyclerView do what it's designed for: reuse existing views instead of generating new ones on every scroll. Now when you scroll, the only thing that changes is the binding context of the reused view, which keeps your items in the correct order and eliminates the scrambling problem.
内容的提问来源于stack exchange,提问作者Daniel John Causer

