diff --git a/src/Uno.UI/UI/Xaml/Controls/ItemsControl/ItemsControl.cs b/src/Uno.UI/UI/Xaml/Controls/ItemsControl/ItemsControl.cs
index 52fe5932b54921f7e0e7b747088d45a18d245afd..f232c6a779f139ee5b0e4afc5681d54fd1b7932a 100644
--- a/src/Uno.UI/UI/Xaml/Controls/ItemsControl/ItemsControl.cs
+++ b/src/Uno.UI/UI/Xaml/Controls/ItemsControl/ItemsControl.cs
@@ -1129,6 +1129,8 @@ namespace Windows.UI.Xaml.Controls
ClearContainerForItemOverride(element, item);
ContainerClearedForItem(item, element as SelectorItem);
+ UIElement.PrepareForRecycle(element);
+
if (element is ContentPresenter presenter
&& (
presenter.ContentTemplate == ItemTemplate
diff --git a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/NativeListViewBase.Android.cs b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/NativeListViewBase.Android.cs
index 55c7c3de48d7868c034a1c415b35056d88e9557b..9d3f5a8a9fdb1a21597c132380958500e0bd6f9b 100644
--- a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/NativeListViewBase.Android.cs
+++ b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/NativeListViewBase.Android.cs
@@ -257,6 +257,11 @@ namespace Windows.UI.Xaml.Controls
if (vh != null)
{
vh.IsDetached = true;
+
+ // This should be invoked only from the LV.CleanContainer()
+ // **BUT** the container is not Cleaned/Prepared by the LV on Android
+ // https://github.com/unoplatform/uno/issues/11957
+ UIElement.PrepareForRecycle(view);
}
}
base.DetachViewFromParent(index);
diff --git a/src/Uno.UI/UI/Xaml/UIElement.Pointers.cs b/src/Uno.UI/UI/Xaml/UIElement.Pointers.cs
index 14ace5552d94632d941e7c8b7b3a4b2a47afade7..276b521c029e345f08651e0c07f3fc00070b5dbc 100644
--- a/src/Uno.UI/UI/Xaml/UIElement.Pointers.cs
+++ b/src/Uno.UI/UI/Xaml/UIElement.Pointers.cs
@@ -216,24 +216,55 @@ namespace Windows.UI.Xaml
if (sender is UIElement elt && elt.GetHitTestVisibility() == HitTestability.Collapsed)
{
- elt.Release(PointerCaptureKind.Any);
- elt.ClearPressed();
- elt.SetOver(null, false, ctx: BubblingContext.NoBubbling);
- elt.ClearDragOver();
+ _currentPointerEventDispatch.VisualTreeAltered = true;
+ elt.ClearPointerState();
}
};
private static readonly RoutedEventHandler ClearPointersStateOnUnload = (object sender, RoutedEventArgs args) =>
{
- if (sender is UIElement elt)
- {
- elt.Release(PointerCaptureKind.Any);
- elt.ClearPressed();
- elt.SetOver(null, false, ctx: BubblingContext.NoBubbling);
- elt.ClearDragOver();
- }
+ _currentPointerEventDispatch.VisualTreeAltered = true;
+ (sender as UIElement)?.ClearPointerState();
};
+ private partial void ClearPointerStateOnRecycle()
+ {
+ _currentPointerEventDispatch.VisualTreeAltered = true;
+ ClearPointerState();
+ }
+
+ internal void ClearPointerState()
+ {
+ Release(PointerCaptureKind.Any);
+ ClearPressed();
+ SetOver(null, false, ctx: BubblingContext.NoBubbling);
+ ClearDragOver();
+ }
+
+ [ThreadStatic]
+ private static PointerEventDispatchResult _currentPointerEventDispatch;
+
+ internal static void BeginPointerEventDispatch()
+ => _currentPointerEventDispatch = new();
+
+ internal static PointerEventDispatchResult EndPointerEventDispatch()
+ => _currentPointerEventDispatch; // No need to clean it right now, we can safely wait for the next sequence to do it.
+
+ internal struct PointerEventDispatchResult
+ {
+ ///
+ /// Indicates that the visual tree has been modified in a way that the input manager must perform a complete hit testing sequence before dispatching a new event.
+ ///
+ ///
+ /// This is designed for the case where for a single native pointer event, we are dispatching multiple managed events (e.g. managed Enter/Exit when we get only a native Move)
+ /// for all other cases **a full hit test must be perform**.
+ /// This means that we must not "capture"/cache the current top-most-element (a.k.a. OriginalSource) and try to update it on next event
+ /// as this flag does not take in consideration RenderTransform and other layout modification that does not alter the state of the pointer.
+ ///
+ /// This is used only for managed dispatch.
+ public bool VisualTreeAltered { get; set; }
+ }
+
///
/// Indicates if this element or one of its child might be target pointer pointer events.
/// Be aware this doesn't means that the element itself can be actually touched by user,
diff --git a/src/Uno.UI/UI/Xaml/UIElement.cs b/src/Uno.UI/UI/Xaml/UIElement.cs
index 3eebd77c73bf900bf545481ff0a23744c81c96b4..c65c27c8835f6b36d6c8d4d5d983e037cb84943b 100644
--- a/src/Uno.UI/UI/Xaml/UIElement.cs
+++ b/src/Uno.UI/UI/Xaml/UIElement.cs
@@ -910,6 +910,45 @@ namespace Windows.UI.Xaml
}
#endif
+ ///
+ /// This method has to be invoked for element that are going to be recycled WITHOUT necessarily being unloaded / loaded.
+ /// For instance, this is is not expected to be invoked for elements recycled by the the template pool as they are always unloaded.
+ /// The main use case is for ListView and is expected to be invoked by the ListView.CleanUpContainer.
+ ///
+ /// This will walk the tree down to invoke this on all children!
+ internal static void PrepareForRecycle(object view)
+ {
+ if (view is UIElement elt)
+ {
+ elt.PrepareForRecycle();
+ }
+ else
+ {
+ foreach (var child in VisualTreeHelper.GetManagedVisualChildren(view))
+ {
+ child.PrepareForRecycle();
+ }
+ }
+ }
+
+ ///
+ /// This method has to be invoked on element that are going to be recycled WITHOUT necessarily being unloaded / loaded.
+ /// For instance, this is is not expected to be invoked for elements recycled by the the template pool as they are always unloaded.
+ /// The main use case is for ListView and is expected to be invoked by the ListView.CleanUpContainer.
+ ///
+ /// This will walk the tree down to invoke this on all children!
+ internal virtual void PrepareForRecycle()
+ {
+ ClearPointerStateOnRecycle();
+
+ foreach (var child in VisualTreeHelper.GetManagedVisualChildren(this))
+ {
+ child.PrepareForRecycle();
+ }
+ }
+
+ private partial void ClearPointerStateOnRecycle();
+
internal virtual bool IsViewHit() => true;
internal virtual bool IsEnabledOverride() => true;