Kurs AngularJS #12 – Animacje

Angular daje nam możliwość stosowania animacji dla najczęściej używanych dyrektyw takich jak ngRepeat, ngSwitch, ngView oraz dla innych, nawet tych tworzonych przez nas za pomocą usługi $animate. Możemy z nich korzystać podczas określonych “momentów z życia” naszych dyrektyw oraz wywoływać na zawołanie. Tak jak w przypadku routingu musimy tutaj dołączyć odpowiedni moduł ngAnimate jako zależność w naszej aplikacji.

Zobaczmy więc jak to działa na prostym przykładzie.

W pliku script.js nie mamy nic do omawiania. Po prostu tworzymy moduł i dodajemy na liście jego zależności ngAnimate. Sam index.html też nie jest za specjalnie interesujący. Mamy checkbox którego zaznaczenie lub nie określa widoczność jakiejś treści. Wszystko co tam jest już znamy i wiemy jak powinno działać. I działa tylko, że treść zamiast zwyczajnie znikać i pojawiać się w ułamku sekundy robi to z ładną animacją. Tutaj właśnie tkwi zasługa ngAnimate a określone jest to tak naprawdę w style.css.

.content-area to nic specjalnego po prostu ustawiamy pewne wartości żeby nasza treść ładniej wyglądała. Później natomiast w .sample-show-hide ustawiamy wartość transition: all linear 0.5s. To tutaj mamy zapisane jak ma przebiegać animacja. Kolejnym stylem jest ustawianie opacity na 0 w przypadku kiedy .sample-show-hide ma dodatkowo klasę .ng-hide a ta z kolei nadawana jest mu właśnie wtedy kiedy nie jest widoczny.

Jak widać więc animacje w Angularze są w pełni oparte na klasach CSS. Tak długo jak możemy się odnieść do jakiegoś elementu po klasie, tak długo możemy go animować.

Zbadajmy więc kolejny przykład tym razem z użyciem ngRepeat.

W HTMLu jak widzisz znowu nie dzieje się nic specjalnego. Mamy iteracje na jakieś liście przedmiotów i przycisk który dodaje kolejną pozycję do tej listy oraz kolejny który jedną pozycję z listy usuwa.

Tutaj również nie ma żadnych nowości – zdefiniowana lista oraz dwie funkcje które odpowiednio dodają i usuwają kolejny przedmiot.

Jak widzisz iterowany obiekt ma klasę repeated-item przez którą odnosimy się do niego w CSS lub JS jeśli to tam przeprowadzamy animacje. Kiedy ngRepeat wykonuje swoją pracę za każdym razem kiedy do listy dodawany jest nowy przedmiot nadaje mu klasę ng-enter. Kiedy natomiast jest usuwany z listy ng-leave. Kiedy natomiast zmienia swoją pozycję na liście – ng-move. Dlatego też w tym CSSie możemy zobaczyć ustawienie animacji na tych właśnie klasach.

Takie samo podejście możemy zastosować używając kodu JS (żeby ułatwić sobie trochę sprawe, posłużymy się jQuery).

Zauważ, że Angular sam wie kiedy ma wykonać animacje, my ją tylko określamy. Nie możemy też określić animacji dla jednego elementu zarówno w CSS jak i w JS.

Zmiana klas i dyrektywa ngClass

Angular śledzi również obecność klas w elementach. Może odpowiednio zareagować kiedy jakaś klasa jest dodawana lub usuwana z elementu tak żeby odbyło się to płynnie. Pamiętaj jednak, że żeby Angular był w stanie uchwycić taką zmianę to nazwa klasy musi znajdować się w nawiasach klamrowych w atrybucie class lub w atrybucie ngClass.

Kod CSS trochę się różni ale zamysł jest dokładnie taki sam. Myślę więc, że już rozumiesz na jakiej zasadzie działają animacje w Angularze.

Lista dyrektyw

