summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/GestureBubblingTests.cs211
-rw-r--r--Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems1
-rw-r--r--Xamarin.Forms.Platform.Android/AppCompat/FrameRenderer.cs23
-rw-r--r--Xamarin.Forms.Platform.WinRT/ButtonRenderer.cs2
-rw-r--r--Xamarin.Forms.Platform.WinRT/DatePickerRenderer.cs2
-rw-r--r--Xamarin.Forms.Platform.WinRT/SliderRenderer.cs2
-rw-r--r--Xamarin.Forms.Platform.WinRT/StepperRenderer.cs2
-rw-r--r--Xamarin.Forms.Platform.WinRT/SwitchRenderer.cs2
-rw-r--r--Xamarin.Forms.Platform.WinRT/TimePickerRenderer.cs2
-rw-r--r--Xamarin.Forms.Platform.WinRT/VisualElementRenderer.cs3
-rw-r--r--Xamarin.Forms.Platform.WinRT/VisualElementTracker.cs51
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