Skip to content

Commit fbb04bd

Browse files
committed
Use native invalidation mechanism to detect TemplatedCell size change.
1 parent b11ac4b commit fbb04bd

File tree

4 files changed

+125
-58
lines changed

4 files changed

+125
-58
lines changed

src/Controls/src/Core/Handlers/Items/iOS/ItemsViewController.cs

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -199,12 +199,30 @@ public override void ViewWillAppear(bool animated)
199199
public override void ViewWillLayoutSubviews()
200200
{
201201
ConstrainItemsToBounds();
202+
InvalidateLayoutIfItemsMeasureChanged();
202203
base.ViewWillLayoutSubviews();
203204
InvalidateMeasureIfContentSizeChanged();
204205
LayoutEmptyView();
205206
}
206207

208+
void InvalidateLayoutIfItemsMeasureChanged()
209+
{
210+
var visibleCells = CollectionView.VisibleCells;
207211

212+
var changed = false;
213+
for (int n = 0; n < visibleCells.Length; n++)
214+
{
215+
if (visibleCells[n] is TemplatedCell { MeasureInvalidated: true } cell && cell.VerifyAndUpdateSize())
216+
{
217+
changed = true;
218+
}
219+
}
220+
221+
if (changed)
222+
{
223+
ItemsViewLayout.InvalidateLayout();
224+
}
225+
}
208226

