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

Commit

Permalink
Cache cell measurements
Browse files Browse the repository at this point in the history
  • Loading branch information
hartez committed Dec 18, 2020
1 parent 8fa8157 commit a9fd28a
Show file tree
Hide file tree
Showing 10 changed files with 197 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,23 @@
<ColumnDefinition Width="2*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Frame BorderColor="Green" BackgroundColor="LightGreen" CornerRadius="5" Grid.Column="1">
<StackLayout Margin="2">
<Label Text="{Binding Text}" LineBreakMode="WordWrap" FontSize="10" HorizontalTextAlignment="End" />
</StackLayout>
</Frame>
</Grid>
</DataTemplate>

<DataTemplate x:Key="Remote">
<Grid>
<Grid Padding="5,0,5,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Frame BorderColor="Blue" BackgroundColor="LightBlue" CornerRadius="5">
<Label Text="{Binding Text}" LineBreakMode="WordWrap" FontSize="10" HorizontalTextAlignment="Start" />
<StackLayout Margin="2">
<Label Text="{Binding Text}" LineBreakMode="WordWrap" FontSize="10" HorizontalTextAlignment="Start" />
</StackLayout>
</Frame>
</Grid>
</DataTemplate>
Expand All @@ -45,13 +49,19 @@
</Grid.RowDefinitions>

<StackLayout Orientation="Horizontal">
<Button x:Name="AppendRandomSizedItem" Text="Append Random Message" />
<Button x:Name="AppendRandomSizedItem" Text="Append Random Message" Margin="2" />
<Button x:Name="Clear" Text="Clear" Margin="2" />
<Button x:Name="Lots" Text="Add 1000 Messages" Margin="2" />
</StackLayout>

<CollectionView ItemsSource="{Binding ChatMessages}"
ItemTemplate="{StaticResource ChatTemplateSelector}"
ItemSizingStrategy="MeasureAllItems"
Grid.Row="1"/>
Grid.Row="1">
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Vertical" ItemSpacing="5"/>
</CollectionView.ItemsLayout>
</CollectionView>

</Grid>
</ContentPage.Content>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Xamarin.Forms;
using Xamarin.Forms.Xaml;

namespace Xamarin.Forms.Controls.GalleryPages.CollectionViewGalleries.ItemSizeGalleries
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class ChatExample : ContentPage
{
Random _random = new Random(DateTime.Now.Millisecond);
ChatExampleViewModel _vm = new ChatExampleViewModel();

public ChatExample()
Expand All @@ -21,38 +17,73 @@ public ChatExample()
BindingContext = _vm;

AppendRandomSizedItem.Clicked += AppendRandomChatMessage;
Clear.Clicked += ClearMessages;
Lots.Clicked += LotsOfMessages;
}

private void AppendRandomChatMessage(object sender, EventArgs e)
void AppendRandomChatMessage(object sender, EventArgs e)
{
const string lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut " +
"labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip " +
"ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat ";
//+ "nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
_vm.ChatMessages.Add(GenerateRandomMessage());
}

var random = new Random(DateTime.Now.Millisecond);
void LotsOfMessages(object sender, EventArgs e)
{
var newVm = new ChatExampleViewModel(GenerateMessages(1000));
_vm = newVm;
BindingContext = _vm;
}

var local = random.Next(0, 2) == 1;
void ClearMessages(object sender, EventArgs e)
{
_vm.ChatMessages.Clear();
}

ChatMessage GenerateRandomMessage()
{
const string lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut "
+ "labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip "
+ "ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat "
+ "nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";

var textLength = random.Next(0, lorem.Length - 1);
var local = _random.Next(0, 2) == 1;

var textLength = _random.Next(0, lorem.Length - 1);

var text = lorem.Substring(0, textLength);

_vm.ChatMessages.Add(new ChatMessage(text, local));
return new ChatMessage(text, local);
}

IEnumerable<ChatMessage> GenerateMessages(int count)
{
for (int n = 0; n < count; n++)
{
yield return GenerateRandomMessage();
}
}
}

public class ChatExampleViewModel
public class ChatExampleViewModel
{
public ObservableCollection<ChatMessage> ChatMessages { get; } = new ObservableCollection<ChatMessage>();
public ObservableCollection<ChatMessage> ChatMessages { get; }

public ChatExampleViewModel()
{
ChatMessages = new ObservableCollection<ChatMessage>();
}

public ChatExampleViewModel(IEnumerable<ChatMessage> chatMessages)
{
ChatMessages = new ObservableCollection<ChatMessage>(chatMessages);
}
}

