最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How can I get jquery to execute animations in exact parallel? - Stack Overflow

programmeradmin2浏览0评论

I'm trying to create an accordion widget in jquery similar to jquery's accordion plugin, with the difference that I want the handles to appear below their respective content instead of above. My accordion works by decreasing the height of the open content section while at the same time increasing the height of the clicked content section. I've posted an example here. My problem is that the animations aren't started at exactly the same time, and there is a noticeable "jump" due to the slight delay before the second animation is started.

Scriptaculous has a function called Effect.Parallel that allows you to create an array of animation effects and execute them in parallel. Unfortunately, I can't seem to find something similar with jquery.

Is there a way I can run precise parallel animations on separate divs in jquery?

Edit: I'm as much interested in alternative methods of coding this accordion widget. So if there is any other method people think would work I'm open to that.

I'm trying to create an accordion widget in jquery similar to jquery's accordion plugin, with the difference that I want the handles to appear below their respective content instead of above. My accordion works by decreasing the height of the open content section while at the same time increasing the height of the clicked content section. I've posted an example here. My problem is that the animations aren't started at exactly the same time, and there is a noticeable "jump" due to the slight delay before the second animation is started.

Scriptaculous has a function called Effect.Parallel that allows you to create an array of animation effects and execute them in parallel. Unfortunately, I can't seem to find something similar with jquery.

Is there a way I can run precise parallel animations on separate divs in jquery?

Edit: I'm as much interested in alternative methods of coding this accordion widget. So if there is any other method people think would work I'm open to that.

Share Improve this question edited May 1, 2009 at 18:15 Adam Plumb asked May 1, 2009 at 14:57 Adam PlumbAdam Plumb 3,7888 gold badges37 silver badges34 bronze badges 2
  • you can do this in SGIL I think, but that's a markup language and a bit of a different topic. – Gordon Gustafson Commented May 1, 2009 at 16:51
  • See my final solution here: stackoverflow.com/questions/811750/… – Adam Plumb Commented May 7, 2009 at 16:57
Add a comment  | 

7 Answers 7

Reset to default 4

One more answer, hopefully my last one...

Unfortunately, John Resig's syncAnimate method is not quite up to snuff for the accordion-type animation I want to do. While it works great on Firefox, I couldn't get it working smoothly on IE or Safari.

With that said, I decided to bite the bullet and write my own animation engine that does simple parallel animations. The class-code uses jquery functions but is not a jquery plugin. Also, I've only set it up to do size/position animations, which is all I need.

ParallelAnimations = function(animations, opts){
    this.init(animations, opts);
};

$.extend(ParallelAnimations.prototype, {
    options: {
        duration: 250
    },
    rules: {},

    init: function(animations, opts){
        // Overwrite the default options
        $.extend(this.options, opts);

        // Create a set of rules to follow in our animation
        for(var i in animations){
            this.rules[i] = {
                element: animations[i].element,
                changes: new Array()
            };

            for(var style in animations[i].styles){

                // Calculate the start and end point values for the given style change
                var from = this.parse_style_value(animations[i].element, style, "");
                var to = this.parse_style_value(animations[i].element, style, animations[i].styles[style]);

                this.rules[i].changes.push({
                    from: from,
                    to: to,
                    style: style
                });
            }
        }

        this.start()
    },

    /*
     * Does some parsing of the given and real style values
     * Allows for pixel and percentage-based animations
     */
    parse_style_value: function(element, style, given_value){
        var real_value = element.css(style);

        if(given_value.indexOf("px") != -1){
            return {
                amount: given_value.substring(0, (given_value.length - 2)),
                unit: "px"
            };
        }

        if(real_value == "auto"){
            return {
                amount: 0,
                unit: "px"
            };
        }

        if(given_value.indexOf("%") != -1){
            var fraction = given_value.substring(0, given_value.length - 1) / 100;

            return {
                amount: (real_value.substring(0, real_value.length - 2) * fraction),
                unit: "px"
            };
        }

        if(!given_value){
            return {
                amount: real_value.substring(0, real_value.length - 2),
                unit: "px"
            };
        }
    },

    /*
     * Start the animation
     */
    start: function(){
        var self = this;
        var start_time = new Date().getTime();
        var freq = (1 / this.options.duration);

        var interval = setInterval(function(){
            var elapsed_time = new Date().getTime() - start_time;

            if(elapsed_time < self.options.duration){
                var f = elapsed_time * freq;

                for(var i in self.rules){
                    for(var j in self.rules[i].changes){
                        self.step(self.rules[i].element, self.rules[i].changes[j], f);
                    }
                }
            }
            else{
                clearInterval(interval);

                for(var i in self.rules){
                    for(var j in self.rules[i].changes)
                        self.step(self.rules[i].element, self.rules[i].changes[j], 1);
                }
            }
        }, 10);
    },

    /*
     * Perform an animation step
     * Only works with position-based animations
     */ 
    step: function(element, change, fraction){

        var new_value;
        switch(change.style){
            case 'height':
            case 'width':
            case 'top':
            case 'bottom':
            case 'left':
            case 'right':
            case 'marginTop':
            case 'marginBottom':
            case 'marginLeft':
            case 'marginRight':
                new_value = Math.round(change.from.amount - (fraction * (change.from.amount - change.to.amount))) + change.to.unit;
                break;
        }

        if(new_value)
            element.css(change.style, new_value);
    }
});