209227
void MauiCollectionView.ICustomMauiCollectionViewDelegate.MovedToWindow(UIView view)
210228
{
@@ -373,7 +391,6 @@ protected virtual void UpdateDefaultCell(DefaultCell cell, NSIndexPath indexPath
373391

374392
protected virtual void UpdateTemplatedCell(TemplatedCell cell, NSIndexPath indexPath)
375393
{
376-
cell.ContentSizeChanged -= CellContentSizeChanged;
377394
cell.LayoutAttributesChanged -= CellLayoutAttributesChanged;
378395

379396
var bindingContext = ItemsSource[indexPath];
@@ -382,7 +399,6 @@ protected virtual void UpdateTemplatedCell(TemplatedCell cell, NSIndexPath index
382399
if (_measurementCells != null && _measurementCells.TryGetValue(bindingContext, out TemplatedCell measurementCell))
383400
{
384401
_measurementCells.Remove(bindingContext);
385-
measurementCell.ContentSizeChanged -= CellContentSizeChanged;
386402
measurementCell.LayoutAttributesChanged -= CellLayoutAttributesChanged;
387403
cell.UseContent(measurementCell);
388404
}
@@ -391,7 +407,6 @@ protected virtual void UpdateTemplatedCell(TemplatedCell cell, NSIndexPath index
391407
cell.Bind(ItemsView.ItemTemplate, ItemsSource[indexPath], ItemsView);
392408
}
393409

394-
cell.ContentSizeChanged += CellContentSizeChanged;
395410
cell.LayoutAttributesChanged += CellLayoutAttributesChanged;
396411

397412
ItemsViewLayout.PrepareCellForLayout(cell);
@@ -407,29 +422,6 @@ protected object GetItemAtIndex(NSIndexPath index)
407422
return ItemsSource[index];
408423
}
409424

410-
[UnconditionalSuppressMessage("Memory", "MEM0003", Justification = "Proven safe in test: CollectionViewTests.ItemsSourceDoesNotLeak")]
411-
void CellContentSizeChanged(object sender, EventArgs e)
412-
{
413-
if (_disposed)
414-
return;
415-
416-
if (!(sender is TemplatedCell cell))
417-
{
418-
return;
419-
}
420-
421-
var visibleCells = CollectionView.VisibleCells;
422-
423-
for (int n = 0; n < visibleCells.Length; n++)
424-
{
425-
if (cell == visibleCells[n])
426-
{
427-
ItemsViewLayout?.InvalidateLayout();
428-
return;
429-
}
430-
}
431-
}
432-
433425
[UnconditionalSuppressMessage("Memory", "MEM0003", Justification = "Proven safe in test: CollectionViewTests.ItemsSourceDoesNotLeak")]
434426
void CellLayoutAttributesChanged(object sender, LayoutAttributesChangedEventArgs args)
435427
{

src/Controls/src/Core/Handlers/Items/iOS/TemplatedCell.cs

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
namespace Microsoft.Maui.Controls.Handlers.Items
1111
{
12-
public abstract class TemplatedCell : ItemsViewCell
12+
public abstract class TemplatedCell : ItemsViewCell, IPlatformMeasureInvalidationController
1313
{
1414
readonly WeakEventManager _weakEventManager = new();
1515

@@ -40,6 +40,7 @@ public DataTemplate CurrentTemplate
4040
// Keep track of the cell size so we can verify whether a measure invalidation
4141
// actually changed the size of the cell
4242
Size _size;
43+
bool _bound;
4344

4445
internal CGSize CurrentSize => _size.ToCGSize();
4546

@@ -50,6 +51,9 @@ protected TemplatedCell(CGRect frame) : base(frame)
5051
}
5152

5253
WeakReference<IPlatformViewHandler> _handler;
54+
bool _measureInvalidated;
55+
56+
internal bool MeasureInvalidated => _measureInvalidated;
5357

5458
internal IPlatformViewHandler PlatformHandler
5559
{
@@ -77,9 +81,10 @@ protected void ClearConstraints()
7781

7882
internal void Unbind()
7983
{
84+
_bound = false;
85+
8086
if (PlatformHandler?.VirtualView is View view)
8187
{
82-
view.MeasureInvalidated -= MeasureInvalidated;
8388
view.BindingContext = null;
8489
}
8590
}
@@ -120,6 +125,7 @@ CGSize UpdateCellSize()
120125
var nativeBounds = platformView.Frame.ToRectangle();
121126
PlatformHandler.VirtualView.Arrange(nativeBounds);
122127
_size = nativeBounds.Size;
128+
_measureInvalidated = false;
123129

124130
return size;
125131
}
@@ -144,7 +150,7 @@ protected void Layout(CGSize constraints)
144150

145151
public override void PrepareForReuse()
146152
{
147-
Unbind();
153+
_bound = false;
148154
base.PrepareForReuse();
149155
}
150156

@@ -160,7 +166,6 @@ public void Bind(DataTemplate template, object bindingContext, ItemsView itemsVi
160166
// Remove the old view, if it exists
161167
if (oldElement != null)
162168
{
163-
oldElement.MeasureInvalidated -= MeasureInvalidated;
164169
oldElement.BindingContext = null;
165170
itemsView.RemoveLogicalChild(oldElement);
166171
ClearSubviews();
@@ -192,16 +197,20 @@ public void Bind(DataTemplate template, object bindingContext, ItemsView itemsVi
192197
else
193198
{
194199
// Same template
195-
if (oldElement != null)
200+
if (oldElement != null && !ReferenceEquals(bindingContext, oldElement.BindingContext))
196201
{
197202
oldElement.BindingContext = bindingContext;
198-
oldElement.MeasureInvalidated += MeasureInvalidated;
199-
200-
UpdateCellSize();
201203
}
202204
}
203205

204206
CurrentTemplate = itemTemplate;
207+
MarkAsBound();
208+
}
209+
210+
void MarkAsBound()
211+
{
212+
_bound = true;
213+
((IPlatformMeasureInvalidationController)this).InvalidateMeasure();
205214
}
206215

207216
void SetRenderer(IPlatformViewHandler renderer)
@@ -216,8 +225,6 @@ void SetRenderer(IPlatformViewHandler renderer)
216225
InitializeContentConstraints(platformView);
217226

218227
UpdateVisualStates();
219-
220-
(renderer.VirtualView as View).MeasureInvalidated += MeasureInvalidated;
221228
}
222229

223230
void ClearSubviews()
@@ -236,6 +243,8 @@ internal void UseContent(TemplatedCell measurementCell)
236243
CurrentTemplate = measurementCell.CurrentTemplate;
237244
_size = measurementCell._size;
238245
SetRenderer(measurementCell.PlatformHandler);
246+
_bound = true;
247+
((IPlatformMeasureInvalidationController)this).InvalidateMeasure();
239248
}
240249

241250
bool IsUsingVSMForSelectionColor(View view)
@@ -285,20 +294,34 @@ public override bool Selected
285294

286295
protected abstract (bool, Size) NeedsContentSizeUpdate(Size currentSize);
287296

288-
void MeasureInvalidated(object sender, EventArgs args)
297+
void IPlatformMeasureInvalidationController.InvalidateMeasure(bool isPropagating)
298+
{
299+
// If the cell is not bound (or getting unbounded), we don't want to measure it
300+
// and cause a useless and harming InvalidateLayout on the collection view layout
301+
if (!_measureInvalidated && _bound)
302+
{
303+
_measureInvalidated = true;
304+
Superview?.SetNeedsLayout();
305+
}
306+
}
307+
308+
internal bool VerifyAndUpdateSize()
289309
{
310+
_measureInvalidated = false;
290311
var (needsUpdate, toSize) = NeedsContentSizeUpdate(_size);
291312

292313
if (!needsUpdate)
293314
{
294-
return;
315+
return false;
295316
}
296317

297318
// Cache the size for next time
298319
_size = toSize;
299320

300-
// Let the controller know that things need to be arranged again
321+
// Notify size has changed
301322
OnContentSizeChanged();
323+
324+
return true;
302325
}
303326

304327
protected void OnContentSizeChanged()
@@ -349,5 +372,10 @@ void UpdateSelectionColor(View view)
349372
SelectedBackgroundView.BackgroundColor = UIColor.Clear;
350373
}
351374
}
375+
376+
void IPlatformMeasureInvalidationController.InvalidateAncestorsMeasuresWhenMovedToWindow()
377+
{
378+
// This is a no-op for cells
379+
}
352380
}
353381
}

src/Controls/src/Core/Handlers/Items2/iOS/ItemsViewController2.cs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,11 +189,31 @@ public override void LoadView()
189189

190190
public override void ViewWillLayoutSubviews()
191191
{
192+
InvalidateLayoutIfItemsMeasureChanged();
192193
base.ViewWillLayoutSubviews();
193194
LayoutEmptyView();
194195
InvalidateMeasureIfContentSizeChanged();
195196
}
196197

198+
void InvalidateLayoutIfItemsMeasureChanged()
199+
{
200+
var visibleCells = CollectionView.VisibleCells;
201+
var changed = false;
202+
for (int n = 0; n < visibleCells.Length; n++)
203+
{
204+
if (visibleCells[n] is TemplatedCell2 { MeasureInvalidated: true })
205+
{
206+
changed = true;
207+
break;
208+
}
209+
}
210+
211+
if (changed)
212+
{
213+
ItemsViewLayout.InvalidateLayout();
214+
}
215+
}
216+
197217
void Items.MauiCollectionView.ICustomMauiCollectionViewDelegate.MovedToWindow(UIView view)
198218
{
199219
if (CollectionView?.Window != null)
@@ -577,8 +597,8 @@ private protected virtual NSIndexPath GetAdjustedIndexPathForItemSource(NSIndexP
577597

578598
internal virtual void CellDisplayingEndedFromDelegate(UICollectionViewCell cell, NSIndexPath indexPath)
579599
{
580-
if (cell is TemplatedCell2 TemplatedCell2 &&
581-
(TemplatedCell2.PlatformHandler?.VirtualView as View)?.BindingContext is object bindingContext)
600+
if (cell is TemplatedCell2 templatedCell2 &&
601+
(templatedCell2.PlatformHandler?.VirtualView as View)?.BindingContext is { } bindingContext)
582602
{
583603
// We want to unbind a cell that is no longer present in the items source. Unfortunately
584604
// it's too expensive to check directly, so let's check that the current binding context
@@ -591,7 +611,7 @@ internal virtual void CellDisplayingEndedFromDelegate(UICollectionViewCell cell,
591611
!Items.IndexPathHelpers.IsIndexPathValid(itemsSource, indexPath) ||
592612
!Equals(itemsSource[indexPath], bindingContext))
593613
{
594-
TemplatedCell2.Unbind();
614+
templatedCell2.Unbind();
595615
}
596616
}
597617
}

src/Controls/src/Core/Handlers/Items2/iOS/TemplatedCell2.cs

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
namespace Microsoft.Maui.Controls.Handlers.Items2
1212
{
13-
public class TemplatedCell2 : ItemsViewCell2
13+
public class TemplatedCell2 : ItemsViewCell2, IPlatformMeasureInvalidationController
1414
{
1515
internal const string ReuseId = "Microsoft.Maui.Controls.TemplatedCell2";
1616

@@ -34,6 +34,13 @@ public event EventHandler<LayoutAttributesChangedEventArgs2> LayoutAttributesCha
3434

3535
WeakReference<DataTemplate> _currentTemplate;
3636

37+
bool _bound;
38+
bool _measureInvalidated;
39+
Size _measuredSize;
40+
Size _cachedConstraints;
41+
42+
internal bool MeasureInvalidated => _measureInvalidated;
43+
3744
public DataTemplate CurrentTemplate
3845
{
3946
get => _currentTemplate is not null && _currentTemplate.TryGetTarget(out var target) ? target : null;
@@ -61,6 +68,8 @@ public TemplatedCell2(CGRect frame) : base(frame)
6168

6269
internal void Unbind()
6370
{
71+
_bound = false;
72+
6473
if (PlatformHandler?.VirtualView is View view)
6574
{
6675
//view.MeasureInvalidated -= MeasureInvalidated;
@@ -76,26 +85,25 @@ public override UICollectionViewLayoutAttributes PreferredLayoutAttributesFittin
7685

7786
if (PlatformHandler?.VirtualView is not null)
7887
{
79-
if (ScrollDirection == UICollectionViewScrollDirection.Vertical)
80-
{
81-
var measure =
82-
PlatformHandler.VirtualView.Measure(preferredAttributes.Size.Width, double.PositiveInfinity);
88+
var constraints = ScrollDirection == UICollectionViewScrollDirection.Vertical
89+
? new Size(preferredAttributes.Size.Width, double.PositiveInfinity)
90+
: new Size(double.PositiveInfinity, preferredAttributes.Size.Height);
8391

84-
preferredAttributes.Frame =
85-
new CGRect(preferredAttributes.Frame.X, preferredAttributes.Frame.Y,
86-
preferredAttributes.Frame.Width, measure.Height);
87-
}
88-
else
92+
if (_measureInvalidated || _cachedConstraints != constraints)
8993
{
90-
var measure =
91-
PlatformHandler.VirtualView.Measure(double.PositiveInfinity, preferredAttributes.Size.Height);
92-
93-
preferredAttributes.Frame =
94-
new CGRect(preferredAttributes.Frame.X, preferredAttributes.Frame.Y,
95-
measure.Width, preferredAttributes.Frame.Height);
94+
var measure = PlatformHandler.VirtualView.Measure(constraints.Width, constraints.Height);
95+
_cachedConstraints = constraints;
96+
_measuredSize = measure;
9697
}
9798

99+
var size = ScrollDirection == UICollectionViewScrollDirection.Vertical
100+
? new Size(preferredAttributes.Size.Width, _measuredSize.Height).ToCGSize()
101+
: new Size(_measuredSize.Width, preferredAttributes.Size.Height).ToCGSize();
102+
103+
preferredAttributes.Frame = new CGRect(preferredAttributes.Frame.Location, size);
98104
preferredAttributes.ZIndex = 2;
105+
106+
_measureInvalidated = false;
99107
}
100108

101109
return preferredAttributes;
@@ -145,6 +153,9 @@ void BindVirtualView(View virtualView, object bindingContext, ItemsView itemsVie
145153
{
146154
view.SetValueFromRenderer(BindableObject.BindingContextProperty, bindingContext);
147155
}
156+
157+
_bound = true;
158+
((IPlatformMeasureInvalidationController)this).InvalidateMeasure();
148159
}
149160

150161
bool IsUsingVSMForSelectionColor(View view)
@@ -258,5 +269,21 @@ void UpdateSelectionColor(View view)
258269
SelectedBackgroundView.BackgroundColor = UIColor.Clear;
259270
}
260271
}
272+
273+
void IPlatformMeasureInvalidationController.InvalidateAncestorsMeasuresWhenMovedToWindow()
274+
{
275+
// This is a no-op
276+
}
277+
278+
void IPlatformMeasureInvalidationController.InvalidateMeasure(bool isPropagating)
279+
{
280+
// If the cell is not bound (or getting unbounded), we don't want to measure it
281+
// and cause a useless and harming InvalidateLayout on the collection view layout
282+
if (!_measureInvalidated && _bound)
283+
{
284+
_measureInvalidated = true;
285+
Superview?.SetNeedsLayout();
286+
}
287+
}
261288
}
262289
}

0 commit comments

Comments
 (0)