[A] Fix Bugzilla 40173 (#62)
authorJason Smith <jason.smith@xamarin.com>
Fri, 8 Apr 2016 16:16:02 +0000 (09:16 -0700)
committerRui Marinho <me@ruimarinho.net>
Fri, 8 Apr 2016 16:16:02 +0000 (17:16 +0100)
* [A]Fix issue where Frame would block all tap gestures under it even if it didn't have one

* [A]Make BoxView non-blocking inside a ListView only

* Add UITest for bz40173

* Fix code review issues

Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla40173.cs [new file with mode: 0644]
Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems
Xamarin.Forms.Platform.Android/AppCompat/FrameRenderer.cs
Xamarin.Forms.Platform.Android/Renderers/BoxRenderer.cs

diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla40173.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla40173.cs
new file mode 100644 (file)
index 0000000..422e363
--- /dev/null
@@ -0,0 +1,110 @@
+using Xamarin.Forms.CustomAttributes;
+#if UITEST
+using NUnit.Framework;
+#endif
+
+namespace Xamarin.Forms.Controls
+{
+    [Preserve(AllMembers = true)]
+    [Issue(IssueTracker.Bugzilla, 40173, "Android BoxView/Frame not clickthrough in ListView")]
+    public class Bugzilla40173 : TestContentPage // or TestMasterDetailPage, etc ...
+    {
+        const string CantTouchButtonId = "CantTouchButtonId";
+        const string CanTouchButtonId = "CanTouchButtonId";
+        const string ListTapTarget = "ListTapTarget";
+        const string CantTouchFailText = "Failed";
+        const string CanTouchSuccessText = "ButtonTapped";
+        const string ListTapSuccessText = "ItemTapped";
+
+#if UITEST
+        [Test]
+        public void ButtonBlocked()
+        {
+            RunningApp.Tap(q => q.Marked(CantTouchButtonId));
+            RunningApp.WaitForNoElement(q => q.Text(CantTouchFailText));
+
+            RunningApp.Tap(q => q.Marked(CanTouchButtonId));
+            RunningApp.WaitForElement(q => q.Text(CanTouchSuccessText));
+
+            RunningApp.Tap(q => q.Marked(ListTapTarget));
+            RunningApp.WaitForElement(q => q.Text(ListTapSuccessText));
+        }
+#endif
+
+        protected override void Init()
+        {
+            var outputLabel = new Label();
+            var testButton = new Button
+            {
+                Text = "Can't Touch This",
+                AutomationId = CantTouchButtonId
+            };
+
+            testButton.Clicked += (sender, args) => outputLabel.Text = CantTouchFailText;
+
+            var testGrid = new Grid
+            {
+                Children =
+                {
+                    testButton,
+                    new BoxView
+                    {
+                        Color = Color.Pink.MultiplyAlpha(0.5)
+                    }
+                }
+            };
+
+            // BoxView over Button prevents Button click
+            var testButtonOk = new Button
+            {
+                Text = "Can Touch This",
+                AutomationId = CanTouchButtonId
+            };
+
+            testButtonOk.Clicked += (sender, args) => outputLabel.Text = CanTouchSuccessText;
+
+            var testGridOk = new Grid
+            {
+                Children =
+                {
+                    testButtonOk,
+                    new BoxView
+                    {
+                        Color = Color.Pink.MultiplyAlpha(0.5),
+                        InputTransparent = true
+                    }
+                }
+            };
+
+            var testListView = new ListView();
+            var items = new[] { "Foo" };
+            testListView.ItemsSource = items;
+            testListView.ItemTemplate = new DataTemplate(() =>
+            {
+                var result = new ViewCell
+                {
+                    View = new Grid
+                    {
+                        Children =
+                        {
+                            new BoxView
+                            {
+                                AutomationId = ListTapTarget,
+                                Color = Color.Pink.MultiplyAlpha(0.5)
+                            }
+                        }
+                    }
+                };
+
+                return result;
+            });
+
+            testListView.ItemSelected += (sender, args) => outputLabel.Text = ListTapSuccessText;
+
+            Content = new StackLayout
+            {
+                Children = { outputLabel, testGrid, testGridOk, testListView }
+            };
+        }
+    }
+}
\ No newline at end of file
index e13847b..e7afb08 100644 (file)
@@ -95,6 +95,7 @@
       <SubType>Code</SubType>
     </Compile>
     <Compile Include="$(MSBuildThisFileDirectory)Bugzilla39702.cs" />
+    <Compile Include="$(MSBuildThisFileDirectory)Bugzilla40173.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)CarouselAsync.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)Bugzilla34561.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)Bugzilla34727.cs" />
index 7b544b2..232b6a6 100644 (file)
@@ -1,4 +1,6 @@
 using System;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
 using System.ComponentModel;
 using Android.Content;
 using Android.Support.V4.View;
@@ -19,11 +21,13 @@ namespace Xamarin.Forms.Platform.Android.AppCompat
 
                float _defaultElevation = -1f;
 
+               bool _clickable;
                bool _disposed;
                Frame _element;
                InnerGestureListener _gestureListener;
                VisualElementPackager _visualElementPackager;
                VisualElementTracker _visualElementTracker;
+               NotifyCollectionChangedEventHandler _collectionChangeHandler;
 
                public FrameRenderer() : base(Forms.Context)
                {
@@ -147,17 +151,20 @@ namespace Xamarin.Forms.Platform.Android.AppCompat
                        ElementChanged?.Invoke(this, new VisualElementChangedEventArgs(e.OldElement, e.NewElement));
 
                        if (e.OldElement != null)
-                               e.OldElement.PropertyChanged -= OnElementPropertyChanged;
-                       else
                        {
-                               SetOnClickListener(this);
-                               SetOnTouchListener(this);
+                               e.OldElement.PropertyChanged -= OnElementPropertyChanged;
+                               UnsubscribeGestureRecognizers(e.OldElement);
                        }
 
                        if (e.NewElement != null)
                        {
                                if (_visualElementTracker == null)
                                {
+                                       SetOnClickListener(this);
+                                       SetOnTouchListener(this);
+
+                                       UpdateGestureRecognizers(true);
+
                                        _visualElementTracker = new VisualElementTracker(this);
                                        _visualElementPackager = new VisualElementPackager(this);
                                        _visualElementPackager.Load();
@@ -166,6 +173,7 @@ namespace Xamarin.Forms.Platform.Android.AppCompat
                                e.NewElement.PropertyChanged += OnElementPropertyChanged;
                                UpdateShadow();
                                UpdateBackgroundColor();
+                               SubscribeGestureRecognizers(e.NewElement);
                        }
                }
 
@@ -185,6 +193,11 @@ namespace Xamarin.Forms.Platform.Android.AppCompat
                        }
                }
 
