jquery / jquery-mousewheel

A jQuery plugin that adds cross-browser mouse wheel support.
Other
3.9k stars 1.69k forks source link

osx inertia #36

Closed annam closed 5 years ago

annam commented 12 years ago

hi!

when using a trackpad or a magic mouse multiple events are triggered for every touch-scroll, sometimes taking as long as 1-2 seconds to stop, depending the scrolling "speed" and due to the inertia setting on mac osx.

I'm using mousewheel in a scenario where I move through news items on mousewheel and ideally I only want to move by on one each mousewheel action on windows, one each touch-scroll gesture on a mac trackpad.

The multiple events triggered make this impossible to do. I've tried unbinding and rebinding but as soon as I rebind the events triggered by the touch action before the unbind keep getting detected. I've tried filtering the delta to only scroll if it's between 2 values but its making the scroll very unreliable. Rebinding mousewheel only after the inertia events stop is also an impossibility because it would mean only allowing one scroll every two seconds.

do you have any ideas on how to overcome this?

danro commented 12 years ago

Underscore's debounce function is a good solution here. http://documentcloud.github.com/underscore/#debounce

You would set up your callback passing the 'immediate' parameter like this: myCallback = _.debounce(myCallback, 200, true);

riccardolardi commented 12 years ago

I was having the exact same problem here. I've been trying to manipulate/lower the delta (by multiplying it with some negative threshold value) to achieve some "slower" scrolling and to deal with osx scroll inertia. Now looking into underscore's (actually using lo-dash.js) debounce function I was able to make the scrolling more controllable. Thanks for pointing that out!

yoannmoinet commented 11 years ago

I found out that the magic number here was 40. Let me explain myself. It seems that with the trackpad (probably with the magic mouse too) the delta get increased 40 times. And it stays that way even if you take back your normal mouse and scroll with it later. So what I did, simply :

b.mousewheel(function(evt,delta,deltaX,deltaY){
    if(Math.abs(deltaY)>=40)
        deltaY/=40;
    if(Math.abs(deltaX)>=40)
        deltaX/=40;

    //Do your stuff here.
});
DigitalWheelie commented 10 years ago

Any recent, all-encompassing solution w/ the latest version, that will work when the Apple Magic Mouse is present, but also if not, on Windows, tablets and phones (iOS and otherwise)?

Some of these ideas seem to help a bit, or in certain circumstances, but then fall apart on others. Can't seem to find a pattern...

Although has some interesting possibilities: http://stackoverflow.com/questions/17798091/osx-inertia-scrolling-causing-mousewheel-js-to-register-multiple-mousewheel-even

Yet also, not quite THE answer.

brandonaaron commented 10 years ago

The issue @NeekGerd brought up is actually related to #66.

brandonaaron commented 10 years ago

I added some functionality that can help with this issue in the 3.2.x branch.

$(...).on('mousewheel', { mousewheel: { behavior: 'throttle', delay: 100 } }, function(event) { ... });

or

$(...).on('mousewheel', { mousewheel: { behavior: 'debounce', delay: 100 } }, function(event) { ... });

brandonaaron commented 10 years ago

I went ahead and bumped this to a 4.0.x branch instead of the previously mentioned 3.2.x branch because the level of changes are pretty extreme.

brandonaaron commented 10 years ago

In experimenting with this feature I noticed that it might be desirable to prevent the default behavior for all the mouse wheel events that are essentially ignored due to the throttling. In addition I was thinking about utilizing this concept of putting the settings in the data param for a few other nice-to-have features as well... So I think I might change up the setting names a little based on how that work goes.

Also thinking about alternative ways to add this behavior. Possibly just expose a factory-style method that can build the desired delayed handler.

brandonaaron commented 10 years ago

Another update on this. You can now pass the following as settings for the mousewheel event:

