Skip to content

Commit

Permalink
Merge pull request #6789 from unoplatform/dev/djo/listview-android-fix-5
Browse files Browse the repository at this point in the history
fix(listview): [Android] Improve performance when updating list items
  • Loading branch information
davidjohnoliver authored Aug 20, 2021
2 parents 7e5f868 + 766ef4f commit 6471ab2
Show file tree
Hide file tree
Showing 6 changed files with 257 additions and 10 deletions.
25 changes: 25 additions & 0 deletions src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_UIElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,31 @@ object GetTreeRoot()
}
}
#endif

[TestMethod]
[RunsOnUIThread]
public async Task When_LayoutInformation_GetAvailableSize_Constraints()
{
var noConstraintsBorder = new Border();
var maxHeightBorder = new Border() { MaxHeight = 122 };
var hostGrid = new Grid
{
Width = 182,
Height = 313,
Children =
{
noConstraintsBorder,
maxHeightBorder
}
};

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

await TestServices.WindowHelper.WaitForEqual(313, () => LayoutInformation.GetAvailableSize(noConstraintsBorder).Height);
var maxHeightAvailableSize = LayoutInformation.GetAvailableSize(maxHeightBorder);
Assert.AreEqual(313, maxHeightAvailableSize.Height, delta: 1); // Should return unmodified measure size, ignoring constraints like MaxHeight
}
}

internal partial class When_UpdateLayout_Then_ReentrancyNotAllowed_Element : FrameworkElement
Expand Down
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,160 @@ 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 container = await WindowHelper.WaitForNonNull(() => SUT.ContainerFromIndex(3) as ListViewItem);

var initialMeasureCount = CounterGrid.GlobalMeasureCount;
var initialArrangeCount = CounterGrid.GlobalArrangeCount;
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>
8 changes: 4 additions & 4 deletions src/Uno.UI/UI/Xaml/Controls/Layouter/Layouter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,18 +104,18 @@ public Size Measure(Size availableSize)
// NaN values are accepted as input here, particularly when coming from
// SizeThatFits in Image or Scrollviewer. Clamp the value here as it is reused
// below for the clipping value.
availableSize = availableSize
var frameworkAvailableSize = availableSize
.NumberOrDefault(MaxSize);

var frameworkAvailableSize = availableSize
frameworkAvailableSize = frameworkAvailableSize
.Subtract(marginSize)
.AtLeastZero()
.AtMost(maxSize);

LayoutInformation.SetAvailableSize(Panel, frameworkAvailableSize);
var desiredSize = MeasureOverride(frameworkAvailableSize);
LayoutInformation.SetAvailableSize(Panel, availableSize);

_logDebug?.LogTrace($"{this}.MeasureOverride(availableSize={frameworkAvailableSize}): desiredSize={desiredSize}");
_logDebug?.LogTrace($"{this}.MeasureOverride(availableSize={availableSize}); frameworkAvailableSize={frameworkAvailableSize}; desiredSize={desiredSize}");

if (
double.IsNaN(desiredSize.Width)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -898,12 +898,8 @@ 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 size = TryMeasureChild(child, slotSize, viewType);

if (!child.IsInLayout)
{
Expand All @@ -919,6 +915,31 @@ protected Size AddViewAtOffset(View child, GeneratorDirection direction, int ext
return size;
}

/// <summary>
/// Measure item view if needed.
/// </summary>
/// <returns>Measured size, or cached size if no measure was necessary.</returns>
private Size TryMeasureChild(View child, Size slotSize, ViewType viewType)
{
var previousAvailableSize = LayoutInformation.GetAvailableSize(child);

if (child.IsLayoutRequested || slotSize != previousAvailableSize)
{
var size = _layouter.MeasureChild(child, slotSize);

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

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

/// <summary>
/// Apply appropriate stretch to measured size return by child view.
/// </summary>
Expand Down

0 comments on commit 6471ab2

Please sign in to comment.