diff --git a/src/Controls/tests/CustomAttributes/Test.cs b/src/Controls/tests/CustomAttributes/Test.cs index 1eed63091e90..83f98602e801 100644 --- a/src/Controls/tests/CustomAttributes/Test.cs +++ b/src/Controls/tests/CustomAttributes/Test.cs @@ -798,5 +798,21 @@ public static class InputTransparencyMatrix public static string GetKey(bool rootTrans, bool rootCascade, bool nestedTrans, bool nestedCascade, bool trans, bool isClickable, bool isPassThru) => $"Root{(rootTrans ? "Trans" : "")}{(rootCascade ? "Cascade" : "")}Nested{(nestedTrans ? "Trans" : "")}{(nestedCascade ? "Cascade" : "")}Control{(trans ? "Trans" : "")}Is{(isClickable ? "" : "Not")}ClickableIs{(isPassThru ? "" : "Not")}PassThru"; + + public static readonly IReadOnlyDictionary<(bool RT, bool RC, bool T), (bool Clickable, bool PassThru)> SimpleStates = + new Dictionary<(bool, bool, bool), (bool, bool)> + { + [(truee, truee, truee)] = (false, truee), + [(truee, truee, false)] = (false, truee), + [(truee, false, truee)] = (false, truee), + [(truee, false, false)] = (truee, false), + [(false, truee, truee)] = (false, false), + [(false, truee, false)] = (truee, false), + [(false, false, truee)] = (false, false), + [(false, false, false)] = (truee, false), + }; + + public static string GetSimpleKey(string prefix, bool rootTrans, bool rootCascade, bool trans, bool isClickable, bool isPassThru) => + $"{prefix}{(rootTrans ? "Trans" : "")}{(rootCascade ? "Cascade" : "")}Control{(trans ? "Trans" : "")}Is{(isClickable ? "" : "Not")}ClickableIs{(isPassThru ? "" : "Not")}PassThru"; } } diff --git a/src/Controls/tests/DeviceTests/Elements/Layout/LayoutTests.iOS.cs b/src/Controls/tests/DeviceTests/Elements/Layout/LayoutTests.iOS.cs index 0069df6c74d7..aefe0f12bbae 100644 --- a/src/Controls/tests/DeviceTests/Elements/Layout/LayoutTests.iOS.cs +++ b/src/Controls/tests/DeviceTests/Elements/Layout/LayoutTests.iOS.cs @@ -160,8 +160,8 @@ void ValidateInputTransparentOnPlatformView(IView view) var platformView = view.ToPlatform(MauiContext); bool value = platformView.UserInteractionEnabled; - if (platformView is LayoutView lv) - value = lv.UserInteractionEnabledOverride; + if (platformView is IInputTransparentManagingView lv) + value = !lv.InputTransparent; Assert.True(view.InputTransparent == !value, $"InputTransparent: {view.InputTransparent}. UserInteractionEnabled: {platformView.UserInteractionEnabled}"); diff --git a/src/Controls/tests/TestCases.HostApp/Concepts/InputTransparencyGalleryPage.cs b/src/Controls/tests/TestCases.HostApp/Concepts/InputTransparencyGalleryPage.cs index 98be75b6debc..c18cfda99529 100644 --- a/src/Controls/tests/TestCases.HostApp/Concepts/InputTransparencyGalleryPage.cs +++ b/src/Controls/tests/TestCases.HostApp/Concepts/InputTransparencyGalleryPage.cs @@ -1,5 +1,6 @@ using System; using Microsoft.Maui.Controls; +using Microsoft.Maui; namespace Maui.Controls.Sample { @@ -147,8 +148,52 @@ protected override void Build() AddNesting(rt, rc, nt, nc, t, clickable, passthru); } + + // Tests for a content view (content view, button) with some variations to ensure + // that all combinations are correctly clickable + foreach (var state in Test.InputTransparencyMatrix.SimpleStates) + { + var (rt, rc, t) = state.Key; + var (clickable, passthru) = state.Value; + + AddSimpleNesting(rt, rc, t, clickable, passthru); + } + + // Tests for a scroll view (scroll view, button) with some variations to ensure + // that all combinations are correctly clickable + foreach (var state in Test.InputTransparencyMatrix.SimpleStates) + { + var (rt, rc, t) = state.Key; + var (clickable, passthru) = state.Value; + + AddSimpleNesting(rt, rc, t, clickable, passthru); + } } + void AddSimpleNesting(bool rootTrans, bool rootCascade, bool trans, bool isClickable, bool isPassThru) where T : Microsoft.Maui.Controls.Compatibility.Layout, IContentView, new() => + Add(Test.InputTransparencyMatrix.GetSimpleKey(typeof(T).Name, rootTrans, rootCascade, trans, isClickable, isPassThru), () => + { + var bottom = new Button { Text = "Bottom Button" }; + var top = new Button + { + InputTransparent = trans, + Text = "Click Me!" + }; + var root = new T + { + InputTransparent = rootTrans, + CascadeInputTransparent = rootCascade, + }; + root.GetType().GetProperty("Content").SetValue(root, top); + var grid = new Grid + { + new Grid { bottom }, + root + }; + return (grid, new { Bottom = bottom, Top = top }); + }) + .With(t => WithAssert(isClickable, isPassThru, t.ViewContainer, t.Additional.Bottom, Annotate(t.Additional.Top, t.ViewContainer.View))); + void AddNesting(bool rootTrans, bool rootCascade, bool nestedTrans, bool nestedCascade, bool trans, bool isClickable, bool isPassThru) => Add(Test.InputTransparencyMatrix.GetKey(rootTrans, rootCascade, nestedTrans, nestedCascade, trans, isClickable, isPassThru), () => { @@ -178,16 +223,15 @@ void AddNesting(bool rootTrans, bool rootCascade, bool nestedTrans, bool nestedC }; return (grid, new { Bottom = bottom, Top = top }); }) - .With(t => - { - var v = t.ViewContainer.View; - var bottom = t.Additional.Bottom; - var top = Annotate(t.Additional.Top, v); + .With(t => WithAssert(isClickable, isPassThru, t.ViewContainer, t.Additional.Bottom, Annotate(t.Additional.Top, t.ViewContainer.View))); + + private static void WithAssert(bool isClickable, bool isPassThru, ExpectedEventViewContainer viewContainer, Button bottom, Button top) + { if (isClickable) { // if the button is clickable, then it should be clickable - bottom.Clicked += (s, e) => t.ViewContainer.ReportFailEvent(); - top.Clicked += (s, e) => t.ViewContainer.ReportSuccessEvent(); + bottom.Clicked += (s, e) => viewContainer.ReportFailEvent(); + top.Clicked += (s, e) => viewContainer.ReportSuccessEvent(); } else if (!isPassThru) { @@ -196,20 +240,20 @@ void AddNesting(bool rootTrans, bool rootCascade, bool nestedTrans, bool nestedC #if ANDROID // TODO: Android is broken with everything passing through // https://github.com/dotnet/maui/issues/10252 - bottom.Clicked += (s, e) => t.ViewContainer.ReportSuccessEvent(); - top.Clicked += (s, e) => t.ViewContainer.ReportFailEvent(); + bottom.Clicked += (s, e) => viewContainer.ReportSuccessEvent(); + top.Clicked += (s, e) => viewContainer.ReportFailEvent(); #else - bottom.Clicked += (s, e) => t.ViewContainer.ReportFailEvent(); - top.Clicked += (s, e) => t.ViewContainer.ReportFailEvent(); + bottom.Clicked += (s, e) => viewContainer.ReportFailEvent(); + top.Clicked += (s, e) => viewContainer.ReportFailEvent(); #endif } else { // otherwise, the tap should go through - bottom.Clicked += (s, e) => t.ViewContainer.ReportSuccessEvent(); - top.Clicked += (s, e) => t.ViewContainer.ReportFailEvent(); + bottom.Clicked += (s, e) => viewContainer.ReportSuccessEvent(); + top.Clicked += (s, e) => viewContainer.ReportFailEvent(); } - }); + } (ExpectedEventViewContainer ViewContainer, T Additional) Add(Test.InputTransparency test, Func<(View View, T Additional)> func) => Add(test.ToString(), func); diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Concepts/InputTransparencyGalleryTests.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Concepts/InputTransparencyGalleryTests.cs index 9e9b2cbc77a7..411082265161 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Concepts/InputTransparencyGalleryTests.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Concepts/InputTransparencyGalleryTests.cs @@ -43,7 +43,32 @@ public void InputTransparencyWhenRootIsNotTransparentMatrix([Values] bool rootCa RunTest(key, clickable, passthru); } - void RunTest(string test, bool? clickable = null, bool? passthru = null) +#if !WINDOWS + + [Test] + [Combinatorial] + public void ScrollViewInputTransparencySimpleMatrix([Values] bool rootTrans, [Values] bool rootCascade, [Values] bool trans) + { + var (clickable, passthru) = Test.InputTransparencyMatrix.SimpleStates[(rootTrans, rootCascade, trans)]; + var key = Test.InputTransparencyMatrix.GetSimpleKey("ScrollView", rootTrans, rootCascade, trans, clickable, passthru); + + // ScrollView is not really a layout so the "broken" rules don't apply + RunTest(key, clickable, passthru, androidIsBroken: false); + } + + [Test] + [Combinatorial] + public void ContentViewInputTransparencySimpleMatrix([Values] bool rootTrans, [Values] bool rootCascade, [Values] bool trans) + { + var (clickable, passthru) = Test.InputTransparencyMatrix.SimpleStates[(rootTrans, rootCascade, trans)]; + var key = Test.InputTransparencyMatrix.GetSimpleKey("ContentView", rootTrans, rootCascade, trans, clickable, passthru); + + RunTest(key, clickable, passthru); + } + +#endif + + void RunTest(string test, bool? clickable = null, bool? passthru = null, bool androidIsBroken = true) { var remote = new EventViewContainerRemote(UITestContext, test); remote.GoTo(test.ToString()); @@ -65,7 +90,7 @@ void RunTest(string test, bool? clickable = null, bool? passthru = null) // if the button is clickable or taps pass through to the base button ClassicAssert.AreEqual($"Event: {test} (SUCCESS 1)", textAfterClick); } - else if (Device == TestDevice.Android) + else if (androidIsBroken && Device == TestDevice.Android) { // TODO: Android is broken with everything passing through so we just use that // to test the bottom button was clickable diff --git a/src/Core/src/Handlers/Layout/LayoutHandler.Android.cs b/src/Core/src/Handlers/Layout/LayoutHandler.Android.cs index eb0df4eca87b..716b6851a946 100644 --- a/src/Core/src/Handlers/Layout/LayoutHandler.Android.cs +++ b/src/Core/src/Handlers/Layout/LayoutHandler.Android.cs @@ -142,10 +142,7 @@ public static partial void MapBackground(ILayoutHandler handler, ILayout layout) public static partial void MapInputTransparent(ILayoutHandler handler, ILayout layout) { - if (handler.PlatformView is LayoutViewGroup layoutViewGroup) - { - layoutViewGroup.InputTransparent = layout.InputTransparent; - } + ViewHandler.MapInputTransparent(handler, layout); } } } diff --git a/src/Core/src/Handlers/ScrollView/ScrollViewHandler.iOS.cs b/src/Core/src/Handlers/ScrollView/ScrollViewHandler.iOS.cs index ec2605fffe8d..cb6e5200ef33 100644 --- a/src/Core/src/Handlers/ScrollView/ScrollViewHandler.iOS.cs +++ b/src/Core/src/Handlers/ScrollView/ScrollViewHandler.iOS.cs @@ -187,6 +187,10 @@ static void InsertContentView(UIScrollView platformScrollView, IScrollView scrol Tag = ContentPanelTag }; + // The content view should be invisible to the user since we are "secretly" + // using it to host the ScrollView's content instead of the ScrollView itself. + ((IInputTransparentManagingView)contentContainer).InputTransparent = true; + // This is where we normally would inject the cross-platform ScrollView's layout logic; instead, we're injecting the // methods from this handler so it can make some adjustments for things like Padding before the default logic is invoked contentContainer.CrossPlatformLayout = crossPlatformLayout; diff --git a/src/Core/src/Handlers/View/ViewHandler.cs b/src/Core/src/Handlers/View/ViewHandler.cs index 0878fb9b6a1c..d8c6a5566e57 100644 --- a/src/Core/src/Handlers/View/ViewHandler.cs +++ b/src/Core/src/Handlers/View/ViewHandler.cs @@ -127,7 +127,7 @@ public bool HasContainer /// public virtual bool NeedsContainer { - get => VirtualView.NeedsContainer(); + get => VirtualView.NeedsContainer(PlatformView); } /// @@ -418,7 +418,7 @@ public static void MapContainerView(IViewHandler handler, IView view) if (handler is ViewHandler viewHandler) handler.HasContainer = viewHandler.NeedsContainer; else - handler.HasContainer = view.NeedsContainer(); + handler.HasContainer = view.NeedsContainer(handler.PlatformView as PlatformView); if(hasContainerOldValue != handler.HasContainer) { @@ -493,7 +493,9 @@ public static void MapInputTransparent(IViewHandler handler, IView view) #if ANDROID handler.UpdateValue(nameof(IViewHandler.ContainerView)); - if (handler.ContainerView is WrapperView wrapper) + if (handler.PlatformView is IInputTransparentManagingView managing) + managing.InputTransparent = view.InputTransparent; + else if (handler.ContainerView is WrapperView wrapper) wrapper.InputTransparent = view.InputTransparent; #else diff --git a/src/Core/src/Platform/Android/ContentViewGroup.cs b/src/Core/src/Platform/Android/ContentViewGroup.cs index 2aec3853a105..84eb933b5889 100644 --- a/src/Core/src/Platform/Android/ContentViewGroup.cs +++ b/src/Core/src/Platform/Android/ContentViewGroup.cs @@ -9,11 +9,13 @@ namespace Microsoft.Maui.Platform { - public class ContentViewGroup : PlatformContentViewGroup, ICrossPlatformLayoutBacking, IVisualTreeElementProvidable + public class ContentViewGroup : PlatformContentViewGroup, ICrossPlatformLayoutBacking, IVisualTreeElementProvidable, IInputTransparentManagingView { IBorderStroke? _clip; readonly Context _context; + bool IInputTransparentManagingView.InputTransparent { get; set; } + public ContentViewGroup(Context context) : base(context) { _context = context; @@ -99,6 +101,16 @@ protected override void OnLayout(bool changed, int left, int top, int right, int CrossPlatformArrange(destination); } + public override bool OnTouchEvent(MotionEvent? e) + { + if (((IInputTransparentManagingView)this).InputTransparent) + { + return false; + } + + return base.OnTouchEvent(e); + } + internal IBorderStroke? Clip { get => _clip; diff --git a/src/Core/src/Platform/Android/LayoutViewGroup.cs b/src/Core/src/Platform/Android/LayoutViewGroup.cs index 6b4f40dad1d2..e37286b1ee67 100644 --- a/src/Core/src/Platform/Android/LayoutViewGroup.cs +++ b/src/Core/src/Platform/Android/LayoutViewGroup.cs @@ -11,7 +11,7 @@ namespace Microsoft.Maui.Platform { - public class LayoutViewGroup : ViewGroup, ICrossPlatformLayoutBacking, IVisualTreeElementProvidable + public class LayoutViewGroup : ViewGroup, ICrossPlatformLayoutBacking, IVisualTreeElementProvidable, IInputTransparentManagingView { readonly ARect _clipRect = new(); readonly Context _context; diff --git a/src/Core/src/Platform/Android/MauiScrollView.cs b/src/Core/src/Platform/Android/MauiScrollView.cs index 0f182b7d22f6..c7281dc38092 100644 --- a/src/Core/src/Platform/Android/MauiScrollView.cs +++ b/src/Core/src/Platform/Android/MauiScrollView.cs @@ -11,7 +11,7 @@ namespace Microsoft.Maui.Platform { - public class MauiScrollView : NestedScrollView, IScrollBarView, NestedScrollView.IOnScrollChangeListener + public class MauiScrollView : NestedScrollView, IScrollBarView, NestedScrollView.IOnScrollChangeListener, IInputTransparentManagingView { View? _content; @@ -26,6 +26,8 @@ public class MauiScrollView : NestedScrollView, IScrollBarView, NestedScrollView internal float LastY { get; set; } internal bool ShouldSkipOnTouch; + + bool IInputTransparentManagingView.InputTransparent { get; set; } public MauiScrollView(Context context) : base(context) { @@ -131,11 +133,14 @@ public void SetOrientation(ScrollOrientation orientation) public override bool OnInterceptTouchEvent(MotionEvent? ev) { - // See also MauiHorizontalScrollView notes in OnInterceptTouchEvent - if (ev == null) return false; + if (((IInputTransparentManagingView)this).InputTransparent) + { + return false; + } + // set the start point for the bidirectional scroll; // Down is swallowed by other controls, so we'll just sneak this in here without actually preventing // other controls from getting the event. @@ -153,6 +158,11 @@ public override bool OnTouchEvent(MotionEvent? ev) if (ev == null || !Enabled || _scrollOrientation == ScrollOrientation.Neither) return false; + if (((IInputTransparentManagingView)this).InputTransparent) + { + return false; + } + if (ShouldSkipOnTouch) { ShouldSkipOnTouch = false; @@ -164,7 +174,7 @@ public override bool OnTouchEvent(MotionEvent? ev) // We'll fall through to the base event so we still get the fling from the ScrollViews. // We have to do this in both ScrollViews, since a single gesture will be owned by one or the other, depending // on the initial direction of movement (i.e., horizontal/vertical). - if (_isBidirectional) // // See also MauiHorizontalScrollView notes in OnInterceptTouchEvent + if (_isBidirectional) { float dX = LastX - ev.RawX; @@ -371,11 +381,6 @@ public override bool OnInterceptTouchEvent(MotionEvent? ev) if (ev == null || _parentScrollView == null) return false; - // TODO ezhart 2021-07-12 The previous version of this checked _renderer.Element.InputTransparent; we don't have acces to that here, - // and I'm not sure it even applies. We need to determine whether touch events will get here at all if we've marked the ScrollView InputTransparent - // We _should_ be able to deal with it at the handler level by force-setting an OnTouchListener for the PlatformView that always returns false; then we - // can just stop worrying about it here because the touches _can't_ reach this. - // set the start point for the bidirectional scroll; // Down is swallowed by other controls, so we'll just sneak this in here without actually preventing // other controls from getting the event. diff --git a/src/Core/src/Platform/Android/WrapperView.cs b/src/Core/src/Platform/Android/WrapperView.cs index 6b863f2606ee..90e6e110158c 100644 --- a/src/Core/src/Platform/Android/WrapperView.cs +++ b/src/Core/src/Platform/Android/WrapperView.cs @@ -10,7 +10,7 @@ namespace Microsoft.Maui.Platform { - public partial class WrapperView : PlatformWrapperView + public partial class WrapperView : PlatformWrapperView, IInputTransparentManagingView { const int MaximumRadius = 100; diff --git a/src/Core/src/Platform/IInputTransparentManagingView.cs b/src/Core/src/Platform/IInputTransparentManagingView.cs new file mode 100644 index 000000000000..c7e5968be219 --- /dev/null +++ b/src/Core/src/Platform/IInputTransparentManagingView.cs @@ -0,0 +1,7 @@ +namespace Microsoft.Maui.Platform +{ + internal interface IInputTransparentManagingView + { + bool InputTransparent { get; set; } + } +} \ No newline at end of file diff --git a/src/Core/src/Platform/Tizen/ViewExtensions.cs b/src/Core/src/Platform/Tizen/ViewExtensions.cs index 83e917292318..b5ca1d6dd39d 100644 --- a/src/Core/src/Platform/Tizen/ViewExtensions.cs +++ b/src/Core/src/Platform/Tizen/ViewExtensions.cs @@ -333,14 +333,6 @@ internal static IDisposable OnUnloaded(this NView view, Action action) return disposable; } - internal static bool NeedsContainer(this IView? view) - { - if (view is IBorderView border) - return border?.Shape != null || border?.Stroke != null; - - return false; - } - internal static T? GetChildAt(this NView view, int index) where T : NView { return (T?)view.Children[index]; diff --git a/src/Core/src/Platform/iOS/LayoutView.cs b/src/Core/src/Platform/iOS/LayoutView.cs index 9c8aa866485a..3662681dbd6a 100644 --- a/src/Core/src/Platform/iOS/LayoutView.cs +++ b/src/Core/src/Platform/iOS/LayoutView.cs @@ -1,12 +1,9 @@ -using CoreGraphics; using UIKit; namespace Microsoft.Maui.Platform { public class LayoutView : MauiView { - bool _userInteractionEnabled; - public override void SubviewAdded(UIView uiview) { InvalidateConstraintsCache(); @@ -20,50 +17,5 @@ public override void WillRemoveSubview(UIView uiview) base.WillRemoveSubview(uiview); Superview?.SetNeedsLayout(); } - - public override UIView? HitTest(CGPoint point, UIEvent? uievent) - { - var result = base.HitTest(point, uievent); - - if (result is null) - { - return null; - } - - if (!_userInteractionEnabled && Equals(result)) - { - // If user interaction is disabled (IOW, if the corresponding Layout is InputTransparent), - // then we exclude the LayoutView itself from hit testing. But it's children are valid - // hit testing targets. - - return null; - } - - if (result is LayoutView layoutView && !layoutView.UserInteractionEnabledOverride) - { - // If the child is a layout then we need to check the UserInteractionEnabledOverride - // since layouts always have user interaction enabled. - - return null; - } - - return result; - } - - internal bool UserInteractionEnabledOverride => _userInteractionEnabled; - - public override bool UserInteractionEnabled - { - get => base.UserInteractionEnabled; - set - { - // We leave the base UIE value true no matter what, so that hit testing will find children - // of the LayoutView. But we track the intended value so we can use it during hit testing - // to ignore the LayoutView itself, if necessary. - - base.UserInteractionEnabled = true; - _userInteractionEnabled = value; - } - } } } \ No newline at end of file diff --git a/src/Core/src/Platform/iOS/MauiScrollView.cs b/src/Core/src/Platform/iOS/MauiScrollView.cs index 00c1ca3d55e8..226ae7bfb839 100644 --- a/src/Core/src/Platform/iOS/MauiScrollView.cs +++ b/src/Core/src/Platform/iOS/MauiScrollView.cs @@ -5,12 +5,14 @@ namespace Microsoft.Maui.Platform { - public class MauiScrollView : UIScrollView, IUIViewLifeCycleEvents + public class MauiScrollView : UIScrollView, IUIViewLifeCycleEvents, IInputTransparentManagingView { public MauiScrollView() { } + bool IInputTransparentManagingView.InputTransparent { get; set; } + // overriding this method so it does not automatically scroll large UITextFields // while the KeyboardAutoManagerScroll is scrolling. public override void ScrollRectToVisible(CGRect rect, bool animated) @@ -32,6 +34,34 @@ public override void MovedToWindow() base.MovedToWindow(); _movedToWindow?.Invoke(this, EventArgs.Empty); } + + public override UIView? HitTest(CGPoint point, UIEvent? uievent) + { + var result = base.HitTest(point, uievent); + + if (result is null) + { + return null; + } + + if (((IInputTransparentManagingView)this).InputTransparent && Equals(result)) + { + // If user interaction is disabled (IOW, if the corresponding View is InputTransparent), + // then we exclude the managing view itself from hit testing. But it's children are valid + // hit testing targets. + + return null; + } + + if (result is IInputTransparentManagingView v && v.InputTransparent) + { + // If the child is a managing view then we need to check the InputTransparent + // since managing view instances always have user interaction enabled. + + return null; + } + + return result; + } } } - diff --git a/src/Core/src/Platform/iOS/MauiView.cs b/src/Core/src/Platform/iOS/MauiView.cs index e125a46f5586..846638c77e67 100644 --- a/src/Core/src/Platform/iOS/MauiView.cs +++ b/src/Core/src/Platform/iOS/MauiView.cs @@ -7,7 +7,7 @@ namespace Microsoft.Maui.Platform { - public abstract class MauiView : UIView, ICrossPlatformLayoutBacking, IVisualTreeElementProvidable, IUIViewLifeCycleEvents + public abstract class MauiView : UIView, ICrossPlatformLayoutBacking, IVisualTreeElementProvidable, IUIViewLifeCycleEvents, IInputTransparentManagingView { static bool? _respondsToSafeArea; @@ -17,6 +17,8 @@ public abstract class MauiView : UIView, ICrossPlatformLayoutBacking, IVisualTre WeakReference? _reference; WeakReference? _crossPlatformLayoutReference; + bool IInputTransparentManagingView.InputTransparent { get; set; } + public IView? View { get => _reference != null && _reference.TryGetTarget(out var v) ? v : null; @@ -174,5 +176,34 @@ public override void MovedToWindow() base.MovedToWindow(); _movedToWindow?.Invoke(this, EventArgs.Empty); } + + public override UIView? HitTest(CGPoint point, UIEvent? uievent) + { + var result = base.HitTest(point, uievent); + + if (result is null) + { + return null; + } + + if (((IInputTransparentManagingView)this).InputTransparent && Equals(result)) + { + // If user interaction is disabled (IOW, if the corresponding View is InputTransparent), + // then we exclude the managing view itself from hit testing. But it's children are valid + // hit testing targets. + + return null; + } + + if (result is IInputTransparentManagingView v && v.InputTransparent) + { + // If the child is a managing view then we need to check the UserInteractionEnabledOverride + // since managing view instances always have user interaction enabled. + + return null; + } + + return result; + } } } diff --git a/src/Core/src/Platform/iOS/ViewExtensions.cs b/src/Core/src/Platform/iOS/ViewExtensions.cs index bec1da442bfc..d947adeede56 100644 --- a/src/Core/src/Platform/iOS/ViewExtensions.cs +++ b/src/Core/src/Platform/iOS/ViewExtensions.cs @@ -505,20 +505,22 @@ internal static Size LayoutToMeasuredSize(this IView view, double width, double public static void UpdateInputTransparent(this UIView platformView, IViewHandler handler, IView view) { if (view is ITextInput textInput) - { platformView.UpdateInputTransparent(textInput.IsReadOnly, view.InputTransparent); - return; - } - - platformView.UserInteractionEnabled = !view.InputTransparent; + else + platformView.UpdateInputTransparent(view.InputTransparent); } - public static void UpdateInputTransparent(this UIView platformView, bool isReadOnly, bool inputTransparent) + public static void UpdateInputTransparent(this UIView platformView, bool isReadOnly, bool inputTransparent) => + platformView.UpdateInputTransparent(isReadOnly || inputTransparent); + + internal static void UpdateInputTransparent(this UIView platformView, bool inputTransparent) { - platformView.UserInteractionEnabled = !(isReadOnly || inputTransparent); + if (platformView is IInputTransparentManagingView itmv) + itmv.InputTransparent = inputTransparent; + else + platformView.UserInteractionEnabled = !inputTransparent; } - internal static UIToolTipInteraction? GetToolTipInteraction(this UIView platformView) { UIToolTipInteraction? interaction = default; diff --git a/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt index a2e3e6026e42..9c647237fce0 100644 --- a/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -70,6 +70,7 @@ override Microsoft.Maui.PlatformAppCompatTextView.JniPeerMembers.get -> Java.Int override Microsoft.Maui.PlatformAppCompatTextView.ThresholdClass.get -> nint override Microsoft.Maui.PlatformAppCompatTextView.ThresholdType.get -> System.Type! override Microsoft.Maui.Platform.ContentViewGroup.GetClipPath(int width, int height) -> Android.Graphics.Path? +override Microsoft.Maui.Platform.ContentViewGroup.OnTouchEvent(Android.Views.MotionEvent? e) -> bool override Microsoft.Maui.Platform.MauiMaterialButton.IconGravity.get -> int override Microsoft.Maui.Platform.MauiMaterialButton.IconGravity.set -> void override Microsoft.Maui.Platform.MauiScrollView.OnMeasure(int widthMeasureSpec, int heightMeasureSpec) -> void diff --git a/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt index 38dcd185dac6..a6475d3b3312 100644 --- a/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -158,5 +158,10 @@ Microsoft.Maui.Platform.MauiView.IsMeasureValid(double widthConstraint, double h *REMOVED*override Microsoft.Maui.Platform.ContentView.SetNeedsLayout() -> void Microsoft.Maui.Platform.UIEdgeInsetsExtensions static Microsoft.Maui.Platform.UIEdgeInsetsExtensions.ToThickness(this UIKit.UIEdgeInsets insets) -> Microsoft.Maui.Thickness +*REMOVED*override Microsoft.Maui.Platform.LayoutView.UserInteractionEnabled.get -> bool +*REMOVED*override Microsoft.Maui.Platform.LayoutView.UserInteractionEnabled.set -> void +*REMOVED*override Microsoft.Maui.Platform.LayoutView.HitTest(CoreGraphics.CGPoint point, UIKit.UIEvent? uievent) -> UIKit.UIView? +override Microsoft.Maui.Platform.MauiView.HitTest(CoreGraphics.CGPoint point, UIKit.UIEvent? uievent) -> UIKit.UIView? +override Microsoft.Maui.Platform.MauiScrollView.HitTest(CoreGraphics.CGPoint point, UIKit.UIEvent? uievent) -> UIKit.UIView? *REMOVED*override Microsoft.Maui.Handlers.BorderHandler.PlatformArrange(Microsoft.Maui.Graphics.Rect rect) -> void -*REMOVED*override Microsoft.Maui.Platform.MauiLabel.InvalidateIntrinsicContentSize() -> void \ No newline at end of file +*REMOVED*override Microsoft.Maui.Platform.MauiLabel.InvalidateIntrinsicContentSize() -> void diff --git a/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt index 870fac341c6e..f58b7c3dc2d8 100644 --- a/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -160,3 +160,8 @@ virtual Microsoft.Maui.MauiUIApplicationDelegate.PerformFetch(UIKit.UIApplicatio Microsoft.Maui.Platform.UIEdgeInsetsExtensions static Microsoft.Maui.Platform.UIEdgeInsetsExtensions.ToThickness(this UIKit.UIEdgeInsets insets) -> Microsoft.Maui.Thickness *REMOVED*override Microsoft.Maui.Platform.MauiLabel.InvalidateIntrinsicContentSize() -> void +*REMOVED*override Microsoft.Maui.Platform.LayoutView.UserInteractionEnabled.get -> bool +*REMOVED*override Microsoft.Maui.Platform.LayoutView.UserInteractionEnabled.set -> void +*REMOVED*override Microsoft.Maui.Platform.LayoutView.HitTest(CoreGraphics.CGPoint point, UIKit.UIEvent? uievent) -> UIKit.UIView? +override Microsoft.Maui.Platform.MauiView.HitTest(CoreGraphics.CGPoint point, UIKit.UIEvent? uievent) -> UIKit.UIView? +override Microsoft.Maui.Platform.MauiScrollView.HitTest(CoreGraphics.CGPoint point, UIKit.UIEvent? uievent) -> UIKit.UIView? diff --git a/src/Core/src/ViewExtensions.cs b/src/Core/src/ViewExtensions.cs index 75cc700f25f7..5fde0953aab7 100644 --- a/src/Core/src/ViewExtensions.cs +++ b/src/Core/src/ViewExtensions.cs @@ -48,27 +48,34 @@ public static partial class ViewExtensions await Screenshot.Default.CaptureAsync(window); #endif -#if !TIZEN - internal static bool NeedsContainer(this IView? view) + internal static bool NeedsContainer(this IView? view, PlatformView? platformView) { +#if !TIZEN if (view?.Clip != null || view?.Shadow != null) return true; +#endif #if ANDROID - if (view?.InputTransparent == true) + // This is only here for Android because almost all Android views will require + // a wrapper when the view is InputTransparent. This is because Android does not + // have a concept of "not hit testable" so we have to emulate it intercepting the + // the touch events with a parent layout. + if (view?.InputTransparent == true && platformView is not IInputTransparentManagingView) return true; #endif #if ANDROID || IOS if (view is IBorder border && border.Border != null) return true; -#elif WINDOWS +#endif + +#if WINDOWS || TIZEN if (view is IBorderView border) return border?.Shape != null || border?.Stroke != null; #endif + return false; } -#endif } } diff --git a/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBaseOfT.Android.cs b/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBaseOfT.Android.cs index 952ec19ac0de..4ca64f60f45a 100644 --- a/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBaseOfT.Android.cs +++ b/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBaseOfT.Android.cs @@ -164,8 +164,13 @@ public async Task NeedsContainerWhenInputTransparent() var handler = await CreateHandlerAsync(view); - if (handler is ViewHandler vh) - Assert.True(vh.NeedsContainer); + if (handler is not ViewHandler vh) + return; + + if (handler.PlatformView is IInputTransparentManagingView) + Assert.False(vh.NeedsContainer, $"{view.GetType().Name} should NOT need a container because it uses a IInputTransparentManagingView platform view."); + else + Assert.True(vh.NeedsContainer, $"{view.GetType().Name} SHOULD need a container because it does NOT use a IInputTransparentManagingView platform view."); } } } \ No newline at end of file diff --git a/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBasementOfT.iOS.cs b/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBasementOfT.iOS.cs index 7cd9ba8a8305..5175a81c22d2 100644 --- a/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBasementOfT.iOS.cs +++ b/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBasementOfT.iOS.cs @@ -102,6 +102,8 @@ protected Visibility GetVisibility(IViewHandler viewHandler) protected bool GetUserInteractionEnabled(IViewHandler viewHandler) { var platformView = (UIView)viewHandler.PlatformView; + if (platformView is IInputTransparentManagingView maui) + return !maui.InputTransparent; return platformView.UserInteractionEnabled; } diff --git a/src/Core/tests/DeviceTests/Handlers/ContentView/ContentViewTests.iOS.cs b/src/Core/tests/DeviceTests/Handlers/ContentView/ContentViewTests.iOS.cs index 956cc11f764b..7d04d7eebce0 100644 --- a/src/Core/tests/DeviceTests/Handlers/ContentView/ContentViewTests.iOS.cs +++ b/src/Core/tests/DeviceTests/Handlers/ContentView/ContentViewTests.iOS.cs @@ -5,7 +5,6 @@ namespace Microsoft.Maui.DeviceTests.Handlers.ContentView { - [Category(TestCategory.ContentView)] public partial class ContentViewTests { [Fact, Category(TestCategory.FlowDirection)]