{ mousewheel: { debounce: { delay: 200 } } { mousewheel: { throttle: { delay: 200 } }

Or you can roll with the defaults by just setting to true like this:

{ mousewheel: { debounce: true } } { mousewheel: { throttle: true } }

Be default it will not prevent the default nor stop the propagation but you can do both like this:

{ mousewheel: { debounce: { preventDefault: true } } { mousewheel: { throttle: { preventDefault: true } }

There is another feature that I added as well... intent. It work similar to the popular hoverIntent plugin.

{ mousewheel: { intent: { delay: 300, sensitivity: 7 } }

Or use the defaults and just pass true:

{ mousewheel: { intent: true } }

You can even use both the intent feature and the throttle/debounce but only preventDefault once the user has expressed intent to use the mousewheel on that particular object.

{ mousewheel: { intent: true, throttle: { preventDefault: true } }

I'm still doing lots of testing and exploring of these features/ideas and they very well could change again. All of this work is happening in the 4.0.x branch.

brandonaaron commented 10 years ago

I just landed leading, trailing, and maxDelay options for throttle/debounce settings in the 4.0.x branch. The maxDelay setting only applies to debounce. By default leading is false and trailing is true for debounce while both leading and trailing are true for throttle.

mikaa123 commented 10 years ago

awesome man. keep up the good work.

jashwant commented 10 years ago

Great update on this @brandonaaron . debounce stops the execution for given seconds. But what does the throttle do then ? Can you create a simple demo, where I can see how I can use those and test ?

miguelpeixe commented 10 years ago

@jashwant throttle caches the event action and do one at a time, by the delay you specified. debounce doesn't cache, executes only 1 and throw away the subsequent actions until the next release (timeout of the delay specified)

miguelpeixe commented 10 years ago

I'm still having issues controlling macosx mousewheel event. I can't use debounce feature because it waits the specified ms to start, I need it to start immediatelly and only allow to scroll again after some time.

I'm able to do that normally on Windows and Linux (any browser) with my own code. On MacOSX feels erratic, some times it works, but with a "strong" mousewheel, it executes the event again before the time. No idea why, how or how to stop it.

Here's a link

Here's the code:

var enableRun = true;

var runSections = function(delta) {

    if(enableRun) {

        if(delta < 0) {
            // up
            self.sly.prev();
        } else if(delta > 0) {
            // down
            self.sly.next();
        }

        enableRun = false;

        setTimeout(function() {
            enableRun = true;
        }, 800);

    }

};

var homeScroll = function(event) {

    var delta = event.deltaY;

    if(self.sly.initialized) {

        var items = self.sly.items;
        var current = self.sly.rel;
        var isLastItem = (items.length - 1 === current.lastItem);

        if(isLastItem) {

            if(delta < 0) {

                runSections(delta);

                event.stopPropagation();
                event.preventDefault();

            }

            if(!enableRun) {
                event.stopPropagation();
                event.preventDefault();
            }

        } else {

            runSections(delta);

            event.stopPropagation();
            event.preventDefault();

        }

    }

};

$(window).on('mousewheel', homeScroll);
brandonaaron commented 10 years ago

@miguelpeixe you can turn on a leading debounce behavior. By default leading is false and trailing is true for debounce. There is also a maxDelay option for the debounce behavior.

$(...).on('mousewheel', { mousewheel: { debounce: { leading: true, trailing: false, maxDelay: 500 } }, fn);

Related code for the leading/trailing default settings: https://github.com/brandonaaron/jquery-mousewheel/blob/4.0.x/jquery.mousewheel.js#L206-L207

miguelpeixe commented 10 years ago

Thanks @brandonaaron, what does maxDelay do?

brandonaaron commented 10 years ago

If the event is debounced for maxDelay milliseconds it will force a fire of the event.

brandonaaron commented 10 years ago

@miguelpeixe Did the additional settings for debounce work for your particular use-case?

miguelpeixe commented 10 years ago

@brandonaaron actually no, my client decided to disable our custom wheel feature but I'm still very interested in solving this out. Such a simple thing, causing so much stress, that's weird.

Do you know what I'm trying to do? if you do and want to solve this, would be nice to have some help!

In my case, I don't really want to force a wheel event, I just need to control it to happen only on the given ms (if the user wants to, with leading turned on). I couldn't manage to do that with the available settings. The closest I got was with the code I previously posted here (worked on every os and browser, except macosx).

fenicento commented 10 years ago

Hi, I'm having the same issue of miguelpeixe. I can control events on all browsers on windows; on mac Os x I'm using a magic mouse or a trackpad: on firefox everything goes fine, while on chrome (espcially with strong wheel actions) things go out of control and preventDefault seems to be ignored... the code is pretty similar to miguelpeixe's one, I can post it if you think it can be useful...

edeesims1 commented 10 years ago

I currently have the following

$(document).on('mousewheel', { mousewheel:{ debounce: { leading: true, trailing: false, preventDefault: true } } }, function(e) {

But when I scroll with a trackpad, I am still getting 3 events firing. I am trying to use this to scroll to specific positions on my page. Works great with a mouse but not with a trackpad.

Any suggestions?

brandonaaron commented 10 years ago

@edeesims1 The default delay is only 100ms. Try tweaking the delay to see if that gets you a better result.

marcbelletre commented 10 years ago

Hi everyone,

I went here a few times ago to find a solution and I finally found one by myself. I wanted to share it with you so here it is. Everytime the event is fired, I check if the previous event was fired less than 100ms before and if the event were fired less than 10 times. Else, I don't allow scrolling so the event can be automatically fired only 10 times by the browser. If the difference between the two times is greater than 100ms, we can conclude that the event was fired by the user, so we can allow scrolling.

function mouseHandle(event) {
    newDate = new Date();
    var scrollAllowed = true;

    if( wheel < 10 && (newDate.getTime()-oldDate.getTime()) < 100 ) {
        scrollPos -= event.deltaY*(10-wheel);
        wheel++;
    }
    else {
        if( (newDate.getTime()-oldDate.getTime()) > 100 ) {
            wheel = 0;
            scrollPos -= event.deltaY*60;
        }
        else {
            scrollAllowed = false;
        }
    }

    oldDate = new Date();

    if( scrollAllowed ) {
        // do your stuff here
    }
}

I hope this could help. In my case it seems to work pretty good !

thSoft commented 10 years ago

Wow, @Prettyletter, your workaround works perfectly! Thank you! @brandonaaron, have you considered incorporating it to the code?

jcsuzanne commented 10 years ago

+100!!!! Finally ;)

In the "debounce" function way, i update the script like this

var killbounce = function(_func, _wait) { var wheel = 0 , oldDate = new Date() , scrollPos = 0 ; return function() {

        var
            context         =   this
        ,   args            =   arguments
        ,   event           =   args[0]
        ,   getDeltaY       =   event.deltaY
        ,   newDate         =   new Date()
        ,   newTime         =   newDate.getTime()
        ,   oldTime         =   oldDate.getTime()
        ,   scrollAllowed   =   true
        ;

        if( wheel < 10 && (newTime-oldTime) < _wait ) {
            scrollPos -= getDeltaY*(10-wheel);
            wheel++;
        }
        else {
            if( (newTime-oldTime) > _wait ) {
                wheel = 0;
                scrollPos -= getDeltaY*60;
            }
            else {
                scrollAllowed = false;
            }
        }
        oldDate = new Date();
        if( scrollAllowed ) {
            _func.apply(context, args);
        }
    }
};

sorry for the trash display...

brandonaaron commented 10 years ago

Sorry I haven't had the time to dig into this some more. Looks promising. Might try to incorporate this into the next major release with the idea of the settings so that it can be configurable. I don't think I'll have time until next month though.

gstaruk commented 10 years ago

Hi there, just wondering when the new version of the plugin is due for release?

majorwaves commented 10 years ago

I think I'm trying to achieve the same things as @Prettyletter and @jcsuzanne, but I'm having trouble putting in their code. Could someone hold my hand and tell me how they implemented that in relation to the '$(document).on('mousewheel'…' line.

marcbelletre commented 10 years ago

@majorwaves you just have to do this :

$('body').on('mousewheel', function(event) { mouseHandle(event); });
WorldWideWebb commented 10 years ago

I made a simple hack if you just want something that works like a swipe event (i.e., horizontal scrolling with no inertia). I needed to make a horizontal-scrolling pane to show several infographics in sequence (much like a slider). I'll be adding additional code to handle swipe events on touch devices, but this seems to work really well for desktops.

This basically runs a bit of code once when the scrolling starts (in either direction), then disables it until the speed slows down to 1 and runs out. Code is then reenabled to swipe again. This also seems to work well for cases in which you swipe a bunch of times in a row before the delta slows down. Haven't tested this on very many browsers yet though.

isMoving = false;
$('.element-that-scrolls').on('mousewheel', function(e) {

    // advance the slides
    if (e.deltaX > 1) {
        if (!isMoving) {
            isMoving = true;
            // do your stuff here   
        }

    }

    // retreat
    else if (e.deltaX < -1) {
        if (!isMoving) {
            isMoving = true;
            // do your stuff here
        }
    }

    else { isMoving = false; }

});
thany commented 10 years ago

My solution/workaround to this problem is kind of like this:

dosomescrolling(event.deltaY / Math.abs(event.deltaY));

This way deltaY is divided by itself, or its positive self when negative. That way, any negative value becomes -1, any positive value becomes 1, and 0 stays 0.

It's probably not the "proper" solution and it completely ignores what OSX folks may be used to, but since where styling scrollbars and dropdowns too, I thought it'd be fine. At least until I figure out a more proper solution :)

jdart commented 10 years ago

I'd like to throw my solution into the mix, it's based on the debounce idea mentioned earlier. It's only for vertical scrolling but could easily be adapter to horizontal scrolling. It tried to detect changes in the acceleration of the mousewheel events, when acceleration is first detected the mousewheel is fired. It could probably be tightened up a lot but it works well for my use case of a vertical slider.

var isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;

function averageChange(arr) {

    var change_sum = 0, 
        avg = null, 
        i = 1;

    for (; i < arr.length; i++) {
        change_sum += (arr[i] - arr[i-1]);
    }

    avg = change_sum / arr.length;

    return avg;
}

var killbounce = function(_func, _wait)
{
    var waitEnd = new Date(0),
        lastState = 'decel',
        deltaHistory = [];

    return function(event) {

        var context = this,   
            args = arguments,
            now = new Date(),
            deltaAverage;

        if ( ! isMac) {
            _func.apply(context, args);
            return;
        }

        deltaHistory.push(Math.abs(event.deltaY));
        deltaHistory = deltaHistory.slice(-10);
        deltaAverage = averageChange(deltaHistory);

        if (now.getTime() < waitEnd.getTime())
            return;

        if (deltaAverage > 1 && lastState !== 'accel') { 

            lastState = 'accel';
            waitEnd = new Date(now.getTime() + _wait);
            _func.apply(context, args);

        } else if (deltaAverage <= 1 && lastState !== 'decel') {

            lastState = 'decel';
            waitEnd = new Date(now.getTime() + _wait);
        }
    }
};
sunaku commented 10 years ago

Nice! :+1: Note that you can simplify your platform check to:

var isMac = /mac/i.test(navigator.platform);
thegeminisociety commented 10 years ago

Hey all,

I am trying to only capture an initial scroll up or down, ignoring ANY type of after scroll inertia.

I need the event to fire right away, meaning if a user scrolls up or down, it should fire the event without waiting.

I got version 4.0 and have tried to implement some of these solutions but have not had any luck.

Here is my current line of code: trigger.on('mousewheel', handleMouseEvents);

Any suggestions using the new debounce, throttle methods would be great! Thanks

thegeminisociety commented 10 years ago

UPDATE: I came up with a fairly simple solution that checks to see if the inertia has kicked in by checking to see if the delta is closer to zero. Seems to work pretty well for my application!

// Handle Mouse Events function handleMouseEvents(event) {

    var fireEvent;

    if(allowScroll == true) {

        var newDelta = event.deltaY;

        if(oldDelta != null) {

            //check to see if they differ directions
            if(oldDelta < 0 && newDelta > 0) {
                fireEvent = true;
            }

            //check to see if they differ directions
            if(oldDelta > 0 && newDelta < 0) {
                fireEvent = true;
            }

            //check to see if they are the same direction
            if(oldDelta > 0 && newDelta > 0) {

                //check to see if the new is higher
                if(oldDelta < newDelta) {
                    fireEvent = true;
                } else {
                    fireEvent = false;
                }
            }

            //check to see if they are the same direction
            if(oldDelta < 0 && newDelta < 0) {

                //check to see if the new is lower
                if(oldDelta > newDelta) {
                    fireEvent = true;
                } else {
                    fireEvent = false;
                }
            }

        } else {

            fireEvent = true;
        }           

        oldDelta = newDelta;

        if(fireEvent == true) {

            // perform your function here
                            fireFunction();
        }

    }
}
Djules commented 10 years ago

@jdart Your solution looks very nice ! But how do you implement this ?

cdekok commented 10 years ago

I see many solutions on this issue which one is the best one? and where is the 3.2 branch i only see a 4.x branch is it moved?

brandonaaron commented 10 years ago

The 3.2.x is still the primary build and is the master branch. The 4.0.x branch is more an experiment right now... in hindsight it would have been more appropriate to not designate a version already.

As for the best solution... I'm not sure yet. I'm not even sure if a solution should be in the main code or just an example implementation or maybe even just an optional include. Probably end up being a recommendation for 3.2.x and a setting for the 4.0.x branch.

I haven't had the time yet to move forward with 4.0.x yet... and actually I've been contemplating extracting out the core mousewheel event normalization into its own generic lib that doesn't require any library. This jQuery plugin would then utilize that generic lib... Time hasn't allowed me any progress on any of this though.

normanmatrix commented 10 years ago

I would like to add that this issue is particularily annoying if you implement custom virtual scrolling via page scrollTop animations.

The innertia scrolling seems to be affected by the page animation, leading to even more prolonged phases of event bursts.

brandonaaron commented 10 years ago

@jdart that is a really interesting way to tackle throttling. I think it could be simplified to only look at the last delta to decide if it is accelerating or not though. Another thing to consider is that some older (very old now) browsers/devices do not convey acceleration or force, just a single event with a constant delta. I believe, haven't tested, your solution doesn't take that into consideration. Probably didn't need to support those old browsers.

I'm wondering if you tried out or saw the code in the 4.0.x branch that has throttling/debounce built in? It'd be great to know if that code specifically didn't work for your use-case. I'm wondering if I should try to incorporate this notion of acceleration/deceleration into the 4.0.x branch.

brandonaaron commented 10 years ago

@jdart I take that back... I don't think it can be simplified to only look at the last delta.

brandonaaron commented 10 years ago

One of my devices (a logitech mouse) has a free spin mode on the mousewheel and it can give erratic results when flicked in free spin mode. Has a clear acceleration pattern but then all the sudden will have a smaller delta in the middle of acceleration. The trackpad is really quite consistent about its acceleration/deceleration flow.

brandonaaron commented 10 years ago

I worked on this a lot today (testing solutions posted here and some of my own ideas) and just haven't come up with a solution that works well enough to pull into the plugin nor recommend yet. I think there are a lot of different use-cases/needs surrounding the inertia events and that is why we are seeing such a diverse set of solutions in this thread that solve the individuals problem.

A very common use-case I think is wanting just one event per "flick" of the mousewheel. Sometimes the inertia events can run on for several seconds depending on the hardware being used. I believe the debounce settings in 4.0.x branch solve this very nicely. I'm not sure if the other throttle settings provide a lot of value but are easy to keep since it reuses a lot of the debounce related code.

One of the shortcomings (or features) of the debounce settings in the 4.0.x branch are related to multiple "flicks" while the first "flick" is still not finished. We don't really have a reliable way to track the acceleration and deceleration curves to know (at least for now) when we should consider a new "flick" has happened.

It seems easy... just keep a running history of the deltas and do some comparisons. However, I haven't been able to come up with the right comparisons that seem to work right. The deltas can be quite erratic depending on the device being used. The trackpad is usually pretty consistent with its acceleration and deceleration curve. Some other devices I have aren't so consistent and jump up and down quite a bit. Perhaps this is an acceptable edge case to just accept. I'm not quite ready to just accept it yet.

Let me know if I'm off track here. I can't really say that I have the same use cases that I think some of y'all have, so I'm doing my best to infer what the problems are.

rafaelbuiu commented 9 years ago

I know this is not the best of the existing solutions, but it solved my problem. The 2000ms delay worked fine for both MAC and PC machines. Hope it can help someone!

Just use the isMoving var inside your code block to check if the scrolling is running or not.

var isMoving=false;

$(document).bind("mousewheel DOMMouseScroll MozMousePixelScroll", function(event, delta) {
   event.preventDefault();
   if (isMoving) return;
   navigateTo();
});

function navigateTo(){
   isMoving = true;
   setTimeout(function() {
    isMoving=false;
   },2000);
}
JakedUp commented 9 years ago

I too would love to see this resolved. This is a major annoyance right now in my vertical swiper!

msimpson commented 9 years ago

So I ran across this issue recently when attempting to put together a vertically stacked website, where each card in the deck is full bleed. I wanted the scroll wheel to move only card to card, but on OSX (of course) I soon saw the issues found above. Double events, huge delays, etc. all manifested from attempting to use debounce and other timing mechanisms as a solution.

Frustrated, I decided to dive deep into the events. Trying to find some common thread to separate each gesture and a way to reduce each inertial chain into a singular intent. Sadly, no information contained within the events could help (as I'm sure others have found). So being that all that's left is the behavior of the inertial algorithm itself. I started to graph it. And after a few more failed attempts, like keying on valleys or slope (like jamesmartis proposed), I found peak detection works best.

See here: https://gist.github.com/msimpson/cd7eca7907132c984171 and Fiddle, here: http://jsfiddle.net/n7bk6pb9/1/

JakedUp commented 9 years ago

Matt, this looks great. I'm having a bit of troubling wrapping my head around the code and seeing how I can apply it in my similar situation. How would we use this any only fire an event on the peak?

msimpson commented 9 years ago

The method works by implementing a sliding window. Watch the mouse wheel events and keep a history/sample of the nine most recent deltas. Shifting the window forward after each event. Kind of like riding a wave on a surf board. And on each new event, try to detect a peak (hasPeak) in the sample of deltas. Like: 3, 3, 9, 15, 23, 15, 9, 3, 3.

If that is found, return true from hasPeak. But, delay peak detection for an entire sample width (ignore 9 to 10 future events). That way, as the window slides over the peak it's quieted from registering true multiple times.

The other bit of code in the event handler (second line, below):

if (hasPeak()) hash();
else if ((deltas[8] == null || Math.abs(deltas[8]) == 120) && Math.abs(delta) == 120) hash();

That checks to see if the first delta in history is null or at the standard 120 delta while the current delta is at 120 as well. And If so, it assumes a normal mouse wheel event and fires.

@JakedUp Here is a demo specifically for implementation: http://jsfiddle.net/n7bk6pb9/7/

lemonlab commented 9 years ago

Thank you @msimpson for this solution & example. But i don't see how can i "hack" the version 3.1.12 to make this work with your "hasPeak" function.

msimpson commented 9 years ago

@FrancoisMartin I'm not terribly sure if this should be added into the plugin, itself. The plugin provides compatible mouse wheel events with normalized deltas. Whereas my code profiles this provided data. Nonetheless, I have constructed a hacky edition of the jquery.mousewheel.js found here:

https://gist.github.com/msimpson/44941935eae5505bf1ec

The addition is very passive. It is enabled or disabled via the inertialProfiling setting, and is enable by default. When an inertial chain occurs and a peak is found, a small property is added to the event object called inertialGesture which is set to the current timestamp. That should be sufficient for triggering on inertial gestures and separating them.

This is only an example, of course.