Skip to content

Commit

Permalink
fix(listview): [Android] Improve performance when updating list items
Browse files Browse the repository at this point in the history
When a property of an item in the list changes, ensure that only that item container is remeasured.
  • Loading branch information
davidjohnoliver committed Aug 17, 2021
1 parent d073d39 commit 6187297
Show file tree
Hide file tree
Showing 4 changed files with 226 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Text;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.Foundation;

namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Controls
{
Expand All @@ -18,8 +19,14 @@ internal partial class CounterGrid : Grid
/// </summary>
public static int BindCount { get; private set; }

public static int GlobalMeasureCount { get; private set; }
public static int GlobalArrangeCount { get; private set; }

public int LocalMeasureCount { get; private set; }
public int LocalArrangeCount { get; private set; }

/// <summary>
/// Raised whenever an instance of this type is created or data-bound.
/// Raised whenever an instance of this type is created, data-bound, measured or arranged.
/// </summary>
public static event Action WasUpdated;

Expand All @@ -40,10 +47,28 @@ private void On_DataContextChanged(DependencyObject sender, DataContextChangedEv
}
}

protected override Size MeasureOverride(Size availableSize)
{
GlobalMeasureCount++;
LocalMeasureCount++;
WasUpdated?.Invoke();
return base.MeasureOverride(availableSize);
}

protected override Size ArrangeOverride(Size finalSize)
{
GlobalArrangeCount++;
LocalArrangeCount++;
WasUpdated?.Invoke();
return base.ArrangeOverride(finalSize);
}

public static void Reset()
{
CreationCount = 0;
BindCount = 0;
GlobalMeasureCount = 0;
GlobalArrangeCount = 0;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,161 @@ public async Task When_Item_Margins()
Assert.AreEqual(132, SUT.ActualHeight);
}

[TestMethod]
public async Task When_Item_Changes_Measure_Count()
{
var template = (DataTemplate)_testsResources["When_Item_Changes_Measure_Count_Template"];
const double baseWidth = 25;
var itemsSource = Enumerable.Range(1, 20).Select(i => new When_Item_Changes_Measure_Count_ItemViewModel(i) { BadgeWidth = baseWidth + (2 * i) % 10 }).ToArray();

var SUT = new ListView
{
ItemsSource = itemsSource,
ItemTemplate = template,
ItemContainerStyle = BasicContainerStyle,
MaxHeight = 300
};

WindowHelper.WindowContent = SUT;
await WindowHelper.WaitForLoaded(SUT);

var initialMeasureCount = CounterGrid.GlobalMeasureCount;
var initialArrangeCount = CounterGrid.GlobalArrangeCount;

var container = SUT.ContainerFromIndex(3) as ListViewItem;
Assert.IsNotNull(container);
var counterGrid = container.FindFirstChild<CounterGrid>();
var badgeBorder = container.FindFirstChild<Border>(b => b.Name == "BadgeView");
Assert.IsNotNull(counterGrid);
Assert.IsNotNull(badgeBorder);
var initialLocalMeasureCount = counterGrid.LocalMeasureCount;
var initialLocalArrangeCount = counterGrid.LocalArrangeCount;

itemsSource[3].BadgeWidth = 42;

await WindowHelper.WaitForEqual(42, () => badgeBorder.ActualWidth);

Assert.AreEqual(initialMeasureCount + 1, CounterGrid.GlobalMeasureCount);
Assert.AreEqual(initialArrangeCount + 1, CounterGrid.GlobalArrangeCount);

Assert.AreEqual(initialLocalMeasureCount + 1, counterGrid.LocalMeasureCount);
Assert.AreEqual(initialLocalArrangeCount + 1, counterGrid.LocalArrangeCount);
}

[TestMethod]
public async Task When_Available_Breadth_Changes()
{
var template = (DataTemplate)_testsResources["When_Available_Breadth_Changes_Template"];
var itemsSource = Enumerable.Range(0, 15).Select(_ => WordGenerator.GetRandomWordSequence(17, new Random(6754))).ToArray();
var SUT = new ListView
{
ItemTemplate = template,
ItemContainerStyle = NoSpaceContainerStyle,
ItemsSource = itemsSource,
MaxHeight = 500
};

var hostGrid = new Grid
{
Width = 196,
Children =
{
SUT
}
};

WindowHelper.WindowContent = hostGrid;
await WindowHelper.WaitForLoaded(SUT);

var container = SUT.ContainerFromIndex(2) as ListViewItem;

var border = container.FindFirstChild<Border>(b => b.Name == "ContainerBorder");
Assert.AreEqual(196, border.ActualWidth, delta: 1);

hostGrid.Width = 244;
await WindowHelper.WaitForEqual(244, () => border.ActualWidth);
}


