Skip to content

Instantly share code, notes, and snippets.

@veryhumble
Last active August 5, 2021 19:13
Show Gist options
  • Star 18 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save veryhumble/0648d7f367a5a808633361ed2e31260e to your computer and use it in GitHub Desktop.
Save veryhumble/0648d7f367a5a808633361ed2e31260e to your computer and use it in GitHub Desktop.
Xamarin.Forms RecyclerView Renderer for Android
using System.Collections;
using System.Diagnostics;
using A11YGuide.Controls;
using A11YGuide.Droid.Helpers;
using A11YGuide.ViewModels.Search;
using Android.Support.V7.Widget;
using Android.Views;
using Android.Widget;
using fivenine.Core.Extensions;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using View = Android.Views.View;
namespace A11YGuide.Droid.Controls
{
public class RecyclerViewAdapter : RecyclerView.Adapter, View.IOnClickListener
{
public const int ItemType = 1;
public const int HeaderType = 2;
private readonly CollectionView _collectionView;
private readonly IList _dataSource;
public RecyclerViewAdapter(CollectionView collectionView, IList dataSource)
{
_collectionView = collectionView;
_dataSource = dataSource;
}
public void Swap(IEnumerable dataSource)
{
_dataSource.Clear();
foreach (var item in dataSource)
{
_dataSource.Add(item);
}
NotifyDataSetChanged();
}
public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int viewType)
{
switch (viewType)
{
case ItemType:
return CreateItemViewHolder(parent);
case HeaderType:
return CreateHeaderViewHolder(parent);
}
return CreateItemViewHolder(parent);
}
public override void OnBindViewHolder(RecyclerView.ViewHolder holder, int position)
{
var itemType = GetItemViewType(position);
switch (itemType)
{
case HeaderType:
var header = (RecyclerViewHeaderHolder) holder;
UpdateHeaderView(header, position);
break;
case ItemType:
var item = (RecyclerViewHolder) holder;
item.ItemView.SetOnClickListener(this);
UpdateItemView(item, position);
break;
}
}
private RecyclerViewHolder CreateItemViewHolder(ViewGroup parent)
{
var contentFrame = new FrameLayout(parent.Context)
{
LayoutParameters = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MatchParent,
ViewGroup.LayoutParams.MatchParent)
{
Height = (int) (_collectionView.ItemHeight * parent.DisplayDensity()),
Width = (int) (_collectionView.ItemWidth * parent.DisplayDensity())
}
};
contentFrame.DescendantFocusability = DescendantFocusability.AfterDescendants;
var viewHolder = new RecyclerViewHolder(contentFrame);
return viewHolder;
}
private RecyclerViewHeaderHolder CreateHeaderViewHolder(ViewGroup parent)
{
// Only the first element should be a header
var dataContext = _dataSource[0];
var headerWrapper = dataContext as HeaderWrapper;
if (headerWrapper != null)
{
var dataTemplate = headerWrapper.HeaderTemplate as DataTemplate;
var formsRoot = dataTemplate.CreateContent() as Xamarin.Forms.View;
formsRoot.BindingContext = headerWrapper.Header;
formsRoot.Parent = _collectionView;
// Layout and Measure Xamarin Forms View
var elementSizeRequest = formsRoot.Measure(double.PositiveInfinity, double.PositiveInfinity, MeasureFlags.IncludeMargins);
formsRoot.Layout(new Rectangle(0, 0, elementSizeRequest.Request.Width, elementSizeRequest.Request.Height));
var height = (int) ((elementSizeRequest.Request.Height + formsRoot.Margin.Top + formsRoot.Margin.Bottom) * parent.DisplayDensity());
var width = (int) ((elementSizeRequest.Request.Width + formsRoot.Margin.Left + formsRoot.Margin.Right) * parent.DisplayDensity());
if (_collectionView.LayoutType == CollectionViewLayoutType.Grid)
{
height = (int) elementSizeRequest.Request.Height;
// TODO Calculate from SpanSize (Column Count of Grid LayoutManager)
width = (int) parent.DisplayWidthDp();
}
// Layout Android View
var layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MatchParent, ViewGroup.LayoutParams.MatchParent)
{
Height = height,
Width = width
};
if (Platform.GetRenderer(formsRoot) == null)
{
Platform.SetRenderer(formsRoot, Platform.CreateRenderer(formsRoot));
}
var renderer = Platform.GetRenderer(formsRoot);
var viewGroup = renderer.ViewGroup;
viewGroup.LayoutParameters = layoutParams;
viewGroup.Layout(0, 0, width, height);
return new RecyclerViewHeaderHolder(viewGroup, formsRoot);
}
return null;
}
private void UpdateHeaderView(RecyclerViewHeaderHolder viewHolder, int position)
{
var dataContext = _dataSource[position];
var headerWrapper = dataContext as HeaderWrapper;
if (headerWrapper != null)
{
viewHolder.UpdateUi(headerWrapper.Header, _collectionView);
}
}
private void UpdateItemView(RecyclerViewHolder viewHolder, int position)
{
var dataContext = _dataSource[position];
if (dataContext != null)
{
var dataTemplate = _collectionView.ItemTemplate;
ViewCell viewCell;
var selector = dataTemplate as DataTemplateSelector;
if (selector != null)
{
var template = selector.SelectTemplate(_dataSource[position], _collectionView.Parent);
viewCell = template.CreateContent() as ViewCell;
}
else
{
viewCell = dataTemplate.CreateContent() as ViewCell;
}
viewHolder.UpdateUi(viewCell, dataContext, _collectionView);
}
}
public override int GetItemViewType(int position)
{
var element = _dataSource[position];
if (element is HeaderWrapper)
{
return HeaderType;
}
return ItemType;
}
public override int ItemCount => _dataSource.Count();
public override long GetItemId(int position)
{
var item = _dataSource[position];
var hasId = item as IHaveId;
if (item != null && hasId?.ViewId != null)
{
return hasId.ViewId.Value;
}
return 0;
}
public void OnClick(View v)
{
var holder = (RecyclerViewHolder) v.GetTag(Resource.String.recycler_tag_id);
if (holder != null)
{
var currentPos = holder.AdapterPosition;
if (currentPos < 0)
{
return;
}
var item = _dataSource[currentPos];
_collectionView.OnItemTapped(this, item);
}
}
}
}
using A11YGuide.Controls;
using Android.Support.V7.Widget;
using Xamarin.Forms;
using View = Android.Views.View;
namespace A11YGuide.Droid.Controls
{
public class RecyclerViewHeaderHolder : RecyclerView.ViewHolder
{
private readonly Xamarin.Forms.View _formsView;
public RecyclerViewHeaderHolder(View itemView, Xamarin.Forms.View formsView) : base(itemView)
{
_formsView = formsView;
ItemView = itemView;
}
public void UpdateUi(object dataContext, CollectionView collectionView)
{
if (_formsView != null)
{
_formsView.BindingContext = dataContext;
}
}
}
}
using A11YGuide.Controls;
using Android.Content.Res;
using Android.Support.V7.Widget;
using Android.Views;
using Android.Widget;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
using View = Android.Views.View;
namespace A11YGuide.Droid.Controls
{
public class RecyclerViewHolder : RecyclerView.ViewHolder
{
private ViewCell _viewCell;
public RecyclerViewHolder(View itemView) : base(itemView)
{
ItemView = itemView;
ItemView.SetTag(Resource.String.recycler_tag_id, this);
}
public void UpdateUi(ViewCell viewCell, object dataContext, CollectionView collectionView)
{
var contentLayout = (FrameLayout)ItemView;
viewCell.BindingContext = dataContext;
viewCell.Parent = collectionView;
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)((collectionView.ItemHeight + viewCell.View.Margin.Top + viewCell.View.Margin.Bottom) * metrics.Density);
var width = (int)((collectionView.ItemWidth + viewCell.View.Margin.Left + viewCell.View.Margin.Right) * metrics.Density);
viewCell.View.Layout(new Rectangle(0, 0, collectionView.ItemWidth, collectionView.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.ViewGroup;
viewGroup.LayoutParameters = layoutParams;
viewGroup.Layout(0, 0, width, height);
contentLayout.RemoveAllViews();
contentLayout.AddView(viewGroup);
}
}
}
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using A11YGuide.Controls;
using A11YGuide.Droid.Controls;
using A11YGuide.Droid.Helpers;
using Android.Support.V7.Widget;
using fivenine.Core.Extensions;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
[assembly: ExportRenderer(typeof(CollectionView), typeof(CollectionViewRenderer))]
namespace A11YGuide.Droid.Controls
{
public class CollectionViewRenderer : ViewRenderer<CollectionView, RecyclerView>
{
private GridLayoutManager _gridLayoutManager;
protected override void OnElementChanged(ElementChangedEventArgs<CollectionView> e)
{
base.OnElementChanged(e);
if (Control == null)
{
}
if (e.OldElement != null)
{
var itemsSource = e.OldElement.ItemsSource as INotifyCollectionChanged;
if (itemsSource != null)
{
itemsSource.CollectionChanged -= ItemsSourceOnCollectionChanged;
}
}
if (e.NewElement != null)
{
if (Control == null)
{
var recyclerView = new RecyclerView(Context);
SetNativeControl(recyclerView);
switch (e.NewElement.LayoutType)
{
case CollectionViewLayoutType.Grid:
_gridLayoutManager = new GridLayoutManager(Context, 2);
var elementSize = Resources.DisplayMetrics.WidthPixels / Resources.DisplayMetrics.Density / 2;
Element.ItemHeight = elementSize;
Element.ItemWidth = elementSize;
recyclerView.SetLayoutManager(_gridLayoutManager);
break;
case CollectionViewLayoutType.HorizontalScroll:
var linearLayout = new LinearLayoutManager(Context, OrientationHelper.Horizontal, false);
recyclerView.SetLayoutManager(linearLayout);
break;
case CollectionViewLayoutType.Tag:
throw new NotSupportedException();
break;
default:
throw new ArgumentOutOfRangeException();
}
UpdateAdapter();
}
var itemsSource = e.NewElement.ItemsSource as INotifyCollectionChanged;
if (itemsSource != null)
{
itemsSource.CollectionChanged += ItemsSourceOnCollectionChanged;
}
Control.LayoutParameters = new LayoutParams(LayoutParams.MatchParent, LayoutParams.MatchParent);
Control.HasFixedSize = true;
Control.AddOnScrollListener(new ScrollListener());
Control.SetOnFlingListener(new FlingListener());
Control.SetClipToPadding(false);
Control.SetPadding(
(int) (Element.Padding.Left * Control.DisplayDensity()),
(int) (Element.Padding.Top * Control.DisplayDensity()),
(int) (Element.Padding.Left * Control.DisplayDensity()),
(int) (Element.Padding.Bottom * Control.DisplayDensity()));
}
}
private void ItemsSourceOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
var adapter = Control.GetAdapter();
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
RefreshAdapter();
adapter.NotifyItemRangeInserted(
positionStart: e.NewStartingIndex,
itemCount: e.NewItems.Count
);
break;
case NotifyCollectionChangedAction.Remove:
if (Element.ItemsSource.Count() == 0) {
RefreshAdapter();
adapter.NotifyDataSetChanged();
return;
}
RefreshAdapter();
adapter.NotifyItemRangeRemoved(
positionStart: e.OldStartingIndex,
itemCount: e.OldItems.Count
);
break;
case NotifyCollectionChangedAction.Replace:
RefreshAdapter();
adapter.NotifyItemRangeChanged(
positionStart: e.OldStartingIndex,
itemCount: e.OldItems.Count
);
break;
case NotifyCollectionChangedAction.Move:
RefreshAdapter();
for (var i = 0; i < e.NewItems.Count; i++)
adapter.NotifyItemMoved(
fromPosition: e.OldStartingIndex + i,
toPosition: e.NewStartingIndex + i
);
break;
case NotifyCollectionChangedAction.Reset:
RefreshAdapter();
adapter.NotifyDataSetChanged();
break;
default:
throw new ArgumentOutOfRangeException();
}
}
private void RefreshAdapter() {
var sourceList = new List<object>();
if (Element.Header != null) {
var header = new HeaderWrapper {
Element = Element,
Header = Element.Header,
HeaderTemplate = Element.HeaderTemplate
};
sourceList.Add(header);
}
var dataSource = Element.ItemsSource.Cast<object>().ToList();
sourceList.AddRange(dataSource);
var newAdapter = new RecyclerViewAdapter(Element, sourceList);
_gridLayoutManager?.SetSpanSizeLookup(new SpanSizeLookup(_gridLayoutManager, newAdapter));
Control.SwapAdapter(newAdapter, false);
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(Element.ItemsSource))
{
UpdateAdapter();
}
}
private void UpdateAdapter()
{
var dataSource = Element.ItemsSource.Cast<object>().ToList();
var adapter = new RecyclerViewAdapter(Element, dataSource) {HasStableIds = true};
_gridLayoutManager?.SetSpanSizeLookup(new SpanSizeLookup(_gridLayoutManager, adapter));
Control.SetAdapter(adapter);
}
}
public class SpanSizeLookup : GridLayoutManager.SpanSizeLookup
{
private readonly GridLayoutManager _layoutManager;
private readonly RecyclerViewAdapter _adapter;
public SpanSizeLookup(GridLayoutManager layoutManager, RecyclerViewAdapter adapter)
{
_layoutManager = layoutManager;
_adapter = adapter;
}
public override int GetSpanSize(int position)
{
var itemType = _adapter.GetItemViewType(position);
if (itemType == RecyclerViewAdapter.HeaderType)
{
return _layoutManager.SpanCount;
}
else
{
return 1;
}
}
}
}
@tekinc
Copy link

tekinc commented Aug 2, 2017

hi, where's your collectionview?

@tekinc
Copy link

tekinc commented Oct 25, 2017

Do you have a working example using this?

@veryhumble
Copy link
Author

Yes, it is in production on the Ginto App (iOS/Android)
https://www.ginto.guide/

Might not be available in your region though.

@DanielCauser
Copy link

DanielCauser commented Jan 16, 2018

Hey! Where is the class up in the forms project ?! I would Like to take a look at it =D @veryhumble

@tekinc
Copy link

tekinc commented May 1, 2018

Can you share the IOS renderer?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment