Skip to content

Commit

Permalink
Implement touch interception on Android
Browse files Browse the repository at this point in the history
  • Loading branch information
mattleibow committed Nov 9, 2023
1 parent 3c3057b commit 043035b
Show file tree
Hide file tree
Showing 19 changed files with 747 additions and 105 deletions.

Large diffs are not rendered by default.

36 changes: 33 additions & 3 deletions src/Controls/samples/Controls.Sample.UITests/Test.cs
Expand Up @@ -713,13 +713,43 @@ public enum CarouselView

public enum InputTransparency
{
Default,
IsFalse,
IsTrue,
// Single Control
ButtonNotSet,
Button,
TransButtonInputBlocked,
// Button
ButtonOverlay,
TransButtonOverlay,
// Image
ImageOverlayInputBlocked,
TransImageOverlay,
ImageBackOverlayInputBlocked,
TransImageBackOverlay,
// Label
LabelOverlayInputBlocked,
TransLabelOverlay,
// ActivityIndicator
ActivityIndicatorOverlayInputBlocked,
TransActivityIndicatorOverlay,
// ProgressBar
ProgressBarOverlayInputBlocked,
TransProgressBarOverlay,
// Layout
LayoutOverlayInputBlocked,
TransLayoutOverlay,
TransLayoutOverlayWithButton,
CascadeTransLayoutOverlay,
CascadeTransLayoutOverlayWithButton,
// CollectionView
CollectionViewItemLayoutOverlay,
CollectionViewItemTransLayoutOverlay,
CollectionViewItemButtonOverlay,
CollectionViewItemTransButtonOverlay,
// ListView
ListViewItemLayoutOverlay,
ListViewItemTransLayoutOverlay,
ListViewItemButtonOverlay,
ListViewItemTransButtonOverlay,
}

public static class InputTransparencyMatrix
Expand Down
Expand Up @@ -61,11 +61,10 @@

<Grid Margin="10" HeightRequest="100" BackgroundColor="LightBlue">

<Button Text="Bottom Button" IsVisible="{Binding InputTransparent, Source={Reference testButton}}" Clicked="ClickSuccess" HorizontalOptions="Center" VerticalOptions="Center" />
<Button Text="Bottom Button" IsVisible="{Binding InputTransparent, Source={Reference testButton}, Converter={StaticResource NegativeConverter}}" Clicked="ClickFail" HorizontalOptions="Center" VerticalOptions="Center" />
<Button Text="Bottom Button" Clicked="ClickBottom" HorizontalOptions="Center" VerticalOptions="Center" Background="Red" />

<Grid x:Name="rootLayout">
<Grid x:Name="nestedLayout">
<Grid x:Name="nestedLayout" ColumnDefinitions="2*,1*">
<Button x:Name="testButton" Text="Test Button" Clicked="ClickSuccess" HorizontalOptions="Center" VerticalOptions="Center" />
</Grid>
</Grid>
Expand Down
Expand Up @@ -22,5 +22,11 @@ void ClickSuccess(object sender, EventArgs e)
Debug.WriteLine("Success; That should have worked, and it did!");
DisplayAlert("Success", "That should have worked, and it did!", "OK");
}

void ClickBottom(object sender, EventArgs e)
{
Debug.WriteLine("Click; You clicked a bottom button.");
DisplayAlert("Click", "You clicked a bottom button.", "OK");
}
}
}
Expand Up @@ -20,7 +20,8 @@ protected override void NavigateToGallery()
}

[Test]
public void Simple([Values] Test.InputTransparency test) => RunTest(test.ToString());
public void Simple([Values] Test.InputTransparency test) =>
RunTest(test.ToString(), !test.ToString().EndsWith("InputBlocked"));

[Test]
[Combinatorial]
Expand All @@ -29,10 +30,10 @@ public void Matrix([Values] bool rootTrans, [Values] bool rootCascade, [Values]
var (clickable, passthru) = Test.InputTransparencyMatrix.States[(rootTrans, rootCascade, nestedTrans, nestedCascade, trans)];
var key = Test.InputTransparencyMatrix.GetKey(rootTrans, rootCascade, nestedTrans, nestedCascade, trans, clickable, passthru);

RunTest(key, clickable, passthru);
RunTest(key, clickable || passthru);
}