Then the original Accordion class only needs to be modified in the animate method to make use of the new call.

Accordion = function(container_id, options){
    this.init(container_id, options);
}

$.extend(Accordion.prototype, {
    container_id: '',
    options: {},
    active_tab: 0,    
    animating: false,
    button_position: 'below',
    duration: 250,
    height: 100,

    handle_class: ".handle",
    section_class: ".section",

    init: function(container_id, options){
        var self = this;
        this.container_id = container_id;
        this.button_position = this.get_button_position();

        // The height of each section, use the height specified in the stylesheet if possible
        this.height = $(this.container_id + " " + this.section_class).css("height");

        if(options && options.duration)    this.duration = options.duration;
        if(options && options.active_tab)  this.active_tab = options.active_tab;

        // Set the first section to have a height and be "open"
        // All the rest of the sections should have 0px height
        $(this.container_id).children(this.section_class).eq(this.active_tab)
            .addClass("open")
            .css("height", this.height)
            .siblings(this.section_class)
            .css("height", "0px");

        // figure out the state of the handles
        this.do_handle_logic($(this.container_id).children(this.handle_class).eq(this.active_tab));

        // Set up an event handler to animate each section
        $(this.container_id + " " + this.handle_class).mouseover(function(){

            if(self.animating)
                return;

            self.animate($(this));
        });
    },

    /*
     * Determines whether handles are above or below their associated section
     */     
    get_button_position: function(){
        return ($(this.container_id).children(":first").hasClass(this.handle_class) ? 'above' : 'below');
    },

    /*
     * Animate the accordion from one node to another
     */ 
    animate: function(handle){
        var active_section = (this.button_position == 'below' ? handle.prev() : handle.next());    
        var open_section = handle.siblings().andSelf().filter(".open");

        if(active_section.hasClass("open"))
            return;

        this.animating = true;

        // figure out the state of the handles
        this.do_handle_logic(handle);

        // Close the open section
        var arr = new Array();
        arr.push({
            element: open_section,
            styles: {
                "height": "0px"
            }
        });
        arr.push({
            element: active_section,
            styles: {
                "height": this.height
            }
        });
        new ParallelAnimations(arr, {duration: this.duration});

        var self = this;
        window.setTimeout(function(){
            open_section.removeClass("open");
            active_section.addClass("open");
            self.animating = false;
        }, this.duration);
    },

    /*
     * Update the current class or "state" of each handle
     */ 
    do_handle_logic: function(handle){
        var all_handles = handle.siblings(".handle").andSelf();
        var above_handles = handle.prevAll(this.handle_class);
        var below_handles = handle.nextAll(this.handle_class);

        // Remove all obsolete handles
        all_handles
            .removeClass("handle_on_above")
            .removeClass("handle_on_below")
            .removeClass("handle_off_below")
            .removeClass("handle_off_above");

        // Apply the "on" state to the current handle
        if(this.button_position == 'below'){
            handle
                .addClass("handle_on_below");
        }
        else{
            handle
                .addClass("handle_on_above");
        }

        // Apply the off above/below state to the rest of the handles
        above_handles
            .addClass("handle_off_above");

        below_handles
            .addClass("handle_off_below");
    }
});

