QuickTip: kolejka animacji z jQuery.each() i setTimeout()

Bartek Stańkowski, .

Jakiś czas temu chciałem korzystając z funkcji each() w jQuery wyświetlić serię załadowanych AJAXem elementów jeden po drugim, z opóźnieniem po każdym kolejnym. Trafiłem na kilka różnych sposobów, ale żaden mnie nie zadowalał. O wiele za bardzo komplikowały prostą sprawę.

Dzisiaj przyszło mi do głowy rozwiązanie tak proste, że trudno uwierzyć, że nie wpadłem na nie od razu.

Na przykładzie: pokazywanie elementów listy jeden po drugim, z przerwą 300ms po każdym. Wydaje się, że wystarczy setTimeout():

$('li').each(function(){
	var $li = $(this);

	setTimeout(function() {
		$li.fadeIn(500);
	}, 300);
});

Ale to nie aż tak proste, bo w ten sposób wszystkie elementy pokażą się razem, z opóźnieniem 300ms od momentu wywołania akcji (np. klik na przycisk), a my chcemy uzyskać takie opóźnienie po animacji każdego kolejnego elementu.

Z pomocą przychodzi parametr index w funkcji each() i odrobina logiki. :) Skoro nie chcę opóźnienia przed pokazaniem pierwszego elementu, a 300ms przerwy po każdym kolejnym tzn., że potrzebuję przerwy 0, 300, 600, 900ms, itd. licząc od momentu wywołania akcji. Najprościej zrobić to mnożąc index, czyli numer elementu (0-based) przez wartość opóźnienia.

$('li').each(function(index){
	var $li = $(this);
	
	setTimeout(function() {
		$li.fadeIn(500);
	}, index * delay);
});

Takie proste. ;) Zobacz demo.

UPDATE: A jednak nie do końca.

Jak Riddle słusznie zauważył w komentarzach, odpalanie wielu timerów jednocześnie może spowolnić przeglądarkę. W moim przykładzie nie robi to różnicy, bo elementów jest tylko kilka. Przetestowałem swoje demo dla 100 elementów listy - skok zużycia procesora to maksymalnie 15% w momencie startu, a przeglądarki w ogóle nie odczuwają różnicy (Firefox 3.5, Safari 4, Opera 10 /Mac). Być może przy jeszcze większej liczbie, bądź na słabszym sprzęcie dałoby się odczuć jakieś problemy.

Tak czy inaczej, z pewnością lepiej i bezpieczniej unikać odpalania wielu setTimeout() jednocześnie.

Komentarze

Przeczytałem.

Sam timeout może mieć sens w tylko jednym przypadku, gdy masz pewność, że animacja każdego elementu będzie trwać dokładnie tyle samo. Więc twój kod zadziała, ale dla identycznych elementów z prostym fade in. Sytuacja: przychodzi klient i mówi, że chce slide down zamiast fade in, w jednym bloczku jest linia tekstu więcej i animacja się psuje. W kolejkowaniu – nie. No i JS to nie harmonogram zadań, żeby mu planować po kilka(naście) timeoutów, które czekają na wystrzał.

Wasacz - bądz realistą. Klient chce - nie ma rady ;) Chyba, że znasz lepszy sposób na kolejkowanie animacji, ja nie znam.

Wasacz: niezupełnie. Problemy mogłyby się pojawić przy dużych różnicach między czasem trwania animacji, a przerwą pomiędzy animacją kolejnych elementów. I to wcale nie poważne, po prostu elementy nakładałyby się na siebie.
A tutaj mam nad tym wszystkim pełną kontrolę.
Slide down, nawet z elementami o różnych wysokościach, nie jest problemem. Jeśli mam slideDown(500), animacja trwa pół sekundy niezależnie od wysokości elementu.

I chodziło o to, że nie znalazłem lepszego sposobu na takie animacje. Widywałem i takie na 20 linijek kodu, a też napisane w jQ i z wykorzystaniem setTimeout().

Nie za dobry sposób, ponieważ przy większej liczbie elementów możesz nieźle spowolnić przeglądarkę – odpalanie na raz n timerów nie zalicza się do dobrych praktyk.

Lepszy wybór:

    function animateNext(handle) {
      var $next = $(handle).next();
      $next.fadeIn('500', function() {
        animateNext($next);
      });
    }           

    $('#list li')
      .hide()
      .eq(0).each(function() {
        animateNext(this);
      });

Pardon, za szybko wysłałem kod. Poprawka, uwzględniająca pierwszy element:

function animateNext(handle) {
  var $next = $(handle).next();
  $next.fadeIn('500', function() {
    delayAnimation($next);
  });
}

function delayAnimation(handle) {
  setTimeout(function() {
    animateNext(handle);
  }, delay);
}       

$('#list li')
  .hide()
  .eq(0).each(function() {
    $(this).fadeIn('500', function() {
      delayAnimation($(this));
    });
  });

Edit: Teraz powinno być okej. :)

Riddle: racja, zdaję sobie z tego sprawę i nie byłem do końca przekonany do odpalania kilku timerów jednocześnie, ale przy niewielkiej liczbie elementów w ogóle się tego nie zauważa.
Jednak moje rozwiązanie było zbyt proste. :)
Dzięki za pomoc.

Riddle: Twój kod jest dobry, ale nie działa w przeglądarce IE. Co jest nie tak?

Przepraszam, działa pod IE, tylko w moim konkretnym przypadku nie działa.
Potrzebuję zastosować jeden z kodów z tego artykułu na wszystkich elementach li w pudełku o id #galeria. Oto jak zmodyfikowałem kod:

function animateNext(handle) {...}
function delayAnimation(handle) {...}

$('#galeria li')
  .hide()
  .eq(0).each(function() {
    $(this).fadeIn('500', function() {
      delayAnimation($(this));
    });
  });

Jednak gryzie się to z jakimiś ustawieniami na stronie bo nie działa.
Podaję podstronę na której ma działać efekt, jeśli ktoś chciałby mi pomóc byłbym wdzięczny.
***
Okazuje się, że efekt działa, tylko elementy <li> mają z zasady display:block. Ja natomiast potrzebuję, żeby każdy element listy był obok siebie. Float:left też nie pomaga. Jak zmienię na display:inline, to znów nie działa .hide().

Być może powinienem zamiast listy wstawić same obrazki i zastosować taki kod:

$('#galeria img')

Tak, teraz działa.

Kolejną modyfikacją efektu jest aby po wyświetleniu pierwszych 4 zdjęć zaczęły one znikać z użyciem fadeOut, zaczynając od pierwszego. W ten sposób można by zobaczyć wszystkie 18 zdjęć. Coś w rodzaju slideshow.

Podaję podstronę na której ma działać efekt, jeśli ktoś chciałby mi pomóc byłbym wdzięczny.

Myślałem też, by może wyświetlić od razu wszystkie obrazki i potem co sekundę usuwać z użyciem fadeOut() pierwszy obrazek. Zmodyfikowałem kod, ale nie działa. Jestem nowicjuszem w jquery i robię to trochę po omacku.

$('#galeria img')
   .show()
   .eq(0).each(function() {
      $(this).fadeOut('500', function() {
         delayAnimation($(this),1000);
    });
});

Gdybym miał taki kod, wystarczyłoby dodać warunek, że po usunięciu wszystkich obrazków, czyli gdy ('#galeria img:eq(18)') użyć show() i zacząć usuwać od nowa.
do funkcji delayanimation(); wprowadziłem zmienną time, która ustala czas opóźnienia, dla wygody.

Dodaj komentarz

Weryfikacja antyspamowa