I have build a custom renderer for RecycleViews.
I have been having one issue and I ran out of ideas of possible fixes, here it is.
Whenever the user scrolls the RecycleView, the next items to be displayed in the screen are shown out of order, it is as if the Recycle is not working.
You can find my code here in GitHub: https://github.com/DanielCauser/XamarinHorizontalList
And this is a link to a video where you can see exactly what my issue is on the bottom list: https://drive.google.com/open?id=1xuuW4479LNiwene0UTMYWl5BPLfT6CJa
This is my View in 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>
This is My custom control in the Xamarin.Forms project:
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;
}
}
This is my Custom Render in the Android Project(With the ViewHolder, View Adapter and View Renderer).
[assembly: ExportRenderer(typeof(HorizontalViewNative), typeof(AndroidHorizontalViewRenderer))]
namespace XamarinHorizontalList.Droid
{
public class AndroidHorizontalViewRenderer : ViewRenderer<HorizontalViewNative, RecyclerView>
{
private LinearLayoutManager _horizontalLayoutManager;
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(Element.ItemsSource))
{
var dataSource = Element.ItemsSource.Cast<object>().ToList();
var adapter = new RecycleViewAdapter(Forms.Context as Android.App.Activity, Element);
adapter.NotifyDataSetChanged();
Control.SetAdapter(adapter);
}
}
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);
Control.SetAdapter(new RecycleViewAdapter(Forms.Context as Android.App.Activity, e.NewElement));
}
}
}
public class RecycleViewAdapter : RecyclerView.Adapter
{
private readonly Activity Context;
private readonly HorizontalViewNative _view;
private readonly IList _dataSource;
public override long GetItemId(int position)
{
return base.GetItemId(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 item = (RecycleViewHolder)holder;
var dataContext = _dataSource[position];
if (dataContext != null)
{
var dataTemplate = _view.ItemTemplate;
ViewCell viewCell;
var selector = dataTemplate as DataTemplateSelector;
if (selector != null)
{
var template = selector.SelectTemplate(_dataSource[position], _view.Parent);
viewCell = template.CreateContent() as ViewCell;
}
else
{
viewCell = dataTemplate?.CreateContent() as ViewCell;
}
item.UpdateUi(viewCell, dataContext, _view);
}
}
public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int viewType)
{
var contentFrame = new FrameLayout(parent.Context)
{
LayoutParameters = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MatchParent,
ViewGroup.LayoutParams.MatchParent)
{
Height = (int)(_view.ItemHeight * Resources.System.DisplayMetrics.Density),
Width = (int)(_view.ItemWidth * Resources.System.DisplayMetrics.Density)
}
};
contentFrame.DescendantFocusability = DescendantFocusability.AfterDescendants;
var viewHolder = new RecycleViewHolder(contentFrame);
return viewHolder;
}
}
public class RecycleViewHolder : RecyclerView.ViewHolder
{
public RecycleViewHolder(Android.Views.View itemView) : base(itemView)
{
ItemView = itemView;
}
public void UpdateUi(ViewCell viewCell, object dataContext, HorizontalViewNative view)
{
var contentLayout = (FrameLayout)ItemView;
viewCell.BindingContext = dataContext;
viewCell.Parent = view;
var metrics = Resources.System.DisplayMetrics;
// Layout and Measure Xamarin Forms View
var elementSizeRequest = viewCell.View.Measure(double.PositiveInfinity, double.PositiveInfinity, MeasureFlags.IncludeMargins);
var height = (int)((view.ItemHeight + viewCell.View.Margin.Top + viewCell.View.Margin.Bottom) * metrics.Density);
var width = (int)((view.ItemWidth + viewCell.View.Margin.Left + viewCell.View.Margin.Right) * metrics.Density);
viewCell.View.Layout(new Rectangle(0, 0, view.ItemWidth, view.ItemHeight));
// Layout Android View
var layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MatchParent, ViewGroup.LayoutParams.MatchParent)
{
Height = height,
Width = width
};
if (Platform.GetRenderer(viewCell.View) == null)
{
Platform.SetRenderer(viewCell.View, Platform.CreateRenderer(viewCell.View));
}
var renderer = Platform.GetRenderer(viewCell.View);
var viewGroup = renderer.View;
viewGroup.LayoutParameters = layoutParams;
viewGroup.Layout(0, 0, width, height);
contentLayout.RemoveAllViews();
contentLayout.AddView(viewGroup);
}
}
}
Yeah, so I was able to fix the issue with the renderer.
1 - I was messing up the adapter call, I called it twice, once in the OnElementChanged and once in the OnElementOnProperty changed.
2 - I made an override of the get item id of the adapter, that made the view properly ordered!
3 - I resized down all of the pictures, I was getting a weird out of memory exception, by making them smaller all the pictures decided to showup.