【问题标题】:How can I get jquery to execute animations in exact parallel?如何让 jquery 完全并行执行动画?
【发布时间】:2009-05-01 14:57:47
【问题描述】:

我正在尝试在 jquery 中创建一个类似于jquery's accordion plugin 的手风琴小部件,不同之处在于我希望句柄出现在它们各自内容的下方而不是上方。我的手风琴的工作原理是降低打开内容部分的高度,同时增加点击内容部分的高度。我发布了一个示例here。我的问题是动画没有在完全相同的时间开始,并且由于第二个动画开始之前的轻微延迟而出现了明显的“跳跃”。

Scriptaculous 有一个名为Effect.Parallel 的函数,允许您创建一组动画效果并并行执行它们。不幸的是,我似乎找不到与 jquery 类似的东西。

有没有办法可以在 jquery 中的单独 div 上运行精确的并行动画?

编辑:我对这个手风琴小部件的替代编码方法很感兴趣。因此,如果有任何其他人们认为可行的方法,我对此持开放态度。

【问题讨论】:

标签: javascript jquery jquery-ui


【解决方案1】:

还有一个答案,希望是我的最后一个......

不幸的是,John Resig 的 syncAnimate 方法无法满足我想要制作的手风琴式动画效果。虽然它在 Firefox 上运行良好,但我无法让它在 IE 或 Safari 上顺利运行。

话虽如此,我决定硬着头皮编写自己的动画引擎来执行简单的并行动画。类代码使用 jquery 函数,但不是 jquery 插件。另外,我只将它设置为做大小/位置动画,这就是我所需要的。

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);
    }
});

那么只需在 animate 方法中修改原有的 Accordion 类即可使用新的调用。

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");
    }
});

HTML 仍然以同样的方式调用:

<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>

我以后可能会添加一些内容: - 排队动画 - 其他类型样式(颜色等)的动画

【讨论】:

【解决方案2】:

John Resig 发布了 synchronized animation sample(没有说明,请单击彩色框)。弄清楚如何将它应用到您的控件可能需要一些工作,但这可能是一个不错的起点。

【讨论】:

  • 事实证明,在我的手风琴上使用它相对容易,而且效果很好。额外的好处是它允许我将所有逻辑排除在标记之外,并将其全部保存在 javascript 中。
【解决方案3】:

这并不能解决并行运行的动画,但是它可以重现您的预期行为而不会出现抖动。我将部分放在手柄内以减少动画的数量。您可以使用 andSelf() 使代码更小,但会更难阅读。您需要进行一些样式调整。

<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>

【讨论】:

  • 此代码有效!它很平滑,没有多余的动作。
  • 玩了一段时间后,我决定使用 Corbin March 发布的解决方案。
【解决方案4】:

感谢 Adam Plumb 为并行动画提供了非常棒的解决方案。不过我有一个小问题,那就是它以某种方式保存了早期动画中的角色,我通过在将规则添加到 init 函数之前将规则设置为 {} 来解决这个问题。不过,它可能可以以更好的方式完成。我还添加了一个在动画完成时调用的回调函数。

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);
    }
});

【讨论】:

    【解决方案5】:

    我认为您的问题不是时间而是像素的分数划分。如果您尝试这段代码,它在句柄 1 和 2 上看起来很流畅,但在 Firefox 3 中却不是其他代码,但在 chrome 中看起来仍然很跳跃。

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

    您是否考虑过将元素的位置设为静态或绝对?如果您只移动两个元素的位置,则不必担心其他元素的跳跃。给我一秒钟,我会试着举个例子。

    【讨论】:

      【解决方案6】:

      更新:我不再使用 John Resig 的 syncAnimate 插件。最终解决方案见我稍后的回答

      我只是想提供我在项目中采用的最终工作解决方案。它使用了 John Resig 写的syncAnimate plugin(由 Corbin March 发布)。

      此代码将:

      • 从 CSS 中读取和使用节高
      • 允许您通过选项对象设置动画持续时间和默认活动部分。
      • 自动检测手柄相对于截面的位置并进行相应调整。因此,您可以将句柄移动到标记中某个部分的上方或下方,而不必更改 js 代码。

      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");
          }
      });
      

      【讨论】:

      • 经过更多检查后,这对于非 FF 浏览器仍然不能很好地工作。仍然有明显的抖动。我已经能够使用 scriptaculous 的 Effect.Parallel 函数成功地做到这一点。但我仍在寻找一种使用 jquery 的方法。
      【解决方案7】:

      您不能在具有适当队列和范围的 jquery 中执行并行效果。 Scriptaculous 在队列和范围方面做得很好,而另一方面,jQuery 有 .queue 和 .animate ,它们基本上是无用的组合。 jQuery 开箱即用的唯一好处是在 dom 上推送一些样式属性,而 Scriptaculous 涵盖了所有可能的效果。

      您需要使用 Scriptaculous,John Resig 应该重新考虑 jQuery.fx,他应该看看 scripty2.com。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2013-04-03
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多