DyrektywaWspierane animacje
ngRepeatdodanie, przesunięcie lub usunięcie elementu
ngIfPojawienie się lub zniknięcie elementu
ngSwitchPojawienie się lub zniknięcie elementu
ngIncludePojawienie się lub zniknięcie elementu
ngViewPojawienie się lub zniknięcie elementu
ngMessage / ngMessageExpPojawienie się lub zniknięcie elementu
ngClass / {{class}}Pojawienie się lub zniknięcie elementu
ngClassEven / ngClassOddPojawienie się lub zniknięcie elementu
ngHidePojawienie się lub zniknięcie elementu (klasa ng-hide)
ngShowPojawienie się lub zniknięcie elementu (klasa ng-hide)
ngModelDodanie lub usunięcie (inne klasy)
form / ngFormDodanie lub usunięcie (inne klasy)
ngMessagesDodanie lub usunięcie (klasy ng-active / ng-inactive)

Jeśli chcesz zobaczyć pełną listę zdarzeń na których możesz dokonać animacji musisz zajrzeć do API.

Używanie animacji na swoich własnych dyrektywach

Animacji na swoich własnych dyrektywach możemy dokonywać poprzez dodanie zależności na $animate.

Klikanie na naszą dyrektywę będzie naprzemiennie dodawało i usuwało klasę clicked. Można oczywiście również zrobić to ładniej pod względem wizualnym i dodać jakieś przejście ale to tylko przykład żeby zademonstrować możliwości.

Animacja podczas ładowania strony

Domyślnie animacje w Angularze podczas ładowania strony są wyłączone. Jeśli używasz dyrektywy ngApp dzieje się to w zdarzeniu DOMContentLoaded a więc zaraz PO załadowaniu strony. Animacje są wyłączone, żeby interfejs i zawartość samej strony od razu były widoczne. W innym przypadku ze zbyt dużą ilością animacji proces ładowania mógłby być wizualnie trochę przytłaczający i wpływać na wydajność.

ngAnimate czeka aż wszystko będzie gotowe i załadowane. Czeka również na obecnie działający $digest i jeszcze jeden po nim – zapewnia to, że wszystko zostanie załadowane, skompilowane i podstawione zanim animacje się uruchomią.

Jeśli jednak mimo wszystko chcesz żeby twoje animacje zostały odpalone już w sumie podczas ładowania strony możesz włączyć samemu taką opcję.

Samodzielne włączanie, wyłączanie i przeskakiwanie wybranych animacji

Jest wiele sposobów na wyłączenie animacji zarówno globalnie jak i tylko tych wybranych. Wyłączanie tych wybranych może czasem znacząco zoptymalizować proces ładowania. Przykładowo kiedy mamy bardzo dużą tablicę iterowaną przez ngRepeat która tak naprawdę animacji nie ma, ngAnimate i tak ją sprawdzi pod tym względem żeby się upewnić czy na pewno nie trzeba w niej nic animować. To potrafi wpłynąć na wydajność.

Podczas konfiguracji

Tym sposobem możemy wywołać funkcję $animateProvider.customFilter() w bloku konfiguracyjnym naszego modułu. Jako argument przyjmuje ona filtr który później będzie używany do filtrowania animacji (w oparciu o element do animowania, rodzaj zdarzenia i opcje animacji). Tylko jeśli funkcja filtrująca zwróci true animacja zostanie wykonana. Zapewnia to całkiem sporą swobodę gdyż możemy w miarę łatwo tworzyć skomplikowane zasady co do tego które animacje mają zostać wykonane i dynamicznie je modyfikować.

Stosowanie tego podejścia jest znacznie szybsze w porównaniu do innych bo customFilter wyłączy animacje jeszcze przed sprawdzeniem ich przez inne strategie.

Inną funkcją która może zostać wykonana podczas konfiguracji jest $animateProvider.classNameFilter(). Jako jedyny argument przyjmuje on wyrażenie regularne na podstawie którego kontroluje klasy elementów i dopuszcza do animacji tylko te które taki test przejdą pozytywnie.

Podczas wykonywania