// Works around ScrollIntoView() not implemented for all platforms
private static void ScrollBy(ListViewBase listViewBase, double scrollBy)
{
var sv = listViewBase.FindFirstChild<ScrollViewer>();
Assert.IsNotNull(sv);
sv.ChangeView(null, scrollBy, null);
}

public class When_Item_Changes_Measure_Count_ItemViewModel : System.ComponentModel.INotifyPropertyChanged
{

public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;

public int Id { get; }

public string Label => $"Item {Id}";

private double _badgeWidth;
public double BadgeWidth
{
get => _badgeWidth;
set
{
if (value != _badgeWidth)
{
_badgeWidth = value;
PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof(BadgeWidth)));
}
}
}

public When_Item_Changes_Measure_Count_ItemViewModel(int id)
{
Id = id;
}
}

private static class WordGenerator
{
private static readonly string Vowels = "aeiou";
private static readonly string Consonants = "bcdfghjklmnprstvwxyz";

public static string GetRandomWordSequence(int count, Random random)
{
var sb = new StringBuilder();
for (int i = 0; i < count; i++)
{
sb.Append(GetRandomWord(random));
sb.Append(" ");
}
sb.Remove(sb.Length - 1, 1);
return sb.ToString();
}

private static string GetRandomWord(Random random)
{
const double startingvowelChance = 0.2;
var sb = new StringBuilder();
if (random.NextDouble() < startingvowelChance)
{
sb.Append(GetRandom(Vowels.ToCharArray(), random));
sb.Append(GetRandom(Consonants.ToCharArray(), random));
sb.Append(GetRandom(Vowels.ToCharArray(), random));
}
else
{
sb.Append(GetRandom(Consonants.ToCharArray(), random));
sb.Append(GetRandom(Vowels.ToCharArray(), random));
sb.Append(GetRandom(Consonants.ToCharArray(), random));
}

return sb.ToString();
}

private static T GetRandom<T>(IList<T> list, Random random)
{
var i = random.Next(list.Count);
return list[i];
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,32 @@
IsExpanded="{Binding Path=IsExpanded, Mode=TwoWay}"
ItemsSource="{Binding Path=Items}" />
</DataTemplate>

<DataTemplate x:Key="When_Item_Changes_Measure_Count_Template">
<local:CounterGrid MinWidth="120"
Height="30"
BorderBrush="DimGray"
BorderThickness="1"
CornerRadius="3">
<TextBlock Margin="3"
Text="{Binding Label}"
HorizontalAlignment="Left"
VerticalAlignment="Center" />
<Border x:Name="BadgeView"
Width="{Binding BadgeWidth}"
Height="20"
Margin="3"
HorizontalAlignment="Right"
VerticalAlignment="Center"
CornerRadius="10"
Background="DodgerBlue" />
</local:CounterGrid>
</DataTemplate>

<DataTemplate x:Key="When_Available_Breadth_Changes_Template">
<Border x:Name="ContainerBorder">
<TextBlock x:Name="ContentTextBlock" Text="{Binding}"
TextWrapping="Wrap" />
</Border>
</DataTemplate>
</ResourceDictionary>
Original file line number Diff line number Diff line change
Expand Up @@ -898,25 +898,35 @@ protected Size AddViewAtOffset(View child, GeneratorDirection direction, int ext
{
slotSize = new Size(double.PositiveInfinity, logicalAvailableBreadth);
}
var size = _layouter.MeasureChild(child, slotSize);

if (ShouldApplyChildStretch)
{
size = ApplyChildStretch(size, slotSize, viewType);
}
var previousAvailableSize = LayoutInformation.GetAvailableSize(child);

if (!child.IsInLayout)
if (child.IsLayoutRequested || slotSize != previousAvailableSize)
{
UnoViewGroup.StartLayoutingFromMeasure();
}
LayoutChild(child, direction, extentOffset, breadthOffset, size);
var size = _layouter.MeasureChild(child, slotSize);

if (ShouldApplyChildStretch)
{
size = ApplyChildStretch(size, slotSize, viewType);
}

if (!child.IsInLayout)
if (!child.IsInLayout)
{
UnoViewGroup.StartLayoutingFromMeasure();
}
LayoutChild(child, direction, extentOffset, breadthOffset, size);

if (!child.IsInLayout)
{
UnoViewGroup.EndLayoutingFromMeasure();
}

return size;
}
else
{
UnoViewGroup.EndLayoutingFromMeasure();
return (child as FrameworkElement)?.AssignedActualSize ?? ViewHelper.PhysicalToLogicalPixels(new Size(child.Width, child.Height));
}

return size;
}

/// <summary>
Expand Down

0 comments on commit 6187297

Please sign in to comment.