Skip to content
This repository was archived by the owner on May 1, 2024. It is now read-only.

Commit 053ede6

Browse files
authored
Fix FlyoutItem recycling (#13681)
* Correctly Disable View Recycling Android Flyout * - fix android to not reset element when items are removed * - fix iOS to not always recreate cells
1 parent e08748c commit 053ede6

File tree

4 files changed

+107
-26
lines changed

4 files changed

+107
-26
lines changed

Xamarin.Forms.Platform.Android/Renderers/ShellFlyoutRecyclerAdapter.cs

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,12 @@ public class ShellFlyoutRecyclerAdapter : RecyclerView.Adapter
1616
readonly IShellContext _shellContext;
1717
List<AdapterListItem> _listItems;
1818
List<List<Element>> _flyoutGroupings;
19-
Dictionary<int, DataTemplate> _templateMap = new Dictionary<int, DataTemplate>();
2019
Action<Element> _selectedCallback;
2120
bool _disposed;
22-
ElementViewHolder _elementViewHolder;
2321

2422
public ShellFlyoutRecyclerAdapter(IShellContext shellContext, Action<Element> selectedCallback)
2523
{
24+
HasStableIds = true;
2625
_shellContext = shellContext;
2726

2827
ShellController.FlyoutItemsChanged += OnFlyoutItemsChanged;
@@ -43,7 +42,22 @@ public ShellFlyoutRecyclerAdapter(IShellContext shellContext, Action<Element> se
4342

4443
public override int GetItemViewType(int position)
4544
{
46-
var item = _listItems[position];
45+
return _listItems[position].Index;
46+
}
47+
48+
DataTemplate GetDataTemplate(int viewTypeId)
49+
{
50+
AdapterListItem item = null;
51+
52+
foreach(var ali in _listItems)
53+
{
54+
if(viewTypeId == ali.Index)
55+
{
56+
item = ali;
57+
break;
58+
}
59+
}
60+
4761
DataTemplate dataTemplate = ShellController.GetFlyoutItemDataTemplate(item.Element);
4862
if (item.Element is IMenuItemController)
4963
{
@@ -57,18 +71,26 @@ public override int GetItemViewType(int position)
5771
}
5872

5973
var template = dataTemplate.SelectDataTemplate(item.Element, Shell);
60-
var id = ((IDataTemplateController)template).Id;
61-
62-
_templateMap[id] = template;
63-
64-
return id;
74+
return template;
6575
}
6676

6777
public override void OnViewRecycled(Java.Lang.Object holder)
6878
{
6979
if (holder is ElementViewHolder evh)
7080
{
71-
evh.Element = null;
81+
// only clear out the Element if the item has been removed
82+
bool found = false;
83+
foreach(var item in _listItems)
84+
{
85+
if(item.Element == evh.Element)
86+
{
87+
found = true;
88+
break;
89+
}
90+
}
91+
92+
if(!found)
93+
evh.Element = null;
7294
}
7395

7496
base.OnViewRecycled(holder);
@@ -154,7 +176,7 @@ public override AView FocusSearch([GeneratedEnum] FocusSearchDirection direction
154176

155177
public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int viewType)
156178
{
157-
var template = _templateMap[viewType];
179+
var template = GetDataTemplate(viewType);
158180

159181
var content = (View)template.CreateContent();
160182

@@ -175,14 +197,13 @@ public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int
175197
container.LayoutParameters = new LP(LP.MatchParent, LP.WrapContent);
176198
linearLayout.AddView(container);
177199

178-
_elementViewHolder = new ElementViewHolder(content, linearLayout, bar, _selectedCallback, _shellContext.Shell);
179-
180-
return _elementViewHolder;
200+
return new ElementViewHolder(content, linearLayout, bar, _selectedCallback, _shellContext.Shell);
181201
}
182202

183203
protected virtual List<AdapterListItem> GenerateItemList()
184204
{
185205
var result = new List<AdapterListItem>();
206+
_listItems = _listItems ?? result;
186207

187208
List<List<Element>> grouping = ((IShellController)_shellContext.Shell).GenerateFlyoutGrouping();
188209

@@ -198,7 +219,18 @@ protected virtual List<AdapterListItem> GenerateItemList()
198219
bool first = !skip;
199220
foreach (var element in sublist)
200221
{
201-
result.Add(new AdapterListItem(element, first));
222+
AdapterListItem toAdd = null;
223+
foreach (var existingItem in _listItems)
224+
{
225+
if(existingItem.Element == element)
226+
{
227+
existingItem.DrawTopLine = first;
228+
toAdd = existingItem;
229+
}
230+
}
231+
232+
toAdd = toAdd ?? new AdapterListItem(element, first);
233+
result.Add(toAdd);
202234
first = false;
203235
}
204236
skip = false;
@@ -229,24 +261,27 @@ protected override void Dispose(bool disposing)
229261
{
230262
((IShellController)Shell).FlyoutItemsChanged -= OnFlyoutItemsChanged;
231263

232-
_elementViewHolder?.Dispose();
233-
234264
_listItems = null;
235265
_selectedCallback = null;
236-
_elementViewHolder = null;
237266
}
238267

239268
base.Dispose(disposing);
240269
}
241270

242271
public class AdapterListItem
243272
{
273+
// This ensures that we have a stable id for each element
274+
// if the elements change position
275+
static int IndexCounter = 0;
276+
244277
public AdapterListItem(Element element, bool drawTopLine = false)
245278
{
246279
DrawTopLine = drawTopLine;
247280
Element = element;
281+
Index = IndexCounter++;
248282
}
249283

284+
public int Index { get; }
250285
public bool DrawTopLine { get; set; }
251286
public Element Element { get; set; }
252287
}

Xamarin.Forms.Platform.iOS/Renderers/ShellTableViewController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ protected ShellTableViewSource CreateShellTableViewSource()
6262

6363
void OnFlyoutItemsChanged(object sender, EventArgs e)
6464
{
65-
_source.ClearCache();
65+
_source.ReSyncCache();
6666
TableView.ReloadData();
6767
ShellFlyoutContentManager.UpdateVerticalScrollMode();
6868
}

Xamarin.Forms.Platform.iOS/Renderers/ShellTableViewSource.cs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,41 @@ public List<List<Element>> Groups
4747

4848
protected virtual DataTemplate DefaultMenuItemTemplate => null;
4949

50+
internal void ReSyncCache()
51+
{
52+
var newGroups = ((IShellController)_context.Shell).GenerateFlyoutGrouping();
53+
54+
if (newGroups == _groups)
55+
return;
56+
57+
_groups = newGroups;
58+
if (_cells == null)
59+
{
60+
_cells = new Dictionary<Element, UIContainerCell>();
61+
return;
62+
}
63+
64+
var oldList = _cells;
65+
_cells = new Dictionary<Element, UIContainerCell>();
66+
67+
foreach (var group in newGroups)
68+
{
69+
foreach(var element in group)
70+
{
71+
UIContainerCell result;
72+
if(oldList.TryGetValue(element, out result))
73+
{
74+
_cells.Add(element, result);
75+
oldList.Remove(element);
76+
}
77+
}
78+
}
79+
80+
foreach (var cell in oldList.Values)
81+
cell.Disconnect(_context.Shell);
82+
}
83+
84+
5085
public void ClearCache()
5186
{
5287
var newGroups = ((IShellController)_context.Shell).GenerateFlyoutGrouping();
@@ -125,7 +160,7 @@ public override UITableViewCell GetCell(UITableView tableView, NSIndexPath index
125160
else
126161
{
127162
var view = _cells[context].View;
128-
cell.Disconnect();
163+
cell.Disconnect(keepRenderer: true);
129164
cell = new UIContainerCell(cellId, view, _context.Shell, context);
130165
}
131166

@@ -148,8 +183,10 @@ public override nfloat GetHeightForFooter(UITableView tableView, nint section)
148183
{
149184
if (section < Groups.Count - 1)
150185
return 1;
186+
151187
return 0;
152188
}
189+
153190
public override UIView GetViewForFooter(UITableView tableView, nint section)
154191
{
155192
return new SeparatorView();

Xamarin.Forms.Platform.iOS/Renderers/UIContainerCell.cs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,14 @@ internal UIContainerCell(string cellId, View view, Shell shell, object context)
1818
View = view;
1919
View.MeasureInvalidated += MeasureInvalidated;
2020
SelectionStyle = UITableViewCellSelectionStyle.None;
21-
22-
_renderer = Platform.CreateRenderer(view);
23-
Platform.SetRenderer(view, _renderer);
21+
22+
_renderer = Platform.GetRenderer(view);
23+
24+
if (_renderer == null)
25+
{
26+
_renderer = Platform.CreateRenderer(view);
27+
Platform.SetRenderer(view, _renderer);
28+
}
2429

2530
ContentView.AddSubview(_renderer.NativeView);
2631
_renderer.NativeView.ClipsToBounds = true;
@@ -49,15 +54,18 @@ internal void ReloadRow()
4954
TableView.ReloadRows(new[] { IndexPath }, UITableViewRowAnimation.Automatic);
5055
}
5156

52-
internal void Disconnect(Shell shell = null)
57+
internal void Disconnect(Shell shell = null, bool keepRenderer = false)
5358
{
5459
ViewMeasureInvalidated = null;
5560
View.MeasureInvalidated -= MeasureInvalidated;
5661
if (_bindingContext != null && _bindingContext is BaseShellItem baseShell)
5762
baseShell.PropertyChanged -= OnElementPropertyChanged;
5863

5964
_bindingContext = null;
60-
Platform.SetRenderer(View, null);
65+
66+
if (!keepRenderer)
67+
Platform.SetRenderer(View, null);
68+
6169
if (shell != null)
6270
shell.RemoveLogicalChild(shell);
6371

@@ -70,7 +78,8 @@ internal void Disconnect(Shell shell = null)
7078
public object BindingContext
7179
{
7280
get => _bindingContext;
73-
set {
81+
set
82+
{
7483
if (value == _bindingContext)
7584
return;
7685

@@ -91,7 +100,7 @@ public object BindingContext
91100
public override void LayoutSubviews()
92101
{
93102
base.LayoutSubviews();
94-
if(View != null)
103+
if (View != null)
95104
View.Layout(Bounds.ToRectangle());
96105
}
97106

0 commit comments

Comments
 (0)