O funkcji $animate.enabled() wspomniałem już wcześniej i jak wiesz możemy dzięki niej wyłączać lub włączać animacje globalnie. Możemy jednak wyłączyć lub włączyć animacje tylko na wybranych elementach podając takie element jako pierwszy argument. Musi to być czysty element DOM lub podany przy pomocy jqLite / jQuery. W tym przypadku funkcja ta wyłącza / włącza animacje również na wszystkich jego elementach potomnych.

Przez CSS

Za każdym razem kiedy animacja się rozpoczyna, ngAnimate załącza do animowanego elementu klasę ng-animate na cały czas trwania animacji. Jeśli odpowiednio zastosujemy dla tej klasy transition możemy dzięki temu całkowicie ją wyłączyć.

Dzięki takiemu rozwiązaniu ngAnimate całkowicie zignoruje tę animację i nie będzie w ogóle próbowała jej wykonać (jednakże animacje zdefiniowane poprzez JS będą dalej się wykonywały).

Zapobieganie mruganiu przed rozpoczęciem animacji

Kiedy element zagnieżdżający ma ustawioną animacje w oparciu o klasy, jak chociażby poprzez ngClass, czasem zdarza się, że przed właściwą animacją nastąpi krótkie mrugnięcie treści – mało widoczne ale jednak jest i czasem potrafi irytować.

Jeśli chcesz tego uniknąć powinieneś zastosować klasę ng-[event]-prepare gdzie [event] to zdarzenie jakiego dotyczy ta konkretna klasa. Zostaje ona dodana do elementu od razu po zainicjowaniu animacji, ale usuwana zanim właściwa animacji się rozpocznie. Klasa dodawana jest tylko dla zdarzeń entermoveleave.

Poniżej pokażę Ci przykład który może powodować

Możliwym jest, że podczas zdarzenia enter, div .message będzie przez chwilę widoczny jeszcze przed rozpoczęciem animacji. W tym przypadku możesz dodać odpowiedni styl, żeby upewnić się, że na pewno nikt go nie zobaczy zanim ty nie będziesz tego chciał.

Zapobieganie kolizjom z istniejącymi animacji i zewnętrznymi bibliotekami

ngAnimate wychodzi zawsze z założenia, iż jeśli obsługuje animacje na jakimś elemencie to wszystkie animacje które są do tego elementu przypisane podlegają pod niego a tak niekoniecznie musi być. Może to czasem powodować kłopoty.

Przykładowo mamy jakiś element który kręci się podczas ładowania czegoś innego. Jest on kompletnie niezależny od ngAnimate ale ma na sobie ngIf dlatego ngAnimate podejrzewa, że animacji kręcenia się podlega pod niego. Więc kiedy wartość wyrażenia w ngIf zmieni się, ngAnimate zastosuje tą animację do zdarzenia enter / leave a tak nie powinno się dziać gdyż jest ona całkowicie niezależna. Oczywiście element dalej zniknie / pojawi się ale z widocznym opóźnieniem, potrzebnym na wykonanie animacji.

Również z niektórymi zewnętrznymi frameworkami może pojawić się ten sam problem, gdyż mogą nakładać jakieś swoje animacje na elementy z których korzysta również Angular.

Z tym problemem również możemy poradzić sobie z poziomu CSS, zwyczajnie wyłączając animacje dla ngAnimate.

Włączanie animacji spoza drzewa DOM

Przed animacją, ngAnimate sprawdza czy element znajduje się w drzewie DOM aplikacji. Przeważnie nie jest to problemem, gdyż większość aplikacji osadza dyrektywę ngApp na <html> bądź <body>. Problem pojawia się wtedy kiedy nasza aplikacja wykonywana jest wewnątrz jakiegoś elementu a animacja ma dotyczyć się elementu spoza jego zasięgu.

Możemy sobie jednak z tym w miarę prosto poradzić używając funkcji $animate.pin(). Dzięki niej możemy powiązać element na którym chcemy dokonać animacji z elementem zawierającym się w drzewie DOM naszej aplikacji. Robimy to podając te elementy odpowiednio jako pierwszy i drugi argument. Musimy jednak wykonać tę funkcję zanim przystąpimy do animacji na danym elemencie, gdyż w innym przypadku zwyczajnie się ona nie wykona.