public class ChatMessage
{
public class ChatMessage
{
public bool IsLocal { get; set; }
public string Text { get; set; }

public ChatMessage(string text) : this(text, true){ }
public ChatMessage(string text) : this(text, true) { }

public ChatMessage(string text, bool isLocal)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using NUnit.Framework;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Foundation;
using NUnit.Framework;

namespace Xamarin.Forms.Platform.iOS.UnitTests
{
Expand Down Expand Up @@ -58,5 +61,26 @@ public void GenerateIndexPathRangeForLoop()
Assert.That((int)result[7].Item, Is.EqualTo(13));
Assert.That((int)result[8].Item, Is.EqualTo(14));
}

[Test]
public void IndexPathValidTest()
{
var list = new List<string>
{
"one",
"two",
"three"
};

var source = new ListSource(list);

var valid = NSIndexPath.FromItemSection(2, 0);
var invalidItem = NSIndexPath.FromItemSection(7, 0);
var invalidSection = NSIndexPath.FromItemSection(1, 9);

Assert.IsTrue(source.IsIndexPathValid(valid));
Assert.IsFalse(source.IsIndexPathValid(invalidItem));
Assert.IsFalse(source.IsIndexPathValid(invalidSection));
}
}
}
15 changes: 15 additions & 0 deletions Xamarin.Forms.Platform.iOS/CollectionView/IndexPathHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,20 @@ public static NSIndexPath[] GenerateLoopedIndexPathRange(int section, int sectio

return result;
}

public static bool IsIndexPathValid(this IItemsViewSource source, NSIndexPath indexPath)
{
if (indexPath.Section >= source.GroupCount)
{
return false;
}

if (indexPath.Item >= source.ItemCountInGroup(indexPath.Section))
{
return false;
}

return true;
}
}
}
43 changes: 41 additions & 2 deletions Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public abstract class ItemsViewController<TItemsView> : UICollectionViewControll
UIView _emptyUIView;
VisualElement _emptyViewFormsElement;
Dictionary<NSIndexPath, TemplatedCell> _measurementCells = new Dictionary<NSIndexPath, TemplatedCell>();
Dictionary<object, CGSize> _cellSizeCache = new Dictionary<object, CGSize>();

protected UICollectionViewDelegateFlowLayout Delegator { get; set; }

Expand Down Expand Up @@ -111,11 +112,16 @@ void CheckForEmptySource()

_isEmpty = ItemsSource.ItemCount == 0;

if (_isEmpty)
{
_measurementCells.Clear();
}

if (wasEmpty != _isEmpty)
{
UpdateEmptyViewVisibility(_isEmpty);
}

if (wasEmpty && !_isEmpty)
{
// If we're going from empty to having stuff, it's possible that we've never actually measured
Expand Down Expand Up @@ -243,12 +249,15 @@ protected virtual void UpdateDefaultCell(DefaultCell cell, NSIndexPath indexPath
protected virtual void UpdateTemplatedCell(TemplatedCell cell, NSIndexPath indexPath)
{
cell.ContentSizeChanged -= CellContentSizeChanged;
cell.LayoutAttributesChanged -= CellLayoutAttributesChanged;

// If we've already created a cell for this index path (for measurement), re-use the content
if (_measurementCells.TryGetValue(indexPath, out TemplatedCell measurementCell))
{
System.Diagnostics.Debug.WriteLine($">>>>>> Reusing a measurement cell ");
_measurementCells.Remove(indexPath);
measurementCell.ContentSizeChanged -= CellContentSizeChanged;
measurementCell.LayoutAttributesChanged -= CellLayoutAttributesChanged;
cell.UseContent(measurementCell);
}
else
Expand All @@ -257,10 +266,11 @@ protected virtual void UpdateTemplatedCell(TemplatedCell cell, NSIndexPath index
}

cell.ContentSizeChanged += CellContentSizeChanged;
cell.LayoutAttributesChanged += CellLayoutAttributesChanged;

ItemsViewLayout.PrepareCellForLayout(cell);
}

public virtual NSIndexPath GetIndexForItem(object item)
{
return ItemsSource.GetIndexForItem(item);
Expand All @@ -279,6 +289,15 @@ void CellContentSizeChanged(object sender, EventArgs e)
Layout?.InvalidateLayout();
}

void CellLayoutAttributesChanged(object sender, LayoutAttributesChangedEventArgs args)
{
var item = ItemsSource[args.NewAttributes.IndexPath];
if (item != null)
{
_cellSizeCache[item] = args.NewAttributes.Size;
}
}

protected virtual string DetermineCellReuseId()
{
if (ItemsView.ItemTemplate != null)
Expand Down Expand Up @@ -486,5 +505,25 @@ public TemplatedCell CreateMeasurementCell(NSIndexPath indexPath)

return templatedCell;
}

internal CGSize GetSizeForItem(NSIndexPath indexPath)
{
if (ItemsViewLayout.EstimatedItemSize.IsEmpty)
{
return ItemsViewLayout.ItemSize;
}

if (ItemsSource.IsIndexPathValid(indexPath))
{
var item = ItemsSource[indexPath];

if (item != null && _cellSizeCache.TryGetValue(item, out CGSize size))
{
return size;
}
}

return ItemsViewLayout.EstimatedItemSize;
}
}
}
48 changes: 1 addition & 47 deletions Xamarin.Forms.Platform.iOS/CollectionView/ItemsViewDelegator.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CoreGraphics;
using Foundation;
using UIKit;
using Xamarin.Forms.Internals;

