diff options
11 files changed, 299 insertions, 2 deletions
diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/GestureBubblingTests.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/GestureBubblingTests.cs new file mode 100644 index 00000000..145fbf80 --- /dev/null +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/GestureBubblingTests.cs @@ -0,0 +1,211 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Xamarin.Forms.CustomAttributes; +using Xamarin.Forms.Internals; + + +#if UITEST +using NUnit.Framework; +using Xamarin.UITest.Queries; +#endif + +namespace Xamarin.Forms.Controls.Issues +{ + // This is similar to the test for 35477, but tests all of the basic controls to make sure that they all exhibit + // the same behavior across all the platforms. The question is whether tapping a control inside of a frame + // will trigger the frame's tap gesture; for most controls it will not (the control itself absorbs the tap), + // but for non-interactive controls (box, frame, image, label) the gesture bubbles up to the container. + + [Preserve(AllMembers = true)] + [Issue(IssueTracker.None, 00100100, "Verify that the tap gesture bubbling behavior is consistent across the platforms", PlatformAffected.All)] + public class GestureBubblingTests : TestNavigationPage + { + const string TargetAutomationId = "controlinsideofframe"; + ContentPage _menu; + +#if UITEST + [Test, TestCaseSource(nameof(TestCases))] + public void VerifyTapBubbling(string menuItem, bool frameShouldRegisterTap) + { + var results = RunningApp.WaitForElement(q => q.Marked(menuItem)); + + if (results.Length > 1) + { + var rect = results.First(r => r.Class.Contains("Button")).Rect; + + RunningApp.TapCoordinates(rect.CenterX, rect.CenterY); + } + else + { + RunningApp.Tap(q => q.Marked(menuItem)); + } + + // Find the start label + RunningApp.WaitForElement(q => q.Marked("Start")); + + // Find the control we're testing + var result = RunningApp.WaitForElement(q => q.Marked(TargetAutomationId)); + var target = result.First().Rect; + + // Tap the control + var y = target.CenterY; + var x = target.CenterX; + + // In theory we want to tap the center of the control. But Stepper lays out differently than the other controls, + // so we need to adjust for it until someone fixes it + if (menuItem == "Stepper") + { + y = target.Y + 5; + x = target.X + 5; + } + + RunningApp.TapCoordinates(x, y); + + if (menuItem == nameof(DatePicker) || menuItem == nameof(TimePicker)) + { + // These controls show a pop-up which we have to cancel/done out of before we can continue +#if __ANDROID__ + var cancelButtonText = "Cancel"; +#elif __IOS__ + var cancelButtonText = "Done"; +#else + var cancelButtonText = "X"; +#endif + RunningApp.WaitForElement(q => q.Marked(cancelButtonText)); + RunningApp.Tap(q => q.Marked(cancelButtonText)); + } + + if (frameShouldRegisterTap) + { + RunningApp.WaitForElement(q => q.Marked("Frame was tapped")); + } + else + { + RunningApp.WaitForElement(q => q.Marked("Start")); + } + } +#endif + + ContentPage CreateTestPage(View view) + { + var instructions = new Label(); + + if (_controlsWhichShouldAllowTheTapToBubbleUp.Contains(view.GetType().Name)) + { + instructions.Text = + "Tap the frame below. The label with the text 'No taps yet' should change its text to 'Frame was tapped'."; + } + else + { + instructions.Text = + "Tap the frame below. The label with the text 'No taps yet' should not change."; + } + + var label = new Label { Text = "Start" }; + + var frame = new Frame { Content = new StackLayout { Children = { view } } }; + + var rec = new TapGestureRecognizer { NumberOfTapsRequired = 1 }; + rec.Tapped += (s, e) => { label.Text = "Frame was tapped"; }; + frame.GestureRecognizers.Add(rec); + + var layout = new StackLayout(); + + layout.Children.Add(instructions); + layout.Children.Add(label); + layout.Children.Add(frame); + + return new ContentPage { Content = layout }; + } + + Button MenuButton(string label, Func<View> view) + { + var button = new Button { Text = label }; + + var testView = view(); + testView.AutomationId = TargetAutomationId; + + button.Clicked += (sender, args) => PushAsync(CreateTestPage(testView)); + + return button; + } + + // These controls should allow the tap gesture to bubble up to their container; everything else should absorb the gesture + readonly List<string> _controlsWhichShouldAllowTheTapToBubbleUp = new List<string> + { + nameof(Image), + nameof(Label), + nameof(BoxView), + nameof(Frame) + }; + + IEnumerable<object[]> TestCases + { + get + { + var layout = BuildMenu().Content as Layout; + var result = + from Layout element in layout.InternalChildren + from Button button in element.InternalChildren + let text = button.Text + select new object[] + { + text, + _controlsWhichShouldAllowTheTapToBubbleUp.Contains(text) + }; + + return result; + } + } + + ContentPage BuildMenu() + { + if (_menu != null) + { + return _menu; + } + + var layout = new Grid + { + VerticalOptions = LayoutOptions.Fill, + HorizontalOptions = LayoutOptions.Fill, + ColumnDefinitions = new ColumnDefinitionCollection { new ColumnDefinition(), new ColumnDefinition() } + }; + + var col1 = new StackLayout(); + layout.Children.Add(col1); + Grid.SetColumn(col1, 0); + + var col2 = new StackLayout(); + layout.Children.Add(col2); + Grid.SetColumn(col2, 1); + + col1.Children.Add(MenuButton(nameof(Image), () => new Image { Source = ImageSource.FromFile("oasis.jpg") })); + col1.Children.Add(MenuButton(nameof(Frame), () => new Frame { BackgroundColor = Color.DarkGoldenrod })); + col1.Children.Add(MenuButton(nameof(Entry), () => new Entry())); + col1.Children.Add(MenuButton(nameof(Editor), () => new Editor())); + col1.Children.Add(MenuButton(nameof(Button), () => new Button { Text = "Test" })); + col1.Children.Add(MenuButton(nameof(Label), () => new Label + { + LineBreakMode = LineBreakMode.WordWrap, + Text = "Lorem ipsum dolor sit amet" + })); + col1.Children.Add(MenuButton(nameof(SearchBar), () => new SearchBar())); + + col2.Children.Add(MenuButton(nameof(DatePicker), () => new DatePicker())); + col2.Children.Add(MenuButton(nameof(TimePicker), () => new TimePicker())); + col2.Children.Add(MenuButton(nameof(Slider), () => new Slider())); + col2.Children.Add(MenuButton(nameof(Switch), () => new Switch())); + col2.Children.Add(MenuButton(nameof(Stepper), () => new Stepper())); + col2.Children.Add(MenuButton(nameof(BoxView), () => new BoxView { BackgroundColor = Color.DarkMagenta, WidthRequest = 100, HeightRequest = 100 })); + + return new ContentPage { Content = layout }; + } + + protected override void Init() + { + PushAsync(BuildMenu()); + } + } +}
\ 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 4369eee3..84d146ba 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 @@ -203,6 +203,7 @@ <DependentUpon>Bugzilla38416.xaml</DependentUpon> </Compile> <Compile Include="$(MSBuildThisFileDirectory)FailImageSource.cs" /> + <Compile Include="$(MSBuildThisFileDirectory)GestureBubblingTests.cs" /> <Compile Include="$(MSBuildThisFileDirectory)InputTransparentTests.cs" /> <Compile Include="$(MSBuildThisFileDirectory)IsInvokeRequiredRaceCondition.cs" /> <Compile Include="$(MSBuildThisFileDirectory)IsPasswordToggleTest.cs" /> diff --git a/Xamarin.Forms.Platform.Android/AppCompat/FrameRenderer.cs b/Xamarin.Forms.Platform.Android/AppCompat/FrameRenderer.cs index d0ed345d..16ee0473 100644 --- a/Xamarin.Forms.Platform.Android/AppCompat/FrameRenderer.cs +++ b/Xamarin.Forms.Platform.Android/AppCompat/FrameRenderer.cs @@ -18,6 +18,7 @@ namespace Xamarin.Forms.Platform.Android.AppCompat readonly PinchGestureHandler _pinchGestureHandler; readonly Lazy<ScaleGestureDetector> _scaleDetector; readonly TapGestureHandler _tapGestureHandler; + readonly MotionEventHelper _motionEventHelper = new MotionEventHelper(); float _defaultElevation = -1f; float _defaultCornerRadius = -1f; @@ -32,6 +33,7 @@ namespace Xamarin.Forms.Platform.Android.AppCompat NotifyCollectionChangedEventHandler _collectionChangeHandler; bool _inputTransparent; + bool _isEnabled; public FrameRenderer() : base(Forms.Context) { @@ -78,6 +80,11 @@ namespace Xamarin.Forms.Platform.Android.AppCompat return false; } + if (Element.GestureRecognizers.Count == 0) + { + return _motionEventHelper.HandleMotionEvent(Parent); + } + return base.OnTouchEvent(e); } @@ -88,6 +95,12 @@ namespace Xamarin.Forms.Platform.Android.AppCompat bool IOnTouchListener.OnTouch(AView v, MotionEvent e) { + if (!_isEnabled) + return true; + + if (_inputTransparent) + return false; + var handled = false; if (_pinchGestureHandler.IsPinchSupported) { @@ -217,8 +230,11 @@ namespace Xamarin.Forms.Platform.Android.AppCompat UpdateBackgroundColor(); UpdateCornerRadius(); UpdateInputTransparent(); + UpdateIsEnabled(); SubscribeGestureRecognizers(e.NewElement); } + + _motionEventHelper.UpdateElement(e.NewElement); } protected override void OnLayout(bool changed, int left, int top, int right, int bottom) @@ -252,6 +268,13 @@ namespace Xamarin.Forms.Platform.Android.AppCompat UpdateCornerRadius(); else if (e.PropertyName == VisualElement.InputTransparentProperty.PropertyName) UpdateInputTransparent(); + else if (e.PropertyName == VisualElement.IsEnabledProperty.PropertyName) + UpdateIsEnabled(); + } + + void UpdateIsEnabled() + { + _isEnabled = Element.IsEnabled; } void UpdateInputTransparent() diff --git a/Xamarin.Forms.Platform.WinRT/ButtonRenderer.cs b/Xamarin.Forms.Platform.WinRT/ButtonRenderer.cs index 1b3960e0..9f662f0c 100644 --- a/Xamarin.Forms.Platform.WinRT/ButtonRenderer.cs +++ b/Xamarin.Forms.Platform.WinRT/ButtonRenderer.cs @@ -99,6 +99,8 @@ namespace Xamarin.Forms.Platform.WinRT return; } + protected override bool PreventGestureBubbling { get; set; } = true; + void OnButtonClick(object sender, RoutedEventArgs e) { ((IButtonController)Element)?.SendReleased(); diff --git a/Xamarin.Forms.Platform.WinRT/DatePickerRenderer.cs b/Xamarin.Forms.Platform.WinRT/DatePickerRenderer.cs index 677563e3..005d30ce 100644 --- a/Xamarin.Forms.Platform.WinRT/DatePickerRenderer.cs +++ b/Xamarin.Forms.Platform.WinRT/DatePickerRenderer.cs @@ -70,6 +70,8 @@ namespace Xamarin.Forms.Platform.WinRT UpdateTextColor(); } + protected override bool PreventGestureBubbling { get; set; } = true; + void OnControlDateChanged(object sender, DatePickerValueChangedEventArgs e) { Element.Date = e.NewDate.Date; diff --git a/Xamarin.Forms.Platform.WinRT/SliderRenderer.cs b/Xamarin.Forms.Platform.WinRT/SliderRenderer.cs index 0fff1496..7bef3e93 100644 --- a/Xamarin.Forms.Platform.WinRT/SliderRenderer.cs +++ b/Xamarin.Forms.Platform.WinRT/SliderRenderer.cs @@ -71,6 +71,8 @@ namespace Xamarin.Forms.Platform.WinRT } } + protected override bool PreventGestureBubbling { get; set; } = true; + void OnNativeValueChanged(object sender, RangeBaseValueChangedEventArgs e) { ((IElementController)Element).SetValueFromRenderer(Slider.ValueProperty, e.NewValue); diff --git a/Xamarin.Forms.Platform.WinRT/StepperRenderer.cs b/Xamarin.Forms.Platform.WinRT/StepperRenderer.cs index 4f09b126..796ef562 100644 --- a/Xamarin.Forms.Platform.WinRT/StepperRenderer.cs +++ b/Xamarin.Forms.Platform.WinRT/StepperRenderer.cs @@ -52,6 +52,8 @@ namespace Xamarin.Forms.Platform.WinRT Control.ButtonBackgroundColor = Element.BackgroundColor; } + protected override bool PreventGestureBubbling { get; set; } = true; + void OnControlValue(object sender, EventArgs e) { Element.SetValueCore(Stepper.ValueProperty, Control.Value); diff --git a/Xamarin.Forms.Platform.WinRT/SwitchRenderer.cs b/Xamarin.Forms.Platform.WinRT/SwitchRenderer.cs index ebc08ad6..05de07a4 100644 --- a/Xamarin.Forms.Platform.WinRT/SwitchRenderer.cs +++ b/Xamarin.Forms.Platform.WinRT/SwitchRenderer.cs @@ -42,6 +42,8 @@ namespace Xamarin.Forms.Platform.WinRT } } + protected override bool PreventGestureBubbling { get; set; } = true; + void OnNativeToggled(object sender, RoutedEventArgs routedEventArgs) { ((IElementController)Element).SetValueFromRenderer(Switch.IsToggledProperty, Control.IsOn); diff --git a/Xamarin.Forms.Platform.WinRT/TimePickerRenderer.cs b/Xamarin.Forms.Platform.WinRT/TimePickerRenderer.cs index 59ce8fcd..336edd30 100644 --- a/Xamarin.Forms.Platform.WinRT/TimePickerRenderer.cs +++ b/Xamarin.Forms.Platform.WinRT/TimePickerRenderer.cs @@ -65,6 +65,8 @@ namespace Xamarin.Forms.Platform.WinRT UpdateTextColor(); } + protected override bool PreventGestureBubbling { get; set; } = true; + void OnControlTimeChanged(object sender, TimePickerValueChangedEventArgs e) { Element.Time = e.NewTime; diff --git a/Xamarin.Forms.Platform.WinRT/VisualElementRenderer.cs b/Xamarin.Forms.Platform.WinRT/VisualElementRenderer.cs index a1cc0e44..778f464e 100644 --- a/Xamarin.Forms.Platform.WinRT/VisualElementRenderer.cs +++ b/Xamarin.Forms.Platform.WinRT/VisualElementRenderer.cs @@ -38,6 +38,8 @@ namespace Xamarin.Forms.Platform.WinRT protected bool ArrangeNativeChildren { get; set; } + protected virtual bool PreventGestureBubbling { get; set; } = false; + IElementController ElementController => Element as IElementController; protected VisualElementTracker<TElement, TNativeElement> Tracker @@ -549,6 +551,7 @@ namespace Xamarin.Forms.Platform.WinRT if (_tracker == null) return; + _tracker.PreventGestureBubbling = PreventGestureBubbling; _tracker.Control = Control; _tracker.Element = Element; _tracker.Container = ContainerElement; diff --git a/Xamarin.Forms.Platform.WinRT/VisualElementTracker.cs b/Xamarin.Forms.Platform.WinRT/VisualElementTracker.cs index a29d61ad..06c72b9a 100644 --- a/Xamarin.Forms.Platform.WinRT/VisualElementTracker.cs +++ b/Xamarin.Forms.Platform.WinRT/VisualElementTracker.cs @@ -67,6 +67,8 @@ namespace Xamarin.Forms.Platform.WinRT } } + public bool PreventGestureBubbling { get; set; } + public TNativeElement Control { get { return _control; } @@ -75,8 +77,19 @@ namespace Xamarin.Forms.Platform.WinRT if (_control == value) return; + if (_control != null) + { + _control.Tapped -= HandleTapped; + _control.DoubleTapped -= HandleDoubleTapped; + } + _control = value; UpdateNativeControl(); + + if (PreventGestureBubbling) + { + UpdatingGestureRecognizers(); + } } } @@ -163,6 +176,12 @@ namespace Xamarin.Forms.Platform.WinRT } } + if (_control != null) + { + _control.Tapped -= HandleTapped; + _control.DoubleTapped -= HandleDoubleTapped; + } + Control = null; Element = null; Container = null; @@ -502,11 +521,29 @@ namespace Xamarin.Forms.Platform.WinRT _container.PointerReleased -= OnPointerReleased; _container.PointerCanceled -= OnPointerCanceled; - if (gestures.GetGesturesFor<TapGestureRecognizer>(g => g.NumberOfTapsRequired == 1).GetEnumerator().MoveNext()) + if (gestures.GetGesturesFor<TapGestureRecognizer>(g => g.NumberOfTapsRequired == 1).Any()) + { _container.Tapped += OnTap; + } + else + { + if (_control != null && PreventGestureBubbling) + { + _control.Tapped += HandleTapped; + } + } - if (gestures.GetGesturesFor<TapGestureRecognizer>(g => g.NumberOfTapsRequired == 2).GetEnumerator().MoveNext()) + if (gestures.GetGesturesFor<TapGestureRecognizer>(g => g.NumberOfTapsRequired == 2).Any()) + { _container.DoubleTapped += OnDoubleTap; + } + else + { + if (_control != null && PreventGestureBubbling) + { + _control.DoubleTapped += HandleDoubleTapped; + } + } bool hasPinchGesture = gestures.GetGesturesFor<PinchGestureRecognizer>().GetEnumerator().MoveNext(); bool hasPanGesture = gestures.GetGesturesFor<PanGestureRecognizer>().GetEnumerator().MoveNext(); @@ -532,5 +569,15 @@ namespace Xamarin.Forms.Platform.WinRT _container.PointerReleased += OnPointerReleased; _container.PointerCanceled += OnPointerCanceled; } + + void HandleTapped(object sender, TappedRoutedEventArgs tappedRoutedEventArgs) + { + tappedRoutedEventArgs.Handled = true; + } + + void HandleDoubleTapped(object sender, DoubleTappedRoutedEventArgs doubleTappedRoutedEventArgs) + { + doubleTappedRoutedEventArgs.Handled = true; + } } }
\ No newline at end of file |