From f7c943dc7798b3449d2bf8319aca8a9ab448ffec Mon Sep 17 00:00:00 2001 From: "E.Z. Hart" Date: Tue, 20 Jun 2017 09:38:58 -0600 Subject: [iOS] Allow Forms gestures on custom renderers for controls which already have gestures (#990) * Repro 57114 with UI test; fix for 57114 on iOS * Repro/UI test for Windows * Add helpful comment for posterity * Remove stray TODO * Only do ShouldReceiveTouch on mobile * Explicitly require wrapped UIView to have gesture recognizers --- .../Xamarin.Forms.ControlGallery.Android.csproj | 1 + .../_57114CustomRenderer.cs | 42 ++++++++ ...in.Forms.ControlGallery.WindowsUniversal.csproj | 1 + .../_57114Renderer.cs | 48 +++++++++ .../Xamarin.Forms.ControlGallery.iOS.csproj | 1 + Xamarin.Forms.ControlGallery.iOS/_57114Renderer.cs | 44 +++++++++ .../Bugzilla57114.cs | 110 +++++++++++++++++++++ .../Xamarin.Forms.Controls.Issues.Shared.projitems | 1 + .../UITestCategories.cs | 1 + Xamarin.Forms.Platform.iOS/EventTracker.cs | 42 +++++++- 10 files changed, 286 insertions(+), 5 deletions(-) create mode 100644 Xamarin.Forms.ControlGallery.Android/_57114CustomRenderer.cs create mode 100644 Xamarin.Forms.ControlGallery.WindowsUniversal/_57114Renderer.cs create mode 100644 Xamarin.Forms.ControlGallery.iOS/_57114Renderer.cs create mode 100644 Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla57114.cs diff --git a/Xamarin.Forms.ControlGallery.Android/Xamarin.Forms.ControlGallery.Android.csproj b/Xamarin.Forms.ControlGallery.Android/Xamarin.Forms.ControlGallery.Android.csproj index e8011acb..473ae27f 100644 --- a/Xamarin.Forms.ControlGallery.Android/Xamarin.Forms.ControlGallery.Android.csproj +++ b/Xamarin.Forms.ControlGallery.Android/Xamarin.Forms.ControlGallery.Android.csproj @@ -182,6 +182,7 @@ + diff --git a/Xamarin.Forms.ControlGallery.Android/_57114CustomRenderer.cs b/Xamarin.Forms.ControlGallery.Android/_57114CustomRenderer.cs new file mode 100644 index 00000000..b4952d02 --- /dev/null +++ b/Xamarin.Forms.ControlGallery.Android/_57114CustomRenderer.cs @@ -0,0 +1,42 @@ +using System; +using Android.Content; +using Android.OS; +using Xamarin.Forms; +using Xamarin.Forms.ControlGallery.Android; +using Xamarin.Forms.Controls.Issues; +using Xamarin.Forms.Platform.Android; +using AView = Android.Views.View; + +[assembly: ExportRenderer(typeof(Bugzilla57114._57114View), typeof(_57114CustomRenderer))] + +namespace Xamarin.Forms.ControlGallery.Android +{ + public class _57114CustomRenderer : Platform.Android.AppCompat.ViewRenderer + { + protected override void OnElementChanged(ElementChangedEventArgs e) + { + if (e.NewElement != null && Control == null) + { + var view = new _57114NativeView(Context); + SetNativeControl(view); + } + + base.OnElementChanged(e); + } + } + + public class _57114NativeView : global::Android.Views.View + { + public _57114NativeView(Context context) + : base(context) + { + Touch += OnTouch; + } + + void OnTouch(object sender, AView.TouchEventArgs e) + { + MessagingCenter.Send(this as object, Bugzilla57114._57114NativeGestureFiredMessage); + e.Handled = false; + } + } +} \ No newline at end of file diff --git a/Xamarin.Forms.ControlGallery.WindowsUniversal/Xamarin.Forms.ControlGallery.WindowsUniversal.csproj b/Xamarin.Forms.ControlGallery.WindowsUniversal/Xamarin.Forms.ControlGallery.WindowsUniversal.csproj index 323d2fd8..666cbe76 100644 --- a/Xamarin.Forms.ControlGallery.WindowsUniversal/Xamarin.Forms.ControlGallery.WindowsUniversal.csproj +++ b/Xamarin.Forms.ControlGallery.WindowsUniversal/Xamarin.Forms.ControlGallery.WindowsUniversal.csproj @@ -120,6 +120,7 @@ + diff --git a/Xamarin.Forms.ControlGallery.WindowsUniversal/_57114Renderer.cs b/Xamarin.Forms.ControlGallery.WindowsUniversal/_57114Renderer.cs new file mode 100644 index 00000000..94032094 --- /dev/null +++ b/Xamarin.Forms.ControlGallery.WindowsUniversal/_57114Renderer.cs @@ -0,0 +1,48 @@ +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; +using Xamarin.Forms.ControlGallery.WindowsUniversal; +using Xamarin.Forms.Controls.Issues; +using Xamarin.Forms.Platform.UWP; + +[assembly: ExportRenderer(typeof(Bugzilla57114._57114View), typeof(_57114Renderer))] + +namespace Xamarin.Forms.ControlGallery.WindowsUniversal +{ + public class _57114Renderer : VisualElementRenderer + { + protected override void OnElementChanged(ElementChangedEventArgs e) + { + if (e.NewElement != null && Control == null) + { + var view = new _57114NativeView(); + SetNativeControl(view); + + } + + base.OnElementChanged(e); + + if (Control != null) + { + Control.Background = ColorToBrush(Element.BackgroundColor); + } + } + + Brush ColorToBrush(Color color) + { + return new SolidColorBrush(Windows.UI.Color.FromArgb((byte)(color.A * 255), (byte)(color.R * 255), (byte)(color.G * 255), (byte)(color.B * 255))); + } + } + + public class _57114NativeView : Windows.UI.Xaml.Controls.Grid + { + public _57114NativeView() + { + Tapped += OnTapped; + } + + void OnTapped(object sender, TappedRoutedEventArgs tappedRoutedEventArgs) + { + MessagingCenter.Send(this as object, Bugzilla57114._57114NativeGestureFiredMessage); + } + } +} \ No newline at end of file diff --git a/Xamarin.Forms.ControlGallery.iOS/Xamarin.Forms.ControlGallery.iOS.csproj b/Xamarin.Forms.ControlGallery.iOS/Xamarin.Forms.ControlGallery.iOS.csproj index 00d23ecc..9ed9b17b 100644 --- a/Xamarin.Forms.ControlGallery.iOS/Xamarin.Forms.ControlGallery.iOS.csproj +++ b/Xamarin.Forms.ControlGallery.iOS/Xamarin.Forms.ControlGallery.iOS.csproj @@ -164,6 +164,7 @@ + diff --git a/Xamarin.Forms.ControlGallery.iOS/_57114Renderer.cs b/Xamarin.Forms.ControlGallery.iOS/_57114Renderer.cs new file mode 100644 index 00000000..f246fa74 --- /dev/null +++ b/Xamarin.Forms.ControlGallery.iOS/_57114Renderer.cs @@ -0,0 +1,44 @@ +using System; +using Foundation; +using UIKit; +using Xamarin.Forms; +using Xamarin.Forms.ControlGallery.iOS; +using Xamarin.Forms.Controls.Issues; +using Xamarin.Forms.Platform.iOS; + +[assembly: ExportRenderer(typeof(Bugzilla57114._57114View), typeof(_57114Renderer))] + +namespace Xamarin.Forms.ControlGallery.iOS +{ + public class _57114Renderer : ViewRenderer + { + protected override void OnElementChanged(ElementChangedEventArgs e) + { + if (e.NewElement != null && Control == null) + { + var view = new _57114NativeView(); + SetNativeControl(view); + } + + base.OnElementChanged(e); + } + } + + public class _57114NativeView : UIView, IUIGestureRecognizerDelegate + { + public _57114NativeView() + { + var rec = new CustomGestureRecognizer(); + AddGestureRecognizer(rec); + } + } + + public class CustomGestureRecognizer : UIGestureRecognizer + { + public override void TouchesBegan(NSSet touches, UIEvent evt) + { + base.TouchesBegan(touches, evt); + MessagingCenter.Instance.Send(this as object, Bugzilla57114._57114NativeGestureFiredMessage); + } + } +} \ No newline at end of file diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla57114.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla57114.cs new file mode 100644 index 00000000..353939ea --- /dev/null +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla57114.cs @@ -0,0 +1,110 @@ +using System; +using System.Diagnostics; + +using Xamarin.Forms.CustomAttributes; +using Xamarin.Forms.Internals; + +#if UITEST +using Xamarin.Forms.Core.UITests; +using Xamarin.UITest; +using NUnit.Framework; +#endif + +namespace Xamarin.Forms.Controls.Issues +{ +#if UITEST + [Category(UITestCategories.Gestures)] +#endif + + [Preserve(AllMembers = true)] + [Issue(IssueTracker.Bugzilla, 57114, "Forms gestures are not supported on UIViews that have native gestures", PlatformAffected.iOS)] + public class Bugzilla57114 : TestContentPage + { + public static string _57114NativeGestureFiredMessage = "_57114NativeGestureFiredMessage"; + + Label _results; + bool _nativeGestureFired; + bool _formsGestureFired; + + const string Testing = "Testing..."; + const string Success = "Success"; + const string AutomationId = "_57114View"; + + protected override void Init() + { + var instructions = new Label + { + Text = $"Tap the Aqua View below. If the label below changes from '{Testing}' to '{Success}', the test has passed." + }; + + _results = new Label { Text = Testing }; + + var view = new _57114View + { + AutomationId = AutomationId, + HeightRequest = 200, WidthRequest = 200, + BackgroundColor = Color.Aqua, + HorizontalOptions = LayoutOptions.Fill, + VerticalOptions = LayoutOptions.Fill + }; + + var tap = new TapGestureRecognizer + { + Command = new Command(() => + { + _formsGestureFired = true; + UpdateResults(); + }) + }; + + MessagingCenter.Subscribe(this, _57114NativeGestureFiredMessage, NativeGestureFired); + + view.GestureRecognizers.Add(tap); + + var layout = new StackLayout() + { + HorizontalOptions = LayoutOptions.Fill, + VerticalOptions = LayoutOptions.Fill, + Children = + { + instructions, _results, view + } + }; + + Content = layout; + } + + void NativeGestureFired(object obj) + { + _nativeGestureFired = true; + UpdateResults(); + } + + void UpdateResults() + { + if (_nativeGestureFired && _formsGestureFired) + { + _results.Text = Success; + } + else + { + _results.Text = Testing; + } + } + + [Preserve(AllMembers = true)] + public class _57114View : View + { + } + +#if UITEST + [Test] + public void _57114BothTypesOfGesturesFire() + { + RunningApp.WaitForElement(Testing); + RunningApp.Tap(AutomationId); + RunningApp.WaitForElement(Success); + } +#endif + } +} \ No newline at end of file diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems index 97f26f7f..62b97166 100644 --- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems @@ -208,6 +208,7 @@ + diff --git a/Xamarin.Forms.Core.UITests.Shared/UITestCategories.cs b/Xamarin.Forms.Core.UITests.Shared/UITestCategories.cs index c5168d7c..cca59b5a 100644 --- a/Xamarin.Forms.Core.UITests.Shared/UITestCategories.cs +++ b/Xamarin.Forms.Core.UITests.Shared/UITestCategories.cs @@ -32,6 +32,7 @@ public const string Maps = "Maps"; public const string InputTransparent = "InputTransparent"; public const string IsEnabled = "IsEnabled"; + public const string Gestures = "Gestures"; public const string ManualReview = "ManualReview"; } diff --git a/Xamarin.Forms.Platform.iOS/EventTracker.cs b/Xamarin.Forms.Platform.iOS/EventTracker.cs index 48f30648..5b2156c9 100644 --- a/Xamarin.Forms.Platform.iOS/EventTracker.cs +++ b/Xamarin.Forms.Platform.iOS/EventTracker.cs @@ -33,7 +33,7 @@ namespace Xamarin.Forms.Platform.MacOS double _previousScale = 1.0; #if __MOBILE__ - UITouchEventArgs _shouldReceive; + UITouchEventArgs _shouldReceiveTouch; #endif public EventTracker(IVisualElementRenderer renderer) @@ -82,9 +82,7 @@ namespace Xamarin.Forms.Platform.MacOS { if (_disposed) throw new ObjectDisposedException(null); -#if __MOBILE__ - _shouldReceive = (r, t) => t.View is IVisualElementRenderer; -#endif + _handler = handler; OnElementChanged(this, new VisualElementChangedEventArgs(null, _renderer.Element)); } @@ -281,11 +279,20 @@ namespace Xamarin.Forms.Platform.MacOS return result; } #endif + void LoadRecognizers() { if (ElementGestureRecognizers == null) return; +#if __MOBILE__ + if (_shouldReceiveTouch == null) + { + // Cache this so we don't create a new UITouchEventArgs instance for every recognizer + _shouldReceiveTouch = ShouldReceiveTouch; + } +#endif + foreach (var recognizer in ElementGestureRecognizers) { if (_gestureRecognizers.ContainsKey(recognizer)) @@ -295,7 +302,7 @@ namespace Xamarin.Forms.Platform.MacOS if (nativeRecognizer != null) { #if __MOBILE__ - nativeRecognizer.ShouldReceiveTouch = _shouldReceive; + nativeRecognizer.ShouldReceiveTouch = _shouldReceiveTouch; #endif _handler.AddGestureRecognizer(nativeRecognizer); @@ -314,6 +321,31 @@ namespace Xamarin.Forms.Platform.MacOS } } +#if __MOBILE__ + bool ShouldReceiveTouch(UIGestureRecognizer recognizer, UITouch touch) + { + if (touch.View is IVisualElementRenderer) + { + return true; + } + + // If the touch is coming from the UIView our renderer is wrapping (e.g., if it's + // wrapping a UIView which already has a gesture recognizer), then we should let it through + // (This goes for children of that control as well) + if (_renderer?.NativeView == null) + { + return false; + } + + if (touch.View.IsDescendantOfView(_renderer.NativeView) && touch.View.GestureRecognizers?.Length > 0) + { + return true; + } + + return false; + } +#endif + void ModelGestureRecognizersOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs) { LoadRecognizers(); -- cgit v1.2.3