The HTML is still called the same way:

<html>
<head>
    <title>Parallel Accordion Animation</title>
    <script type="text/javascript" src="jquery.js"></script>
    <script type="text/javascript" src="ui.js"></script>
    <script type="text/javascript">
    $(document).ready(function(){
        new Accordion("#accordion");
    });
    </script>
    <style type="text/css">
    #accordion{
        position: relative;
    }
    #accordion .handle{
        width: 260px;
        height: 30px;
        background-color: orange;
    }
    #accordion .section{
        width: 260px;
        height: 445px;
        background-color: #a9a9a9;
        overflow: hidden;
        position: relative;
    }
    </style>
</head>
<body>

<div id="accordion">
    <div class="section"><!-- --></div>
    <div class="handle">handle 1</div>
    <div class="section"><!-- --></div>
    <div class="handle">handle 2</div>
    <div class="section"><!-- --></div>
    <div class="handle">handle 3</div>
    <div class="section"><!-- --></div>
    <div class="handle">handle 4</div>
    <div class="section"><!-- --></div>
    <div class="handle">handle 5</div>
</div>

</body>
</html>

There are a few things I may add in the future: - Queued Animations - Animations for other types of styles (colors,etc)

John Resig posted a synchronized animation sample (no instructions, click a colored box). It might take some work to figure out how to apply it to your control, but it could be a good place to start.

This does not solve running animations in parallel however it reproduces your expected behavior without the jitter. I placed section inside of handle to reduce the number of animations. You could use andSelf() to make the code smaller but it would be harder to read. You will need to make some style tweaks.

<html>
<head>
    <title>Accordion Test</title>
    <script type="text/javascript" src="jquery.js"></script>
    <script type="text/javascript">

    $(document).ready(function(){
        $("#accordion .handle").click(function(){
            var open = $(this).parent().children(".section, .open");
            var active = $(this);

            if (!active.hasClass("open"))
            {
                if (active.hasClass("up"))
                {
                  console.log("up");
                  active.animate({top:"+=100"}).removeClass("up");
                  active.nextAll(".handle").andSelf().filter(".up").animate({top:"+=100"}).removeClass("up");
                  $(".section", active).slideUp();
                  $(".section", active.nextAll()).slideUp();
                  $(".section", active.prev()).slideDown();
                }
                else
                {
                  active.prevAll(".handle").not(".up").animate({top:"-=100"}).addClass("up");
                  $(".section", active.prev()).slideDown();
                }

                open.removeClass("open");
                active.addClass("open");
            }
        });
    });

    </script>
    <style type="text/css">
        #accordion{
            width: 200px;
            position:relative;
        }
        #accordion .section{
            width: 196px;
            margin-left: 2px;
            height: 100px;
            background-color: #b9b9b9;
            display:none;
        }
        #accordion .handle{
            width: 200px;
            height: 30px;
            background-color: #d9d9d9;
            border: 1px solid black;
            cursor: pointer;
            cursor: hand;
            position: absolute;
        }
        #accordion .handle .header {
            height: 30px;
        }
    </style>
</head>
<body>

<div id="accordion">
    <div id="s1" class="section open" style="display:block">This is section 1</div>

    <div class="handle open" style="top:100;">
      <div class="header">handle 1</div>
      <div class="section">This is section 2</div>
    </div>

    <div class="handle" style="top:130;">
      <div class="header">handle 2</div>
      <div class="section">This is section 3</div>
    </div>

    <div class="handle" style="top:160;">
      <div class="header">handle 3</div>
      <div class="section">This is section 4</div>
    </div>

    <div class="handle" style="top:190;">
      <div class="header">handle 4</div>
      <div class="section">This is section 5</div>
    </div>

    <div class="handle" style="top:220;">
      <div class="content">handle 5</div>
    </div>
</div>

</body>
</html>

Thanks Adam Plumb for a really great solution to parallel animations. I had a small problem with it though and that was that it somehow saved roles from earlier animations i fixed that by setting the rules to {} before adding them in the init function. It can probably be done in a better way though. I also added a callback function that is called when the animation have finished.

