jqueryでアニメーションを正確に並行して実行するにはどうすればよいですか?
-
03-07-2019 - |
質問
jqueryのアコーディオンプラグインに似たjqueryでアコーディオンウィジェットを作成しようとしています。ハンドルを上ではなくそれぞれのコンテンツの下に表示したいという違い。私のアコーディオンは、開いているコンテンツセクションの高さを減らすと同時に、クリックされたコンテンツセクションの高さを増やすことで機能します。 こちらの例を投稿しました。私の問題は、アニメーションがまったく同時に開始されず、顕著な「ジャンプ」が発生することです。 2番目のアニメーションが開始されるまでにわずかな遅延があるためです。
Scriptaculousには、 Effect.Parallel という関数を使用して作成できます。アニメーション効果の配列とそれらを並行して実行します。残念ながら、jqueryと似たようなものを見つけることができないようです。
jqueryの個別のdivで正確な並列アニメーションを実行する方法はありますか?
編集:私はこのアコーディオンウィジェットをコーディングする別の方法にも興味があります。だから、他の方法で人々がうまくいくと思うものがあれば、私はそれを受け入れます。
解決
もう一つの答え、できれば私の最後の答え...
残念なことに、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);
}
});
その後、元のAccordionクラスは、新しいメソッドを使用するためにanimateメソッドでのみ変更する必要があります。
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>
今後追加する可能性のあるものがいくつかあります。 -キューに入れられたアニメーション -他のタイプのスタイル(色など)のアニメーション
他のヒント
John Resigが同期アニメーションサンプルを投稿しました(説明はありません。色付きのボックスをクリックしてください)。コントロールにそれを適用する方法を理解するには多少の作業が必要になる場合がありますが、開始するには良い場所です。
これは、実行中のアニメーションを並行して解決するわけではありませんが、ジッターなしで期待される動作を再現します。アニメーションの数を減らすために、ハンドルの内側にセクションを配置しました。 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>
並列アニメーションの本当に素晴らしいソリューションを提供してくれた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);
}
});
あなたの問題はタイミングではなく、ピクセルの分数分割だと思います。このコードを試してみると、Firefox 3ではハンドル1と2で滑らかに見えますが、他のコードでは滑らかではありませんが、クロムではまだびくびくしています。
active
.animate({ height: "100px" })
.siblings(".section")
.animate({ height: "0px" });
要素の位置を静的または絶対にすることを考えましたか? 2つの要素の位置を移動するだけであれば、他の要素がジャンプすることを心配する必要はありません。ちょっと待って、例を作ってみます。
更新:John ResigのsyncAnimateプラグインを使用しなくなりました。最終的な解決策については、後の回答をご覧ください
私は自分のプロジェクトで採用している最終的な作業ソリューションを提供したかっただけです。 John Resigが書いた
このコードは:
- 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");
}
});
適切なキューとスコープを使用してjqueryで並列効果を実行することはできません。 Scriptaculousは、jQueryが基本的には役に立たない.queueと.animateを持っているキューとスコープで正しく動作しました。 jQueryがすぐに使用できるのは、いくつかのスタイル属性をDOMにプッシュすることだけですが、Scriptaculousはエフェクトで可能なことの全範囲をカバーします。
Scriptaculousを使用する必要があり、John ResigはjQuery.fxを再考する必要があります。彼はscripty2.comを確認する必要があります。