namespace Xamarin.Forms.Platform.iOS
{
Expand All @@ -16,7 +14,6 @@ public class ItemsViewDelegator<TItemsView, TViewController> : UICollectionViewD
public TViewController ViewController { get; }

protected float PreviousHorizontalOffset, PreviousVerticalOffset;
readonly Dictionary<DataTemplate, CGSize> _templateSizeEstimates = new Dictionary<DataTemplate, CGSize>();

public ItemsViewDelegator(ItemsViewLayout itemsViewLayout, TViewController itemsViewController)
{
Expand Down Expand Up @@ -165,52 +162,9 @@ static NSIndexPath GetCenteredIndexPath(UICollectionView collectionView)
return centerItemIndex;
}

// Note: we are deliberately avoiding calculating the exact size of every item that the UICollectionViewFlowLayout
// requests from us; instead, we use the exact ItemSize (when possible) or the EstimatedItemSize.
// UICollectionViewFlowLayout will request the size for _every single item_ in our datasource, even if it's not going
// to be on screen yet. For small datasources, realizing the Forms content and measuring it is no problem.
// But for large datasets (hundreds or thousands of items), we'd be realizing a Forms datatemplate and binding it for
// every single item, which defeats virtualization almost entirely.
// So we only create a measurement cell and measure it in this method if we don't already have a cached estimate for
// that item's data template.

public override CGSize GetSizeForItem(UICollectionView collectionView, UICollectionViewLayout layout, NSIndexPath indexPath)
{
//if (ItemsViewLayout.EstimatedItemSize.IsEmpty)
//{
// return ItemsViewLayout.ItemSize;
//}

//var itemTemplate = ViewController.ItemsView.ItemTemplate;

//if (!(itemTemplate is DataTemplateSelector dataTemplateSelector))
//{
// // If the DataTemplate only maps to a single template, then our original size estimate will be fine
// return ItemsViewLayout.EstimatedItemSize;
//}

//// Determine the template type for the current item
//var targetTemplate = dataTemplateSelector.SelectDataTemplate(ViewController.ItemsSource[indexPath], ViewController.ItemsView);

//if (_templateSizeEstimates.TryGetValue(targetTemplate, out CGSize templateSizeEstimate))
//{
// // We've seen this template before; use the cached estimate
// return templateSizeEstimate;
//}

var measurementCell = ViewController.CreateMeasurementCell(indexPath);

//if (measurementCell == null)
//{
// // If we couldn't get a measurement cell for some reason, fall back to the old estimate
// return ItemsViewLayout.EstimatedItemSize;
//}

// Measure the cell and cache the result as our estimate for this template
var size = measurementCell.Measure();
_templateSizeEstimates[measurementCell.CurrentTemplate] = size;

return size;
return ViewController.GetSizeForItem(indexPath);
}
}
}
Loading

0 comments on commit a9fd28a

Please sign in to comment.