ParallelAnimations = function(animations, opts){
    this.init(animations, opts);
};

$.extend(ParallelAnimations.prototype, {
    options: {
        duration: 250,
        callback: null
    },
    rules: {},

    init: function(animations, opts){
        // Overwrite the default options
        $.extend(this.options, opts);

        // Create a set of rules to follow in our animation
        this.rules = {}; // Empty the rules.
        for(var i in animations){
            this.rules[i] = {
                element: animations[i].element,
                changes: new Array()
            };

            for(var style in animations[i].styles){

                // Calculate the start and end point values for the given style change
                var from = this.parse_style_value(animations[i].element, style, "");
                var to = this.parse_style_value(animations[i].element, style, animations[i].styles[style]);

                this.rules[i].changes.push({
                    from: from,
                    to: to,
                    style: style
                });
            }
        }

        this.start()
    },

    /*
     * Does some parsing of the given and real style values
     * Allows for pixel and percentage-based animations
     */
    parse_style_value: function(element, style, given_value){
        var real_value = element.css(style);

        if(given_value.indexOf("px") != -1){
            return {
                amount: given_value.substring(0, (given_value.length - 2)),
                unit: "px"
            };
        }

        if(real_value == "auto"){
            return {
                amount: 0,
                unit: "px"
            };
        }

        if(given_value.indexOf("%") != -1){
            var fraction = given_value.substring(0, given_value.length - 1) / 100;

            return {
                amount: (real_value.substring(0, real_value.length - 2) * fraction),
                unit: "px"
            };
        }

        if(!given_value){
            return {
                amount: real_value.substring(0, real_value.length - 2),
                unit: "px"
            };
        }
    },

    /*
     * Start the animation
     */
    start: function(){
        var self = this;
        var start_time = new Date().getTime();
        var freq = (1 / this.options.duration);

        var interval = setInterval(function(){
            var elapsed_time = new Date().getTime() - start_time;

            if(elapsed_time < self.options.duration){
                var f = elapsed_time * freq;

                for(var i in self.rules){
                    for(var j in self.rules[i].changes){
                        self.step(self.rules[i].element, self.rules[i].changes[j], f);
                    }
                }
            }
            else{
                clearInterval(interval);

                for(var i in self.rules){
                    for(var j in self.rules[i].changes)
                        self.step(self.rules[i].element, self.rules[i].changes[j], 1);
                }
                if(self.options.callback != null) {
                    self.options.callback(); // Do Callback
                }
            }
        }, 10);
    },

    /*
     * Perform an animation step
     * Only works with position-based animations
     */ 
    step: function(element, change, fraction){

        var new_value;
        switch(change.style){
            case 'height':
            case 'width':
            case 'top':
            case 'bottom':
            case 'left':
            case 'right':
            case 'marginTop':
            case 'marginBottom':
            case 'marginLeft':
            case 'marginRight':
                new_value = Math.round(change.from.amount - (fraction * (change.from.amount - change.to.amount))) + change.to.unit;
                break;
        }

        if(new_value)
            element.css(change.style, new_value);
    }
});

I think your problem isn't timing but fractional division of a pixel. If you try this code it looks smooth for handle 1 and 2 but not others in Firefox 3 but still looks jumpy in chrome.

 active
    .animate({ height: "100px" })
    .siblings(".section")
    .animate({ height: "0px" });

Have you thought about making the position of the elements static or absolute? If your only moving the position of two elements you don't have to worry about the other ones jumping. Give me a second and I'll try to make an example.

Update: I'm no longer using John Resig's syncAnimate plugin. See my later answer for the final solution

I just wanted to supply the final working solution that I'm employing on my project. It uses the syncAnimate plugin that John Resig wrote (posted by Corbin March).

This code will:

  • Read and use the section height from CSS
  • Allow you to set the animation duration, and default active section through an options object.
  • Automatically detect handle position relative to section and adjusts accordingly. So you move the handles above or below a section in the markup and not have to change the js code.

HTML

<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="ui.js"></script>