+               void HandleGestureRecognizerCollectionChanged(object sender, NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs)
+               {
+                       UpdateGestureRecognizers();
+               }
+
                void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
                {
                        if (e.PropertyName == Frame.HasShadowProperty.PropertyName)
@@ -193,12 +206,54 @@ namespace Xamarin.Forms.Platform.Android.AppCompat
                                UpdateBackgroundColor();
                }
 
+               void SubscribeGestureRecognizers(VisualElement element)
+               {
+                       var view = element as View;
+                       if (view == null)
+                               return;
+
+                       if (_collectionChangeHandler == null)
+                               _collectionChangeHandler = HandleGestureRecognizerCollectionChanged;
+
+                       var observableCollection = (ObservableCollection<IGestureRecognizer>)view.GestureRecognizers;
+                       observableCollection.CollectionChanged += _collectionChangeHandler;
+               }
+
+               void UnsubscribeGestureRecognizers(VisualElement element)
+               {
+                       var view = element as View;
+                       if (view == null || _collectionChangeHandler == null)
+                               return;
+
+                       var observableCollection = (ObservableCollection<IGestureRecognizer>)view.GestureRecognizers;
+                       observableCollection.CollectionChanged -= _collectionChangeHandler;
+               }
+
                void UpdateBackgroundColor()
                {
                        Color bgColor = Element.BackgroundColor;
                        SetCardBackgroundColor(bgColor.IsDefault ? AColor.White : bgColor.ToAndroid());
                }
 
+               void UpdateClickable(bool force = false)
+               {
+                       var view = Element as View;
+                       if (view == null)
+                               return;
+
+                       bool newValue = view.ShouldBeMadeClickable();
+                       if (force || _clickable != newValue)
+                               Clickable = newValue;
+               }
+
+               void UpdateGestureRecognizers(bool forceClick = false)
+               {
+                       if (Element == null)
+                               return;
+
+                       UpdateClickable(forceClick);
+               }
+
                void UpdateShadow()
                {
                        float elevation = _defaultElevation;
index 725829f..b29dd76 100644 (file)
@@ -5,6 +5,8 @@ namespace Xamarin.Forms.Platform.Android
 {
        public class BoxRenderer : VisualElementRenderer<BoxView>
        {
+               bool _isInViewCell;
+
                public BoxRenderer()
                {
                        AutoPackage = false;
@@ -12,13 +14,29 @@ namespace Xamarin.Forms.Platform.Android
 
                public override bool OnTouchEvent(MotionEvent e)
                {
-                       base.OnTouchEvent(e);
-                       return !Element.InputTransparent;
+                       if (base.OnTouchEvent(e))
+                               return true;
+                       return !Element.InputTransparent && !_isInViewCell;
                }
 
                protected override void OnElementChanged(ElementChangedEventArgs<BoxView> 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;
+                               }
+                       }
+
                        UpdateBackgroundColor();
                }