devguydavid
devguydavid

Reputation: 4149

jQuery Animation Triggered By User Interaction Depends on Previous Animation Completion

I have a timeline that can be zoomed by clicking a zoom in or zoom out button. This timeline doesn't all fit on the screen at once, so it is a scrollable div. When the user clicks to zoom, I want the position in the timeline to be the same, so I calculate a new scrollTop for the scrollable div. Here's a simplified version of what I'm doing:

var self = this;
...
this.zoomIn = function() {
    var offset = $("#scrollable").scrollTop();
    self.increaseZoomLevel(); // Assume this sets the correct zoom level
    var newOffset = offset * self.zoomLevel();
    $("#scrollable").scrollTop(newOffset);
};

This works fine. Now I'd like to animate the scrolling. This almost works:

var self = this;
...
this.zoomIn = function() {
    var offset = $("#scrollable").scrollTop();
    self.increaseZoomLevel(); // Assume this sets the correct zoom level
    var newOffset = offset * self.zoomLevel();
    $("#scrollable").animate({ scrollTop: newOffset });
};

It works if it's clicked once. However, if a second call to zoomIn happens while the animation is still running, the newOffset calculation is wrong because the offset is set to scrollTop() before scrollTop() is correct since the animation is still manipulating it.

I've tried to use jQuery's queue in various ways to make this calculation happen first, and that seems to work sometimes:

var self = this;
...
this.zoomIn = function() {
    $("#scrollable").queue(function(next) {
        var offset = $("#scrollable").scrollTop();
        self.increaseZoomLevel(); // Assume this sets the correct zoom level
        var newOffset = offset * self.zoomLevel();
        next();
    }).animate({ scrollTop: newOffset });
};

I think I'm just not understanding queue properly. How do I keep everything in order even when zoomIn is called repeatedly and rapidly? I want:

zoomIn x 2 clicks

to give me:

calculate 1 -> animate 1 start -> animate 1 finish -> calculate 2 -> animate 2 start -> animate 2 finish

and not

calculate 1 -> animate 1 start -> calculate 2 -> animate 1 finish -> animate 2 start -> animate 2 finish

Because then animate 2 is based on incorrect calculations.

Thanks!

Upvotes: 1

Views: 158

Answers (3)

devguydavid
devguydavid

Reputation: 4149

I very much appreciate the answers given. They both would work, but my unwritten requirements included animations that completed entirely as well as no loss of clicks. I know, I should have been more thorough in my question.

Anyway, I believe I have a solution that fits both of those requirements using jQuery queues. There were a couple of things I didn't realize about queues that I learned that got me going in the right direction. The biggest thing is this from the jQuery .animate docs:

When a custom queue name is used the animation does not automatically start...

This allowed me to have complete control over the queue. I believe this is similar to (or maybe exactly what) @RobinJonsson's comment meant.

var top = 0;
var animating = false;

function calcAndAnimate(top) {
    $("#block").queue("other", function() {
        // Calculations go here
        animating = true;
        // This kicks off the next animation
        $("#block").dequeue("other");
    });
    $("#block").animate({
        top: top
    }, {
        duration: 2000,
        queue: "other",
        complete: function () {
            animating = false;
            // No need; it looks like animate dequeues for us, which makes sense.
            // So the next calculation will be kicked off for us.
            //$("#block").dequeue("other");
        }
    });
}

$("#queueButton").click(function() {
    top += 20;
    calcAndAnimate(top);
    if (!animating) {
        // Initial animation, need to kick it off
        $("#block").dequeue("other");
    }
});

There's a working example with log messages showing the enforced order at http://jsfiddle.net/cygnl7/6h3c2/3/

Upvotes: 0

omma2289
omma2289

Reputation: 54629

Here's an implementation of @RobinJonsson's comment, which would be my proposed solution too, using a boolean to allow a new zoom action only after the previous animation is complete:

var self = this;
...
this.zooming = false;
this.zoomIn = function() {
    if(!self.zooming){
        self.zooming = true;
        var offset = $("#scrollable").scrollTop();
        self.increaseZoomLevel(); // Assume this sets the correct zoom level
        var newOffset = offset * self.zoomLevel();
        $("#scrollable").animate({ scrollTop: newOffset },function(){
            self.zooming = false;
        });
    }
};

Upvotes: 1

lucido-media.de
lucido-media.de

Reputation: 140

Hm... what about: stop(true,true)? See: http://api.jquery.com/stop/

var self = this;
...
this.zoomIn = function() {
  var offset = $("#scrollable").stop(true,true).scrollTop();
  self.increaseZoomLevel(); // Assume this sets the correct zoom level
  var newOffset = offset * self.zoomLevel();
  $("#scrollable").animate({ scrollTop: newOffset });
};

Upvotes: 1

Related Questions