<script type="text/javascript">
$(document).ready(function(){
    new Accordion("#accordion", {active_tab: 0});
});
</script>
<style type="text/css">
#accordion .handle{
    width: 260px;
    height: 30px;
    background-color: orange;
}
#accordion .section{
    width: 260px;
    height: 445px;
    background-color: #a9a9a9;
    overflow: hidden;
    position: relative;
}

</style>

<div id="accordion">
    <div class="section">Section Code</div>
    <div class="handle">handle 1</div>

    <div class="section">Section Code</div>
    <div class="handle">handle 2</div>

    <div class="section">Section Code</div>
    <div class="handle">handle 3</div>

    <div class="section">Section Code</div>
    <div class="handle">handle 4</div>

    <div class="section">Section Code</div>
    <div class="handle">handle 5</div>
</div>

ui.js

Accordion = function(container_id, options){
    this.init(container_id, options);
}

$.extend(Accordion.prototype, {
    container_id: '',
    options: {},
    active_tab: 0,    
    animating: false,
    button_position: 'below',
    duration: 250,
    height: 100,

    handle_class: ".handle",
    section_class: ".section",

    init: function(container_id, options){
        var self = this;
        this.container_id = container_id;
        this.button_position = this.get_button_position();

        // The height of each section, use the height specified in the stylesheet if possible
        this.height = $(this.container_id + " " + this.section_class).css("height");

        if(options && options.duration)    this.duration = options.duration;
        if(options && options.active_tab)  this.active_tab = options.active_tab;

        // Set the first section to have a height and be "open"
        // All the rest of the sections should have 0px height
        $(this.container_id).children(this.section_class).eq(this.active_tab)
            .addClass("open")
            .css("height", this.height)
            .siblings(this.section_class)
            .css("height", "0px");

        // figure out the state of the handles
        this.do_handle_logic($(this.container_id).children(this.handle_class).eq(this.active_tab));

        // Set up an event handler to animate each section
        $(this.container_id + " " + this.handle_class).mouseover(function(){

            if(self.animating)
                return;

            self.animate($(this));
        });
    },

    /*
     * Determines whether handles are above or below their associated section
     */     
    get_button_position: function(){
        return ($(this.container_id).children(":first").hasClass(this.handle_class) ? 'above' : 'below');
    },

    /*
     * Animate the accordion from one node to another
     */ 
    animate: function(handle){
        var active_section = (this.button_position == 'below' ? handle.prev() : handle.next());    
        var open_section = handle.siblings().andSelf().filter(".open");

        if(active_section.hasClass("open"))
            return;

        this.animating = true;

        // figure out the state of the handles
        this.do_handle_logic(handle);

        // Close the open section
        open_section
            .syncAnimate(active_section, {"height": "0px"}, {queue:false, duration:this.duration}, '')
            .removeClass("open");

        // Open the new section
        active_section
            .syncAnimate(open_section, {"height": this.height}, {queue:false, duration:this.duration}, '')
            .addClass("open");

        var self = this;
        window.setTimeout(function(){
            self.animating = false;
        }, this.duration);
    },

    /*
     * Update the current class or "state" of each handle
     */ 
    do_handle_logic: function(handle){
        var all_handles = handle.siblings(".handle").andSelf();
        var above_handles = handle.prevAll(this.handle_class);
        var below_handles = handle.nextAll(this.handle_class);

        // Remove all obsolete handles
        all_handles
            .removeClass("handle_on_above")
            .removeClass("handle_on_below")
            .removeClass("handle_off_below")
            .removeClass("handle_off_above");

        // Apply the "on" state to the current handle
        if(this.button_position == 'below'){
            handle
                .addClass("handle_on_below");
        }
        else{
            handle
                .addClass("handle_on_above");
        }

        // Apply the off above/below state to the rest of the handles
        above_handles
            .addClass("handle_off_above");

        below_handles
            .addClass("handle_off_below");
    }
});

You can't do a parallel effect in jquery with proper queue and scope. Scriptaculous got it right with queue and scope where jQuery on the other hand has .queue and .animate that are basically useless combined. The only thing jQuery is good for out of the box is pushing some style attributes around on the dom whereas Scriptaculous covers the whole spectrum of what's possible with effects.

You need to use Scriptaculous and John Resig should rethink jQuery.fx, he should have a look at scripty2.com while he's at it.

发布评论

评论列表(0)

  1. 暂无评论