Last active
August 5, 2021 19:13
-
-
Save veryhumble/0648d7f367a5a808633361ed2e31260e to your computer and use it in GitHub Desktop.
Xamarin.Forms RecyclerView Renderer for Android
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} | |
} |
Yes, it is in production on the Ginto App (iOS/Android)
https://www.ginto.guide/
Might not be available in your region though.
Hey! Where is the class up in the forms project ?! I would Like to take a look at it =D @veryhumble
Can you share the IOS renderer?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Do you have a working example using this?