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;