From f27f5a3650f37894d4a1ac925d6fab4dc7350087 Mon Sep 17 00:00:00 2001 From: "E.Z. Hart" Date: Thu, 23 Mar 2017 11:18:38 -0600 Subject: UI tests for InputTransparent and fixes for Android/Windows (#808) * Set up automated UI tests for InputTransparent * Pull in Adrian's UI tests from PR 483 * Fix bugs with box/label/image gestures passing through when not transparent * Fix disabling of layouts on Windows; fix 44096 test for iOS/Windows; * Automate the 53445 test --- .../AppCompat/FrameRenderer.cs | 26 ++++++++++- Xamarin.Forms.Platform.Android/Platform.cs | 44 ++++++++++++++++++ .../Renderers/BoxRenderer.cs | 19 ++------ .../Renderers/ImageRenderer.cs | 14 +++++- .../Renderers/LabelRenderer.cs | 14 +++++- .../Renderers/MotionEventHelper.cs | 53 ++++++++++++++++++++++ .../VisualElementRenderer.cs | 28 +++++++++--- .../Xamarin.Forms.Platform.Android.csproj | 1 + 8 files changed, 172 insertions(+), 27 deletions(-) create mode 100644 Xamarin.Forms.Platform.Android/Renderers/MotionEventHelper.cs (limited to 'Xamarin.Forms.Platform.Android') diff --git a/Xamarin.Forms.Platform.Android/AppCompat/FrameRenderer.cs b/Xamarin.Forms.Platform.Android/AppCompat/FrameRenderer.cs index 7c40ea80..d0ed345d 100644 --- a/Xamarin.Forms.Platform.Android/AppCompat/FrameRenderer.cs +++ b/Xamarin.Forms.Platform.Android/AppCompat/FrameRenderer.cs @@ -31,6 +31,8 @@ namespace Xamarin.Forms.Platform.Android.AppCompat VisualElementTracker _visualElementTracker; NotifyCollectionChangedEventHandler _collectionChangeHandler; + bool _inputTransparent; + public FrameRenderer() : base(Forms.Context) { _tapGestureHandler = new TapGestureHandler(() => Element); @@ -69,6 +71,16 @@ namespace Xamarin.Forms.Platform.Android.AppCompat } } + public override bool OnTouchEvent(MotionEvent e) + { + if (_inputTransparent) + { + return false; + } + + return base.OnTouchEvent(e); + } + void IOnClickListener.OnClick(AView v) { _tapGestureHandler.OnSingleClick(); @@ -83,14 +95,16 @@ namespace Xamarin.Forms.Platform.Android.AppCompat ScaleGestureDetectorCompat.SetQuickScaleEnabled(_scaleDetector.Value, true); handled = _scaleDetector.Value.OnTouchEvent(e); } - + if (_gestureDetector.IsValueCreated && _gestureDetector.Value.Handle == IntPtr.Zero) { // This gesture detector has already been disposed, probably because it's on a cell which is going away return handled; } - return _gestureDetector.Value.OnTouchEvent(e) || handled; + // It's very important that the gesture detection happen first here + // if we check handled first, we might short-circuit and never check for tap/pan + return _gestureDetector.Value.OnTouchEvent(e) || handled; } VisualElement IVisualElementRenderer.Element => Element; @@ -202,6 +216,7 @@ namespace Xamarin.Forms.Platform.Android.AppCompat UpdateShadow(); UpdateBackgroundColor(); UpdateCornerRadius(); + UpdateInputTransparent(); SubscribeGestureRecognizers(e.NewElement); } } @@ -235,6 +250,13 @@ namespace Xamarin.Forms.Platform.Android.AppCompat UpdateBackgroundColor(); else if (e.PropertyName == Frame.CornerRadiusProperty.PropertyName) UpdateCornerRadius(); + else if (e.PropertyName == VisualElement.InputTransparentProperty.PropertyName) + UpdateInputTransparent(); + } + + void UpdateInputTransparent() + { + _inputTransparent = Element.InputTransparent; } void SubscribeGestureRecognizers(VisualElement element) diff --git a/Xamarin.Forms.Platform.Android/Platform.cs b/Xamarin.Forms.Platform.Android/Platform.cs index ca0b9708..0fc287c9 100644 --- a/Xamarin.Forms.Platform.Android/Platform.cs +++ b/Xamarin.Forms.Platform.Android/Platform.cs @@ -1024,6 +1024,50 @@ namespace Xamarin.Forms.Platform.Android internal class DefaultRenderer : VisualElementRenderer { + bool _notReallyHandled; + internal void NotifyFakeHandling() + { + _notReallyHandled = true; + } + + public override bool DispatchTouchEvent(MotionEvent e) + { + #region + // Normally dispatchTouchEvent feeds the touch events to its children one at a time, top child first, + // (and only to the children in the hit-test area of the event) stopping as soon as one of them has handled + // the event. + + // But to be consistent across the platforms, we don't want this behavior; if an element is not input transparent + // we don't want an event to "pass through it" and be handled by an element "behind/under" it. We just want the processing + // to end after the first non-transparent child, regardless of whether the event has been handled. + + // This is only an issue for a couple of controls; the interactive controls (switch, button, slider, etc) already "handle" their touches + // and the events don't propagate to other child controls. But for image, label, and box that doesn't happen. We can't have those controls + // lie about their events being handled because then the events won't propagate to *parent* controls (e.g., a frame with a label in it would + // never get a tap gesture from the label). In other words, we *want* parent propagation, but *do not want* sibling propagation. So we need to short-circuit + // base.DispatchTouchEvent here, but still return "false". + + // Duplicating the logic of ViewGroup.dispatchTouchEvent and modifying it slightly for our purposes is a non-starter; the method is too + // complex and does a lot of micro-optimization. Instead, we provide a signalling mechanism for the controls which don't already "handle" touch + // events to tell us that they will be lying about handling their event; they then return "true" to short-circuit base.DispatchTouchEvent. + + // The container gets this message and after it gets the "handled" result from dispatchTouchEvent, + // it then knows to ignore that result and return false/unhandled. This allows the event to propagate up the tree. + #endregion + + _notReallyHandled = false; + + var result = base.DispatchTouchEvent(e); + + if (result && _notReallyHandled) + { + // If the child control returned true from its touch event handler but signalled that it was a fake "true", leave the event unhandled + // so parent controls have the opportunity + return false; + } + + return result; + } } #region IPlatformEngine implementation diff --git a/Xamarin.Forms.Platform.Android/Renderers/BoxRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/BoxRenderer.cs index 380d4018..aa19d81c 100644 --- a/Xamarin.Forms.Platform.Android/Renderers/BoxRenderer.cs +++ b/Xamarin.Forms.Platform.Android/Renderers/BoxRenderer.cs @@ -5,7 +5,7 @@ namespace Xamarin.Forms.Platform.Android { public class BoxRenderer : VisualElementRenderer { - bool _isInViewCell; + readonly MotionEventHelper _motionEventHelper = new MotionEventHelper(); public BoxRenderer() { @@ -16,26 +16,15 @@ namespace Xamarin.Forms.Platform.Android { if (base.OnTouchEvent(e)) return true; - return !Element.InputTransparent && !_isInViewCell; + + return _motionEventHelper.HandleMotionEvent(Parent); } protected override void OnElementChanged(ElementChangedEventArgs e) { base.OnElementChanged(e); - if (e.NewElement != null) - { - var parent = e.NewElement.Parent; - while (parent != null) - { - if (parent is ViewCell) - { - _isInViewCell = true; - break; - } - parent = parent.Parent; - } - } + _motionEventHelper.UpdateElement(e.NewElement); UpdateBackgroundColor(); } diff --git a/Xamarin.Forms.Platform.Android/Renderers/ImageRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/ImageRenderer.cs index e18d4b95..fee05f1b 100644 --- a/Xamarin.Forms.Platform.Android/Renderers/ImageRenderer.cs +++ b/Xamarin.Forms.Platform.Android/Renderers/ImageRenderer.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.IO; using System.Threading.Tasks; using Android.Graphics; +using Android.Views; using AImageView = Android.Widget.ImageView; using Xamarin.Forms.Internals; @@ -11,8 +12,7 @@ namespace Xamarin.Forms.Platform.Android public class ImageRenderer : ViewRenderer { bool _isDisposed; - - IElementController ElementController => Element as IElementController; + readonly MotionEventHelper _motionEventHelper = new MotionEventHelper(); public ImageRenderer() { @@ -44,6 +44,8 @@ namespace Xamarin.Forms.Platform.Android SetNativeControl(view); } + _motionEventHelper.UpdateElement(e.NewElement); + UpdateBitmap(e.OldElement); UpdateAspect(); } @@ -117,5 +119,13 @@ namespace Xamarin.Forms.Platform.Android ((IVisualElementController)Element).NativeSizeChanged(); } } + + public override bool OnTouchEvent(MotionEvent e) + { + if (base.OnTouchEvent(e)) + return true; + + return _motionEventHelper.HandleMotionEvent(Parent); + } } } \ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/LabelRenderer.cs b/Xamarin.Forms.Platform.Android/Renderers/LabelRenderer.cs index c544a239..ab1a6a35 100644 --- a/Xamarin.Forms.Platform.Android/Renderers/LabelRenderer.cs +++ b/Xamarin.Forms.Platform.Android/Renderers/LabelRenderer.cs @@ -4,8 +4,8 @@ using Android.Content.Res; using Android.Graphics; using Android.Text; using Android.Util; +using Android.Views; using Android.Widget; -using AColor = Android.Graphics.Color; namespace Xamarin.Forms.Platform.Android { @@ -23,6 +23,8 @@ namespace Xamarin.Forms.Platform.Android FormsTextView _view; bool _wasFormatted; + readonly MotionEventHelper _motionEventHelper = new MotionEventHelper(); + public LabelRenderer() { AutoPackage = false; @@ -97,6 +99,8 @@ namespace Xamarin.Forms.Platform.Android if (e.OldElement.HorizontalTextAlignment != e.NewElement.HorizontalTextAlignment || e.OldElement.VerticalTextAlignment != e.NewElement.VerticalTextAlignment) UpdateGravity(); } + + _motionEventHelper.UpdateElement(e.NewElement); } protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) @@ -217,5 +221,13 @@ namespace Xamarin.Forms.Platform.Android _lastSizeRequest = null; } + + public override bool OnTouchEvent(MotionEvent e) + { + if (base.OnTouchEvent(e)) + return true; + + return _motionEventHelper.HandleMotionEvent(Parent); + } } } \ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/Renderers/MotionEventHelper.cs b/Xamarin.Forms.Platform.Android/Renderers/MotionEventHelper.cs new file mode 100644 index 00000000..4ed06d26 --- /dev/null +++ b/Xamarin.Forms.Platform.Android/Renderers/MotionEventHelper.cs @@ -0,0 +1,53 @@ +using Android.Views; + +namespace Xamarin.Forms.Platform.Android +{ + internal class MotionEventHelper + { + VisualElement _element; + bool _isInViewCell; + + public bool HandleMotionEvent(IViewParent parent) + { + if (_isInViewCell || _element.InputTransparent) + { + return false; + } + + var renderer = parent as Platform.DefaultRenderer; + if (renderer == null) + { + return false; + } + + // Let the container know that we're "fake" handling this event + renderer.NotifyFakeHandling(); + + return true; + } + + public void UpdateElement(VisualElement element) + { + _isInViewCell = false; + _element = element; + + if (_element == null) + { + return; + } + + // Determine whether this control is inside a ViewCell; + // we don't fake handle the events because ListView needs them for row selection + var parent = _element.Parent; + while (parent != null) + { + if (parent is ViewCell) + { + _isInViewCell = true; + break; + } + parent = parent.Parent; + } + } + } +} \ No newline at end of file diff --git a/Xamarin.Forms.Platform.Android/VisualElementRenderer.cs b/Xamarin.Forms.Platform.Android/VisualElementRenderer.cs index 7f5c7f91..8f6722c3 100644 --- a/Xamarin.Forms.Platform.Android/VisualElementRenderer.cs +++ b/Xamarin.Forms.Platform.Android/VisualElementRenderer.cs @@ -92,14 +92,20 @@ namespace Xamarin.Forms.Platform.Android public override bool OnInterceptTouchEvent(MotionEvent ev) { - if (Element.InputTransparent && Element.IsEnabled) - return false; + if (!Element.IsEnabled || (Element.InputTransparent && Element.IsEnabled)) + return true; return base.OnInterceptTouchEvent(ev); } bool AView.IOnTouchListener.OnTouch(AView v, MotionEvent e) { + if (!Element.IsEnabled) + return true; + + if (Element.InputTransparent) + return false; + var handled = false; if (_pinchGestureHandler.IsPinchSupported) { @@ -116,7 +122,11 @@ namespace Xamarin.Forms.Platform.Android return handled; } - return _gestureDetector.Value.OnTouchEvent(e) || handled; + // It's very important that the gesture detection happen first here + // if we check handled first, we might short-circuit and never check for tap/pan + handled = _gestureDetector.Value.OnTouchEvent(e) || handled; + + return handled; } VisualElement IVisualElementRenderer.Element => Element; @@ -191,8 +201,6 @@ namespace Xamarin.Forms.Platform.Android SoundEffectsEnabled = false; } - InputTransparent = Element.InputTransparent; - // must be updated AFTER SetOnClickListener is called // SetOnClickListener implicitly calls Clickable = true UpdateGestureRecognizers(true); @@ -215,6 +223,7 @@ namespace Xamarin.Forms.Platform.Android SetContentDescription(); SetFocusable(); + UpdateInputTransparent(); Performance.Stop(); } @@ -305,14 +314,14 @@ namespace Xamarin.Forms.Platform.Android { if (e.PropertyName == VisualElement.BackgroundColorProperty.PropertyName) UpdateBackgroundColor(); - else if (e.PropertyName == VisualElement.InputTransparentProperty.PropertyName) - InputTransparent = Element.InputTransparent; else if (e.PropertyName == Accessibility.HintProperty.PropertyName) SetContentDescription(); else if (e.PropertyName == Accessibility.NameProperty.PropertyName) SetContentDescription(); else if (e.PropertyName == Accessibility.IsInAccessibleTreeProperty.PropertyName) SetFocusable(); + else if (e.PropertyName == VisualElement.InputTransparentProperty.PropertyName) + UpdateInputTransparent(); } protected override void OnLayout(bool changed, int l, int t, int r, int b) @@ -398,6 +407,11 @@ namespace Xamarin.Forms.Platform.Android return true; } + void UpdateInputTransparent() + { + InputTransparent = Element.InputTransparent; + } + protected void SetPackager(VisualElementPackager packager) { _packager = packager; diff --git a/Xamarin.Forms.Platform.Android/Xamarin.Forms.Platform.Android.csproj b/Xamarin.Forms.Platform.Android/Xamarin.Forms.Platform.Android.csproj index 29562618..fd894e47 100644 --- a/Xamarin.Forms.Platform.Android/Xamarin.Forms.Platform.Android.csproj +++ b/Xamarin.Forms.Platform.Android/Xamarin.Forms.Platform.Android.csproj @@ -174,6 +174,7 @@ + -- cgit v1.2.3