2

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.

Camputer
  • 387
  • 2
  • 16

2 Answers2

2

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;
        }
Camputer
  • 387
  • 2
  • 16
0

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;
        }
    }
}
SushiHangover
  • 73,120
  • 10
  • 106
  • 165
  • I tried this, but what seems to be happening is that the second time you make a gesture (while moving), the `GestureRecognizerShouldBegin` is called with the default pan gesture recognizer (reporting 0), then the velocity used is the *last* reported velocity from the "new" pan gesture recognizer. In other words, the new pan gesture is holding a stale value from the previous gesture, so that at the time the "regular" pan gesture fails (reports 0), the new pan gesture has not yet "began" and updated its value. I can't figure out how to get the custom one to fire the `ShouldBegin` method... – Camputer Oct 27 '15 at 13:25
  • I've just also tried removing the default pan gesture recognizer from the scroll view, which leaves only the new pan gesture recognizer. I see the messages that it enters the state "Began", but with only that gesture assigned to the scroll view, the scroll view never fires the `GestureRecognizerShouldBegin` method, nor does it move at all any more. There's something I'm doing wrong to "connect" this gesture to the view I think. – Camputer Oct 27 '15 at 13:39
  • I don't think I'm doing anything special, this is the entire UIScrollView subclass, I threw it on a View (AddSubView) and added a really big image ( UIImage via UIImageView) in order to have something to pan. I normally stay away from messing with velocitys now..., users always seem to frown when their experience altered from the norm... What is your use-case on using GestureRecognizerShouldBegin? Maybe another way to do what you need(?) – SushiHangover Oct 27 '15 at 13:53
  • 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. – Camputer Oct 27 '15 at 14:00
  • I copied your code exactly, it's just that it's giving an "old" velocity. Leave only the log output in the `GestureRecognizerShouldBegin` method, then scroll downward then upward. You will notice that the initial pan has a negative Y velocity (down): `GestureRecognizerShouldBegin velocity: {X=73.03223, Y=-985.935}`, then the second gesture shows the "zero case" where it uses the stale velocity from the other recognizer: `!!!! ShouldBegin velocity not reset !!!!`, `GestureRecognizerShouldBegin velocity: {X=265.2679, Y=-1527.014}` (also downward), then the new gesture recognizer kicks in: – Camputer Oct 27 '15 at 14:05
  • `Touch-enabled Pan gesture BEGAN velocity: {X=-153.3697, Y=587.9171}` (upward) - but by this time it's too late, the `ShouldBegin` method has already come and gone. Basically, like I said the newly added gesture begins *after* the default pan gesture of the scroll view. So any velocity we save from this new gesture recognizer is not applicable at the time the default pan gesture "fails" (fails because it has zero velocity). In fact, if you continually pan in alternate directions, you always hit the "zero" case and you'll see the old velocity, which is actually in the opposite direction. – Camputer Oct 27 '15 at 14:07