void RunTest(string test, bool? clickable = null, bool? passthru = null)
static void RunTest(string test, bool updatable)
{
var remote = new EventViewContainerRemote(UITestContext, test);
remote.GoTo(test.ToString());
Expand All @@ -44,23 +45,11 @@ void RunTest(string test, bool? clickable = null, bool? passthru = null)

var textAfterClick = remote.GetEventLabel().GetText();

if (clickable is null || passthru is null)
{
// some tests are really basic so have no need for fancy checks
Assert.AreEqual($"Event: {test} (SUCCESS 1)", textAfterClick);
}
else if (clickable == true || passthru == true)
if (updatable)
{
// if the button is clickable or taps pass through to the base button
Assert.AreEqual($"Event: {test} (SUCCESS 1)", textAfterClick);
}
else if (Device == TestDevice.Android)
{
// TODO: Android is broken with everything passing through so we just use that
// to test the bottom button was clickable
// https://github.com/dotnet/maui/issues/10252
Assert.AreEqual($"Event: {test} (SUCCESS 1)", textAfterClick);
}
else
{
// sometimes nothing can happen, so try test that
Expand Down
45 changes: 44 additions & 1 deletion src/Controls/tests/UITests/Tests/_ViewUITests.cs
Expand Up @@ -51,6 +51,49 @@ public virtual void _IsVisible()

Assert.AreEqual(0, viewPost.Count);
}
}

[Test]
public virtual void _InputTransparent()
{
var remote = new LayeredViewContainerRemote (UITestContext, Test.VisualElement.InputTransparent);
remote.GoTo ();

var hiddenButtonClickedLabelTextPre = remote.GetLayeredLabel ().Text;
Assert.AreEqual ("Hidden Button (Not Clicked)", hiddenButtonClickedLabelTextPre);

remote.TapHiddenButton ();

var hiddenButtonClickedLabelTextPost = remote.GetLayeredLabel ().Text;
var hiddenButtonClicked = hiddenButtonClickedLabelTextPost == "Hidden Button (Clicked)";

// // Allow tests to continue by dismissing DatePicker that should not show
// // Remove when InputTransparency works
// if (!hiddenButtonClicked && PlatformViewType == PlatformViews.DatePicker)
// remote.DismissPopOver ();

Assert.True (hiddenButtonClicked);
}

[Test]
public virtual void _NotInputTransparent()
{
var remote = new LayeredViewContainerRemote (UITestContext, Test.VisualElement.NotInputTransparent);
remote.GoTo ();

var hiddenButtonClickedLabelTextPre = remote.GetLayeredLabel ().Text;
Assert.AreEqual ("Hidden Button (Not Clicked)", hiddenButtonClickedLabelTextPre);

remote.TapHiddenButton ();

var hiddenButtonClickedLabelTextPost = remote.GetLayeredLabel ().Text;
var hiddenButtonClicked = hiddenButtonClickedLabelTextPost == "Hidden Button (Not Clicked)";

// // Allow tests to continue by dismissing DatePicker that should not show
// // Remove when InputTransparency works
// if (!hiddenButtonClicked && PlatformViewType == PlatformViews.DatePicker)
// remote.DismissPopOver ();

Assert.True (hiddenButtonClicked);
}
}
}
3 changes: 2 additions & 1 deletion src/Core/src/Handlers/Image/ImageHandler.Android.cs
@@ -1,5 +1,6 @@
using System.Threading.Tasks;
using Android.Graphics.Drawables;
using Android.Views;
using Android.Widget;
using AndroidX.AppCompat.Widget;
using Google.Android.Material.Button;
Expand All @@ -10,7 +11,7 @@ public partial class ImageHandler : ViewHandler<IImage, ImageView>
{
protected override ImageView CreatePlatformView()
{
var imageView = new AppCompatImageView(Context);
var imageView = new MauiImageView(Context);

// Enable view bounds adjustment on measure.
// This allows the ImageView's OnMeasure method to account for the image's intrinsic
Expand Down
9 changes: 2 additions & 7 deletions src/Core/src/Handlers/Layout/LayoutHandler.Android.cs
Expand Up @@ -153,12 +153,7 @@ static int IndexOf(LayoutViewGroup viewGroup, AView view)
handler.PlatformView?.UpdateBackground(layout);
}

public static partial void MapInputTransparent(ILayoutHandler handler, ILayout layout)
{
if (handler.PlatformView is LayoutViewGroup layoutViewGroup)
{
layoutViewGroup.InputTransparent = layout.InputTransparent;
}
}
public static partial void MapInputTransparent(ILayoutHandler handler, ILayout layout) =>
ViewHandler.MapInputTransparent(handler, layout);
}
}
14 changes: 4 additions & 10 deletions src/Core/src/Handlers/View/ViewHandler.cs
Expand Up @@ -127,7 +127,7 @@ public bool HasContainer
/// </summary>
public virtual bool NeedsContainer
{
get => VirtualView.NeedsContainer();
get => VirtualView.NeedsContainer(this);
}

