Skip to content

Commit

Permalink
fix(transformtovisual): [iOS] Fix TransformToVisual inside ListView
Browse files Browse the repository at this point in the history
Fix regression introduced by #5944 . TransformToVisual() is now calculated correctly for elements within the hierarchy of a ListView, accounting for the presence of intermediate native elements.
  • Loading branch information
davidjohnoliver committed Oct 28, 2021
1 parent c0ad447 commit 2d8b9a0
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Private.Infrastructure;
using Uno.Extensions;
using Uno.UI.Extensions;
using DependencyObjectExtensions = Uno.UI.Extensions.DependencyObjectExtensions;
using static Private.Infrastructure.TestServices.WindowHelper;
using Windows.UI.Xaml.Shapes;
using Uno.UI.RuntimeTests.Helpers;

#if __IOS__
using UIKit;
#else
using Uno.UI.Extensions;
#endif

namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml
Expand Down Expand Up @@ -146,6 +148,57 @@ void AssertItem(int index)
}
}
#endif
[TestMethod]
[RunsOnUIThread]
public async Task When_TransformToVisual_Through_ListView_Scrolled()
{
var listView = new ListView
{
ItemContainerStyle = TestsResourceHelper.GetResource<Style>("NoExtraSpaceListViewContainerStyle"),
ItemTemplate = TestsResourceHelper.GetResource<DataTemplate>("FixedSizeItemTemplate"),
ItemsSource = Enumerable.Range(0, 20).ToArray(),
Height = 120
};
var sut = new Grid
{
Height = 300,
Width = 200,
Children = { listView }
};

WindowContent = sut;
await WaitForLoaded(listView);

AssertItem(0, 0);
AssertItem(1, 29);

var sv = listView.FindFirstChild<ScrollViewer>();
Assert.IsNotNull(sv);
sv.ChangeView(null, 10, null);
await WaitForEqual(10, () => sv.VerticalOffset);

AssertItem(0, -10);
AssertItem(1, 19);

sv.ChangeView(null, 40, null);

await WaitForEqual(40, () => sv.VerticalOffset);

AssertItem(1, -11);

void AssertItem(int index, double expectedY)
{
const double defaultTolerance = 1.5;
var tolerance = defaultTolerance * Math.Min(index + 1, 3);

var container = listView.ContainerFromIndex(index) as ContentControl
?? throw new NullReferenceException($"Cannot find the container of item {index}");

var containerToListView = container.TransformToVisual(listView).TransformBounds(new Rect(0, 0, 42, 42));

Assert.AreEqual(expectedY, containerToListView.Y, tolerance);
}
}

[TestMethod]
[RunsOnUIThread]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
using Uno.Logging;
using Uno.UI.Extensions;
using Microsoft.Extensions.Logging;
using Uno.UI.UI.Xaml.Controls.Layouter;

#if XAMARIN_IOS_UNIFIED
using Foundation;
Expand Down Expand Up @@ -655,7 +656,7 @@ private NSString GetReusableCellIdentifier(NSIndexPath indexPath)
/// <summary>
/// A hidden root item that allows the reuse of ContentControl features.
/// </summary>
internal class ListViewBaseInternalContainer : UICollectionViewCell
internal class ListViewBaseInternalContainer : UICollectionViewCell, ISetLayoutSlots
{
/// <summary>
/// Native constructor, do not use explicitly.
Expand Down
6 changes: 5 additions & 1 deletion src/Uno.UI/UI/Xaml/FrameworkElement.iOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ public override void LayoutSubviews()

Rect finalRect;
var parent = Superview;
if (parent is UIElement || parent is ISetLayoutSlots)
if (parent is UIElement
|| parent is ISetLayoutSlots
// In the case of ListViewItem inside native list, its parent's parent is ListViewBaseInternalContainer
|| parent?.Superview is ISetLayoutSlots
)
{
finalRect = LayoutSlotWithMarginsAndAlignments;
}
Expand Down
6 changes: 6 additions & 0 deletions src/Uno.UI/UI/Xaml/UIElement.iOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,12 @@ private bool TryGetParentUIElementForTransformToVisual(out UIElement parentEleme

switch (parent)
{
case ListViewBaseInternalContainer listViewBaseInternalContainer:
// In the case of ListViewBaseInternalContainer, the first managed parent is normally ItemsPresenter. We omit
// the offset since it's incorporated separately via the layout slot propagated to ListViewItem + the scroll offset.
parentElement = listViewBaseInternalContainer.FindFirstParent<UIElement>();
return true;

case UIElement eltParent:
// We found a UIElement in the parent hierarchy, we compute the X/Y offset between the
// first parent 'view' and this 'elt', and return it.
Expand Down

0 comments on commit 2d8b9a0

Please sign in to comment.