Custom Renderer ViewGroup not compatible with TapgestureRecognizer?

NicolasHotterbeekxNicolasHotterbeekx USMember ✭✭✭
edited June 2015 in Xamarin.Forms

Hi,

How did the team from xamarin.forms implement the tapgesture recognizer for android?

I was trying to create a custom renderer for android that would detect a swipe gesture, but leaves the tap gestures of it's children still fire.
For that I created a new custom renderer that would set a new viewgroup as native element:

  • this.SetNativeControl(new MyViewGroup());

In my custom MyViewGroup, I only intercep the swipe gesture, not the tapped gesture.
As described in the android documentation: https://developer.android.com/training/gestures/viewgroup.html

And this works perfectly, but as soon as I put an element with a tap gesture, the element (with the tapgesture) does not fire the OnInterceptTouchEvent(MotionEvent ev) in the viewgroup, but directly fires his own event. As by this, I cannot intercept the swipe in my viewgroup

So how is this possible?

Thanks in advance!

Best Answer

Answers

  • adamkempadamkemp USInsider, Developer Group Leader mod

    Can you share an example? That sounds like it could be a bug with the tap gesture handling in Xamarin.Forms for Android.

  • NicolasHotterbeekxNicolasHotterbeekx USMember ✭✭✭

    Hi @adamkemp

    In attachment you can find an example.
    When running on android, you can see that when performing a gesture on the green boxview (which has a tapgesturerecognizer), the gesture is is not intercepted by my custom ViewGroup, but fires the Tapped Event directly.
    However when doing this on the red boxview, the gestures are intercepted in MyViewGroup.

    If you got any other idea's on how I can achieve the behaviour I want, I would be very grateful.

    Thanks!

  • adamkempadamkemp USInsider, Developer Group Leader mod

    I think the only bug here is that a "tap" gesture on Android recognizes even if you move your finger or hold it down too long. That is, it seems to be recognizing any "up" even as a tap, which isn't what I would expect. On iOS a "tap" is a short touch with little or not movement.

    So basically what's happening here is that when the up event comes the tap gesture causes the inner view to say "that's my gesture", which blocks it from being handled by the outer view. The outer view can only handle a gesture if no nested views want to handle it. The outer view can prevent that by intercepting the touch, which is done by returning true from OnInterceptTouchEvent.

    In order to do this you should instead use a GestureDetector, like this:

    public class MySwipeViewRenderer : ViewRenderer<MySwipeView, ViewGroup>, GestureDetector.IOnGestureListener
    {
        private MyViewGroup _myViewGroup;
    
        private GestureDetector _detector;
    
        public MySwipeViewRenderer()
        {
            _myViewGroup = new MyViewGroup(Forms.Context);
            _detector = new GestureDetector(Forms.Context, this);
        }
    
        public override bool OnInterceptTouchEvent(MotionEvent ev)
        {
            return _detector.OnTouchEvent(ev);
        }
    
        public override bool OnTouchEvent(MotionEvent e)
        {
            _detector.OnTouchEvent(e);
            return base.OnTouchEvent(e);
        }
    
        public bool OnDown(MotionEvent e)
        {
            return false;
        }
    
        public bool OnFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)
        {
            Console.WriteLine("fling");
            return true;
        }
    
        public void OnLongPress(MotionEvent e)
        {
        }
    
        public bool OnScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)
        {
            return false;
        }
    
        public void OnShowPress(MotionEvent e)
        {
        }
    
        public bool OnSingleTapUp(MotionEvent e)
        {
            return false;
        }
    
        protected override void OnElementChanged(ElementChangedEventArgs<MySwipeView> e)
        {
            base.OnElementChanged(e);
            Console.WriteLine("In OnElementChanged");
            var child = RendererFactory.GetRenderer(this.Element.Content);
            _myViewGroup.AddView(child.ViewGroup);
            this.SetNativeControl(_myViewGroup);
        }
    }
    
  • adamkempadamkemp USInsider, Developer Group Leader mod

    I reported this bug for the Android tap gesture behavior.

  • NicolasHotterbeekxNicolasHotterbeekx USMember ✭✭✭

    Hi Adam,

    First of all, thanks for the help.
    I agree with the bug you reported.
    However, second reply, is not completely correct, but it helped me fixing my problem. Let me explain ;)

    You say "The outer view can only handle a gesture if no nested views want to handle it" but that is not correct, in android is always the outer view (viewgroup) who can decide to handle the touch event, or to pass it down to its inner views.
    I quote from https://developer.android.com/training/gestures/viewgroup.html

    The onInterceptTouchEvent() method is called whenever a touch event is detected on the surface of a ViewGroup, including on the surface of its children. If onInterceptTouchEvent() returns true, the MotionEvent is intercepted, meaning it will be not be passed on to the child, but rather to the onTouchEvent() method of the parent
    The onInterceptTouchEvent() method gives a parent the chance to see any touch event before its children do. If you return true from onInterceptTouchEvent(), the child view that was previously handling touch events receives an ACTION_CANCEL, and the events from that point forward are sent to the parent's onTouchEvent() method for the usual handling. onInterceptTouchEvent() can also return false and simply spy on events as they travel down the view hierarchy to their usual targets, which will handle the events with their own onTouchEvent().

    So the problem with the code you sugested, was that now it was possible to do tap and swipe on green boxview (who has a tapgesturerecognizer), but the swipe was not detected on the red boxview (without tapgesturerecognizer).

    But here is the code I use now, which works perfectly for what I need, I removed my custom viewgroup in an other class, and you create the viewgroup renderer like you. Then I had to tweak the onInterceptTouchEvent() and onTouch methods.

    public class MySwipeViewRenderer : ViewRenderer<MySwipeView, ViewGroup>, GestureDetector.IOnGestureListener
    {
        private GestureDetector _detector;
    
        public MySwipeViewRenderer()
        {
            _detector = new GestureDetector(Forms.Context, this);
        }
    
        public override bool OnInterceptTouchEvent(MotionEvent ev)
        {
            // Always handle the case of the touch gesture being complete.
            if (ev.Action == MotionEventActions.Cancel || ev.Action == MotionEventActions.Up) {
                return false; // Do not intercept touch event, let the child handle it
            }
    
            switch (ev.Action) {
                case MotionEventActions.Move:
                {
                    return true;
                    break;
                }       
                case MotionEventActions.Down:
                    return false;
                    break;
            }
            return false;
    
        }
    
        public override bool OnTouchEvent(MotionEvent e)
        {
            return _detector.OnTouchEvent(e);
        }
    
        public bool OnDown(MotionEvent e)
        {
            return true;
        }
    
        public bool OnFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)
        {
            Console.WriteLine("Renderer: Fling");
            return true;
        }
    
        public void OnLongPress(MotionEvent e)
        {
    
        }
    
        public bool OnScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY)
        {
            return false;
        }
    
        public void OnShowPress(MotionEvent e)
        {
        }
    
        public bool OnSingleTapUp(MotionEvent e)
        {
            return false;
        }
    
        protected override void OnElementChanged(ElementChangedEventArgs<MySwipeView> e)
        {
            base.OnElementChanged(e);
            var child = RendererFactory.GetRenderer(this.Element.Content);
            this.ViewGroup.AddView(child.ViewGroup);
        }
    }
    

    But once again, thanks for the help, without your input I would'n have finished it!

  • NicolasHotterbeekxNicolasHotterbeekx USMember ✭✭✭

    Hi Adam,

    Thanks thanks and thanks! The piece of the ActionMasked thing was the last piece of information I was missing ;)
    But in the OnInterceptTouchEvent, i have to spy on the MotionEvent.Down, because with the whole technique we are using, the OnFling method e1 evenargs are null, so I keep track of the last MotionEvent.Down x coordinate to determine a left or right swipe:

        public override bool OnInterceptTouchEvent(MotionEvent ev)
        {
            ViewConfiguration vc = ViewConfiguration.Get(Forms.Context);
            int mSlop = vc.ScaledTouchSlop;           
            switch (ev.ActionMasked)
            {               
    
                case MotionEventActions.Move:                   
                    float deltaX = ev.RawX - _lastOnDownX;
                    if (Math.Abs(deltaX) > mSlop)
                    {
                        return true;
                    }
                    else
                    {
                        return false;
                    }
                    break;
                case MotionEventActions.Down:
                    _lastOnDownX = ev.GetX();
                    return false;
                    break;
            }
            return false;
    
        }
    
        public bool OnFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)
        {
            if (_lastOnDownX - e2.GetX() > SWIPE_MIN_DISTANCE && Math.Abs(velocityX) > SWIPE_THRESHOLD_VELOCITY)
            {
                // right to left swipe                
                Console.WriteLine("right to left swipe")
                return true;
    
            }
            if (e2.GetX() - _lastOnDownX > SWIPE_MIN_DISTANCE && Math.Abs(velocityX) > SWIPE_THRESHOLD_VELOCITY)
            {
                // left to right swipe
                Console.WriteLine(" left to right swipe")
                return true;
    
            }
            return false;
        }
    
Sign In or Register to comment.