/// <summary>
Expand Down Expand Up @@ -416,7 +416,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);
}

/// <summary>
Expand Down Expand Up @@ -481,20 +481,14 @@ public static void MapInputTransparent(IViewHandler handler, IView view)
{
#if ANDROID
handler.UpdateValue(nameof(IViewHandler.ContainerView));
#endif

if (handler.ContainerView is WrapperView wrapper)
wrapper.InputTransparent = view.InputTransparent;
#else

#if IOS || MACCATALYST
// Containers on iOS/Mac Catalyst may be hit testable, so we need to
// Containers may also need to be hit testable, so we need to
// propagate the the view's values to its container view.
if (handler.ContainerView is WrapperView wrapper)
wrapper.UpdateInputTransparent(handler, view);
#endif

((PlatformView?)handler.PlatformView)?.UpdateInputTransparent(handler, view);
#endif
}

/// <summary>
Expand Down
25 changes: 17 additions & 8 deletions src/Core/src/Platform/Android/LayoutViewGroup.cs
Expand Up @@ -11,7 +11,7 @@

namespace Microsoft.Maui.Platform
{
public class LayoutViewGroup : ViewGroup, ICrossPlatformLayoutBacking, IVisualTreeElementProvidable
public class LayoutViewGroup : ViewGroup, ICrossPlatformLayoutBacking, IVisualTreeElementProvidable, ITouchInterceptingView, IInputTransparentCapable
{
readonly ARect _clipRect = new();
readonly Context _context;
Expand Down Expand Up @@ -122,16 +122,25 @@ protected override void OnLayout(bool changed, int l, int t, int r, int b)
}
}

public override bool OnTouchEvent(MotionEvent? e)
{
if (InputTransparent)
{
return false;
}
WeakReference<IOnTouchListener>? _touchListener;

return base.OnTouchEvent(e);
public override void SetOnTouchListener(IOnTouchListener? l)
{
_touchListener = l is null ? null : new(l);
base.SetOnTouchListener(l);
}

bool ITouchInterceptingView.TouchEventNotReallyHandled { get; set; }

public override bool OnTouchEvent(MotionEvent? e) =>
base.OnTouchEvent(e) ||
TouchEventInterceptor.OnTouchEvent(this, e);

public override bool DispatchTouchEvent(MotionEvent? e) =>
TouchEventInterceptor.DispatchingTouchEvent(this, e) &&
base.DispatchTouchEvent(e) &&
TouchEventInterceptor.DispatchedTouchEvent(this, e, _touchListener?.GetTargetOrDefault());

IVisualTreeElement? IVisualTreeElementProvidable.GetElement()
{
if (CrossPlatformLayout is IVisualTreeElement layoutElement &&
Expand Down
19 changes: 19 additions & 0 deletions src/Core/src/Platform/Android/MauiImageView.cs
@@ -0,0 +1,19 @@
using Android.Content;
using Android.Views;
using AndroidX.AppCompat.Widget;

namespace Microsoft.Maui.Platform
{
class MauiImageView : AppCompatImageView, IInputTransparentCapable
{
public MauiImageView(Context context) : base(context)
{
}

bool IInputTransparentCapable.InputTransparent { get; set; }

public override bool OnTouchEvent(MotionEvent? e) =>
base.OnTouchEvent(e) ||
TouchEventInterceptor.OnTouchEvent(this, e);
}
}
9 changes: 8 additions & 1 deletion src/Core/src/Platform/Android/MauiTextView.cs
@@ -1,15 +1,18 @@
using System;
using Android.Content;
using Android.Views;
using AndroidX.AppCompat.Widget;

namespace Microsoft.Maui.Platform
{
public class MauiTextView : AppCompatTextView
public class MauiTextView : AppCompatTextView, IInputTransparentCapable
{
public MauiTextView(Context context) : base(context)
{
}

bool IInputTransparentCapable.InputTransparent { get; set; }

internal event EventHandler<LayoutChangedEventArgs>? LayoutChanged;

protected override void OnLayout(bool changed, int l, int t, int r, int b)
Expand All @@ -18,6 +21,10 @@ protected override void OnLayout(bool changed, int l, int t, int r, int b)

LayoutChanged?.Invoke(this, new LayoutChangedEventArgs(l, t, r, b));
}

public override bool OnTouchEvent(MotionEvent? e) =>
base.OnTouchEvent(e) ||
TouchEventInterceptor.OnTouchEvent(this, e);
}

public class LayoutChangedEventArgs : EventArgs
Expand Down

0 comments on commit 043035b

Please sign in to comment.