Xamarin UIScrollView GestureRecognizerShouldBegin

2019-08-09 01:43发布

问题:

I'm using Xamarin to develop an iOS app and I want to subclass UIScrollView in order to handle the pan gesture in the scroll view based on its velocity. So, I made an override of GestureRecognizerShouldBegin and I check the VelocityInView of the pan gesture. This works fine for the first gesture, but subsequent pan gestures that fire while the scroll view is in motion (decelerating) always report a velocity of (0, 0):

public class MyScroll : UIScrollView
{
    public override bool GestureRecognizerShouldBegin(UIGestureRecognizer gestureRecognizer)
    {
        UIPanGestureRecognizer panGesture = gestureRecognizer as UIPanGestureRecognizer;
        if (panGesture != null)
        {
            CGPoint velocity = panGesture.VelocityInView(this);
            Console.WriteLine("Pan gesture velocity: " + velocity);
        }
        return true;
    }
}

Output after panning once and then a second time while the scroll is in motion:

Pan gesture velocity: {X=37.92359, Y=-872.2426}
Pan gesture velocity: {X=0, Y=0}

Is this a bug or is this the expected behavior?

Edit: cross-posted on Xamarin's forum: https://forums.xamarin.com/discussion/54478/uiscrollview-pan-gesture-velocity-reporting-0-if-it-is-already-moving#latest

Edit to clarify:

To clarify what I'm ultimately trying to do: I have a vertical scroll view inside a horizontal paging view. I want to check the velocity of the pan so that I can tell the scroll view to not handle that gesture if the pan is "horizontal" (i.e., X velocity > Y velocity). The default behavior is such that once the scroll view is in motion, another gesture still scrolls, but this makes it difficult for users to scroll horizontally (across pages) until the vertical scroll has completely settled.

回答1:

I finally figured it out. Thanks to @RobertN for his assistance :)

The key is that the default pan gesture recognizer used by the scroll view will always report 0 velocity if it is already in motion (e.g., the inertia from a previous gesture is still in effect). Adding a new UIPanGestureRecognizer is a good way to record the "actual" velocity of a subsequent gesture, but by that time it is too late to affect the original pan gesture's GestureRecognizerShouldBegin. So all I have to do is add a ShouldBegin delegate to my new UIPanGestureRecognizer and use that to return false in the case where I want the gesture to "fall through" to the parent pager.

        public MyScroll() : base()
        {
            UIPanGestureRecognizer panGesture = new UIPanGestureRecognizer();

            panGesture.ShouldBegin = delegate(UIGestureRecognizer recognizer)
            {
                CGPoint v = panGesture.VelocityInView(this);

                if (v.X != 0 || v.Y != 0)
                {
                    if (Math.Abs(v.X) > Math.Abs(v.Y))
                    {
                        return false;
                    }
                }

                return true;
            };

            this.AddGestureRecognizer(panGesture);
        }

This way, I just let the default scroll view pan gesture recognizer do its thing, while my new UIPanGestureRecognizer recognizes when the user is making a new horizontal gesture, and passes that one through so that the pager can page. This makes the combination of the parent pager and vertical page scroll views operate nicely together (imagine having vertically scrolling pages and being able to flip through pages, even if the vertical page is in motion). Note, you also need to implement the following method to allow both gesture recognizers to operate simultaneously:

        [Export("gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:")]
        public bool ShouldRecognizeSimultaneously(UIGestureRecognizer gestureRecognizer, UIGestureRecognizer otherGestureRecognizer)
        {
            return true;
        }


回答2:

Is this a bug or is this the expected behavior?

In terms of grabbing the p/s via VelocityInView on GestureRecognizerShouldBegin, getting 0,0 after a pan motion has started, but not stopped/reset, is expected, at least in my experience. Obj-C/Swift is going to return the same thing, do not ask me why, have to get an actual iOS dev to ask the reason on that one.

Grabbing the velocity 'anywhere' else and you should be golden, if you really need within GestureRecognizerShouldBegin assign a private CGPoint within your UIScrollView sub-class from any other pan recognizer (I do that in the example below)...

Example output:

2015-10-26 12:07:06.676 iOSVelocity[68486:2309184] Touch-enabed Pan gesture velocity: {X=-608.4813, Y=0}
2015-10-26 12:07:06.703 iOSVelocity[68486:2309184] Touch-enabed Pan gesture velocity: {X=-1213.629, Y=0}
2015-10-26 12:07:06.726 iOSVelocity[68486:2309184] Touch-enabed Pan gesture velocity: {X=-935.5507, Y=0}
2015-10-26 12:07:06.771 iOSVelocity[68486:2309184] Touch-enabed Pan gesture velocity: {X=-1191.385, Y=-8.564461}
2015-10-26 12:07:06.772 iOSVelocity[68486:2309184] otherGestureRecognizer velocity: {X=-1191.385, Y=-8.564461}
2015-10-26 12:07:06.772 iOSVelocity[68486:2309184] otherGestureRecognizer velocity: {X=-1191.385, Y=-8.564461}
2015-10-26 12:07:08.882 iOSVelocity[68486:2309184] !!!! ShouldBegin velocity not reset !!!!
2015-10-26 12:07:08.885 iOSVelocity[68486:2309184] GestureRecognizerShouldBegin velocity: {X=-1191.385, Y=-8.564461}
2015-10-26 12:07:08.887 iOSVelocity[68486:2309184] otherGestureRecognizer velocity: {X=-1191.385, Y=-8.564461}
2015-10-26 12:07:08.889 iOSVelocity[68486:2309184] gestureRecognizer velocity: {X=0, Y=0}
2015-10-26 12:07:08.890 iOSVelocity[68486:2309184] otherGestureRecognizer velocity: {X=0, Y=0}
2015-10-26 12:07:08.891 iOSVelocity[68486:2309184] gestureRecognizer velocity: {X=0, Y=0}
2015-10-26 12:07:08.937 iOSVelocity[68486:2309184] otherGestureRecognizer velocity: {X=0, Y=0}
2015-10-26 12:07:08.938 iOSVelocity[68486:2309184] otherGestureRecognizer velocity: {X=0, Y=0}
2015-10-26 12:07:08.939 iOSVelocity[68486:2309184] gestureRecognizer velocity: {X=-336.9197, Y=0}
2015-10-26 12:07:08.940 iOSVelocity[68486:2309184] Touch-enabled Pan gesture velocity: {X=-336.9197, Y=0}
2015-10-26 12:07:08.954 iOSVelocity[68486:2309184] Touch-enabled Pan gesture velocity: {X=-650.7258, Y=0}
2015-10-26 12:07:08.961 iOSVelocity[68486:2309184] Touch-enabled Pan gesture velocity: {X=-650.7258, Y=0}
2015-10-26 12:07:08.993 iOSVelocity[68486:2309184] Touch-enabled Pan gesture velocity: {X=-914.0547, Y=0}
2015-10-26 12:07:09.027 iOSVelocity[68486:2309184] Touch-enabled Pan gesture velocity: {X=-734.1516, Y=0}
2015-10-26 12:07:09.032 iOSVelocity[68486:2309184] otherGestureRecognizer velocity: {X=-734.1516, Y=0}
2015-10-26 12:07:09.033 iOSVelocity[68486:2309184] otherGestureRecognizer velocity: {X=-734.1516, Y=0}
2015-10-26 12:07:09.060 iOSVelocity[68486:2309184] Touch-enabled Pan gesture velocity: {X=-1086.368, Y=0}

Example UIScrollView subclass:

Note: This uses shouldRecognizeSimultaneouslyWithGestureRecognizer in order to allow auto-panning to continue after the user lifts their touch

Note2: Not sure if I captured all the gesture state permutations, so adjust as needed

using System;
using UIKit;
using CoreGraphics;
using CoreFoundation;
using CoreData;
using Foundation;
using CoreMotion;

namespace iOSVelocity
{
    public class MyScroll : UIScrollView
    {
        UIPanGestureRecognizer panGesture;
        CGPoint velocity;

        public MyScroll (CGRect cGRect) : base (cGRect)
        {
            panGesture = new UIPanGestureRecognizer (() => {
                if ((panGesture.State == UIGestureRecognizerState.Began || panGesture.State == UIGestureRecognizerState.Changed) && (panGesture.NumberOfTouches == 1)) {
                    velocity = panGesture.VelocityInView (this);
                    Console.WriteLine ("Touch-enabled Pan gesture velocity: " + velocity);
                } else if (panGesture.State == UIGestureRecognizerState.Ended) {
                    // Gesture ended, but auto-panning could still be going... 
                }
            });
            AddGestureRecognizer (panGesture);
        }

        [Export ("gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:")]
        public bool ShouldRecognizeSimultaneously (UIGestureRecognizer gestureRecognizer, UIGestureRecognizer otherGestureRecognizer)
        {
            if (gestureRecognizer is UIPanGestureRecognizer) {
                var panRecognizer = (UIPanGestureRecognizer)gestureRecognizer;
                velocity = panRecognizer.VelocityInView (this);
                Console.WriteLine ("gestureRecognizer velocity: " + velocity);
            } else if (otherGestureRecognizer is UIPanGestureRecognizer) {
                var panRecognizer2 = (UIPanGestureRecognizer)otherGestureRecognizer;
                CGPoint beginvelocity = panRecognizer2.VelocityInView(this);
                if (beginvelocity.X != 0 && beginvelocity.Y != 0)
                    velocity = panRecognizer2.VelocityInView (this);
                Console.WriteLine ("otherGestureRecognizer velocity: " + velocity);
            } else {
                // What should we do here?
            }
            return true;
        }

        public override bool GestureRecognizerShouldBegin (UIGestureRecognizer gestureRecognizer)
        {
            UIPanGestureRecognizer panGesture = gestureRecognizer as UIPanGestureRecognizer;
            if (panGesture != null) {
                CGPoint beginvelocity = panGesture.VelocityInView(this);
                if (beginvelocity.X == 0 && beginvelocity.Y == 0) {
                    Console.WriteLine ("!!!! ShouldBegin velocity not reset !!!!");
                } else {
                    velocity = beginvelocity;
                }
                Console.WriteLine ("GestureRecognizerShouldBegin velocity: " + velocity);
            }
            return true;
        }
    }
}