-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy path14-animations.md.erb
271 lines (185 loc) · 14.4 KB
/
14-animations.md.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
---
title: Animacje
slug: animations
date: 0014/01/01
number: 14
contents: Co dzieje się za kulisami Meteora podczas zamiany dwóch elementów DOM.|Jak animować zmianę kolejności postów.|Jak animować wstawianie nowych postów.
paragraphs: 58
---
Opanowaliśmy już głosowanie w czasie rzeczywistym, punktację i ranking. Niestety te usprawnienia doprowadziły do nieprzyjemnego interfejsu użytkownika, ponieważ posty skaczą po stronie głównej. Użyjemy animacji aby pozbyć się tego efektu i wygładzić interfejs.
### Meteor i DOM
Zanim dojdziemy do prawdziwej zabawy (przemieszczania elementów na stronie), należy zrozumieć jak Meteor współpracuje z DOM (ang. Document Object Model -- kolekcją elementów HTML które składają się na zawartość strony).
Główną zasadą o której należy pamiętać jest to, że elementy DOM *nie mogą się przemieszczać*. Mogą być wyłącznie usuwane i tworzone (miej na uwadze, że jest to ograniczenie DOM, a nie Meteora). Aby więc spowodawać iluzję zamiany elementów A i B Meteor usunie element B i wstawi całkiem nową kopię (B') przed elementem A.
Nie ułatwia to animacji, ponieważ nie można po prostu animować elementu B na nową pozycję, ponieważ B zniknie zaraz po przerenderowaniu strony (co jak wiemy dzieje się natychmiastowo dzięki reaktywności). Zamiast tego należy animować nowo stworzony B', który przemieszcza się z początkowej pozycji B' do nowej pozycji przed elementem A.
Aby zamienić miejscami posty A i B (umieszczone w miejscach odpowiednio p1 i p2) przejdziemy przez kolejne kroki:
1. Usunięcie B
2. Utworzenie B' przed A w DOM
3. Przesunięcie B' na pozycję p2
4. Przesunięcie A na pozycję p1
5. Animowanie A na pozycję p2
6. Animowanie B' na pozycję p1
Poniższy diagram wyjaśnia szczegółowo te kroki:
<%= diagram "animation_diagram", "Zamiana dwóch postów", "pull-center" %>
Miej na uwadze, że dla kroków 3 i 4 nie *animujemy* A i B' do ich pozycji ale "teleportujemy" je natychmiastowo. Ponieważ dzieje się to natychmiastowo, daje to iluzję tego, że element B nie został usunięty i odpowiednio umieści oba elementy do annimacji na nowe pozycje.
Na szczęście Meteor zatroszczy się za nas o kroki 1 & 2, zatem musimy martwić się wyłącznie o kroki od 3 do 6.
Co więcej, w krokach 5 i 6 wszystko co robimy, to przesuwanie elementów do ich docelowych miejsc. W związku z tym musimy się martwić wyłącznie o kroki 3 i 4 tj. wysłanie elementów do startowych punktów animacji.
### Dopasowanie czasu animacji
Do tej pory mówiliśmy o tym *jak* animować posty ale nie *kiedy* je animować.
Dla kroków 3 i 4 odpowiedź brzmi: podczas renderowania callback'a `rendered` szablonu w środku managera `post_item.js`, który jest wywoływany przy każdej zmianie własności posta (w naszym przypadku rankingu).
Kroki 5 i 6 są bardziej podchwytliwe. Pomyśl o nich w następujący sposób: jeżeli każesz perfekcyjnie myślącemu robotowi poruszać się na północ przez 5 minut, a następnie po zakończeniu poruszać się na południe przez 5 minut, prawdopodobnie wydedukował by, że skończy w tej samej pozycji i zamiast martwić swoją energię wcale nie zmieniłby pozycji.
Jeżeli chcesz się upewnić, że robot będzie się poruszał przez całe 10 minut, należy *poczekać* aż przebył pierwsze 5 minut, i *wtedy* kazać mu wrócić.
Przeglądarka działa w podobny sposób: jeżeli damy jej 2 instrukcje jednocześnie, nowe współrzędne zajmą miejsce starych i nic by się nie wydarzyło. Inaczej mówiąc, nie bylibyśmy w stanie ich animować.
Meteor nie zapewnia callbacka `justAfterRendered` (ang. `zaraz po renderowaniu`), ale możemy go symulować używając `Meteor.defer()` który bierze funkcję i odracza jej wykonanie aż do momentu zarejestrowania jej jako inne zdarzenie.
### Pozycjonowanie CSS
Aby animować posty, które są zamieniane kolejnością na stronie, musimy wejść w terytorium CSS. W związku z tym szybko przedstawimy umieszczanie elementów za pomocą CSS.
Elementy na stronie domyślnie używają **statycznego** pozycjonowania. Statycznie pozycjonowane elementy naturalnie wpasowują się w stronę i ich współrzędne nie mogą być zmieniane lub animowane.
**Względne** pozycjonowanie oznacza z innej strony, że element także jest naturanie wpasowany w stronę, ale może być umieszczony z przesunięcięm *względnym do pozycji początkowej*.
**Bezwzględne** pozycjonowanie idzie o jeden krok dalej i pozwala nadać określony współrzędne x/y dla danego elementu względnie do punktu początkowego **dokumentu** lub **pierwszego bezwzględnego lub względnie przesuniętego elementu nadrzędnego**.
Użyjemy pozycjonowania względnego aby animować posty. Zatroszczyliśmy się za Ciebie o kod CSS, ale jeżeli chcesz to zrobić sam, wystarczy dodać poniższy kod do arkusza stylów (ang. stylesheet):
~~~css
.post{
position:relative;
transition:all 300ms 0ms ease-in;
}
~~~
<%= caption "client/stylesheets/style.css" %>
Ułatwia to znacznie kroki 5 i 6: wszystko co należy zrobić, to ustawić `top` na `0px` (wartość domyślną) i nasze posty automatycznie przesuną się na ich "normalną" pozycję.
Oznacza to, że jedynym wyzwaniem jest znalezienie miejsca *z którego* je animować (kroki 3 i 4) względnie do ich nowej pozycji. Mówiąc inaczej jak bardzo je przesunąć. Nie jest to również trudne: prawidłowy offset to po prostu poprzednia pozycja postu minus jego nowa pozycja.
<% note do %>
### Pozycja bezwględna (Position:absolute)
Moglibyśmy również użyć `position:absolute` z względnym elementem nadrzędnym do pozycjonowania elementów. Wielkim minusem bezwzględnie pozycjonowanych elementów jest to, że są całkowicie usuwane z przepływu strony, powodując zwinięcie nadrzędnego kontenera tak jak gdyby byłby pusty.
To w konsekwencji oznacza, że potrzebowalibyśmy sztucznie ustawić wysokość kontenera za pomocą JavaScript zamiast postawić przeglądarce na naturalne rozmieszczenie elementów. W związku z tym, gdzie tylko można najlepiej używać pozycjonowania względnego.
<% end %>
### Pamięć absolutna.
Pozostaje jeszcze jeden problem. Podczas gdy element A zostaje w DOM i może dzięki temu "pamiętać" poprzednią pozcyję, element B przeżywa reinkarnację jako element B' i ma wyczyszczoną pamięć o poprzedniej pozycji.
Na szczęście Meteor radzi sobie z tym problemem przez udostępnienie objektu **instancji szablonu** w callbacku `rendered`. Za oficjalną [dokumentacją Meteora](http://docs.meteor.com/#template_rendered):
> W callbacku `this` odności się do obiektu instancji szablonu, który jest unikatowy do tego wystąpienia szablonu i zachowany przy renderowaniu.
Zatem co zrobimy, to odnajdziemy obecną pozycję posta na stronie i następnie zapamiętamy tą pozycję w obiekcie instancji szablonu. W ten sposób, nawet wtedy gdy post jest usuwany i ponownie tworzony jesteśmy w stanie określić skąd go animować.
Instancje szablonu pozwalają również na dostęp do danych kolekcji przez własność `data`. Okaże się to pomocne do ustalenia rankingu posta.
### Ranking Postów
Mówiliśmy poprzednio o rankingu postów ale "ranking" właściwie nie istnieje jako właściwość posta, ponieważ jest jedynie konsekwencją ułożenia postów w kolekcji. Jeżeli chcemy być w stanie animować posty zgodnie z ich rankingiem, będziemy musieli w jakiś sposób wyczarować tą własność znikąd.
Zauważ, że nie możemy wstawić tej własności "ranking" bezpośrednio do bazy danych, ponieważ ranking jest względną własnością zależną od sposobu sortowania postów (np. post może być pierwszy w rankingu jeżeli jest sortowany po dacie publikacji lub trzeci gdy jest posortowany względem punktów).
Idealnie wstawilibyśmy tą własność w kolekcje `newPosts` i `topPosts` ale Meteor jeszcze nie dostarcza wygodnego mechanizmu pozwalającego to osiągnąć.
Zatem wstawimy "ranking" jako ostatni możliwy krok managera szabloku `postList`:
~~~js
Template.postsList.helpers({
postsWithRank: function() {
this.posts.rewind();
return this.posts.map(function(post, index, cursor) {
post._rank = index;
return post;
});
}
});
~~~
<%= caption "/client/views/posts/posts_list.js" %>
<%= highlight "2~8" %>
Zamiast prostego zwracania kursora `Posts.find({}, {sort: {submitted: -1}, limit: postsHandle.limit()})` jak w poprzednim helperze `posts`, helper `postsWithRank` używa dostępnego kursora i dodaje własność `_rank` do każdego z jego dokumentów.
Nie zapomnij uaktualnić szablonu `postList`:
~~~html
<template name="postsList">
<div class="posts">
{{#each postsWithRank}}
{{> postItem}}
{{/each}}
{{#if nextPath}}
<a class="load-more" href="{{nextPath}}">Load more</a>
{{/if}}
</div>
</template>
~~~
<%= caption "/client/views/posts/posts_list.html" %>
<%= highlight "3" %>
<% note do %>
### A teraz bądź tak miły i przewiń wstecz
Meteor jest jednym z najbardziej przemyślanych i najnowocześniejszych z dostępnych frameworków webowych, ale jedna z jego funcji przywodzi na myśl lata 90te i kasety magnetowidowe: funkcja `rewind()`.
Za każdym razem, gdy używasz kursora z `forEach()`, `map()`, czy `fetch()` będziesz zmuszony przewinąć kursor do tyłu zanim będzie mógł być ponownie użyty.
W niektórych przypadkach jest lepiej prewencyjnie `przewinąć` kursor niż ryzykować pojawienie się buga.
<% end %>
### Składając wszystko razem
Można teraz złożyć wszystkie elementy razem używając callbacka szablonu `rendered` managera `post_item.js` do uruchomienia naszej animacji:
~~~js
Template.postItem.helpers({
//...
});
Template.postItem.rendered = function(){
// animacja postu na nową pozycję
var instance = this;
var rank = instance.data._rank;
var $this = $(this.firstNode);
var postHeight = 80;
var newPosition = rank * postHeight;
// jeżeli element posiada currentPosition (tzn. nie jest pierwszy raz renderowany)
if (typeof(instance.currentPosition) !== 'undefined') {
var previousPosition = instance.currentPosition;
// oblicz różnicę między starą i nową pozycją i wyślij tam element
var delta = previousPosition - newPosition;
$this.css("top", delta + "px");
}
// rysuj w starej pozycji..
Meteor.defer(function() {
instance.currentPosition = newPosition;
// przenieś element na pozycję początkową
$this.css("top", "0px");
});
};
Template.postItem.events({
//...
});
~~~
<%= caption "/client/views/posts/post_item.js" %>
<%= highlight "5~27" %>
<%= commit "14-1", "Dodano animację uporządkowania postów." %>
Nie powinno być trudne rozumienie tego, jeżeli spojrzysz na nasz poprzedni diagram.
Zwróć uwagę, że skoro ustawiamy własność `currentPosition` instancji szablonu w callbacku `defer`, oznacza to że ta własność nie będzie istniała podczas pierwszego renderowania fragmentu szablonu. Nie jest to jednak problemem, ponieważ i tak nie jesteśmy zainteresowani animowaniem pierwszego renderowania.
Teraz otwórz swoją stronę w przeglądarce i zacznij dodawać głosy. Powinieneś zaobserować delikatne przemieszczanie się postów niczym podczas baletu!
### Animacja Nowych Postów
Nasze posty teraz odpowiednio się przemieszczają, ale nie mieliśmy jeszcze żadnej animacji *nowego posta*. Zamiast prostego wstawiania nowego posta, użyjmy efektu ściemniania podczas wstawiania.
Jest to bardziej skomplikowane, niż wydaje się na pierwszy rzut oka. Problem polega na tym, że callback Meteora `rendered` jest wołany w dwóch osobnych przypadkach:
1. Kiedy nowy szablon jest wstawiany w DOM
2. Za każdym razem gdy zmianie ulegają dane, z których korzysta szablon.
Wyłącznie przypadek 1 powinien być animowany, chyba że masz na celu zmiany interfejsu użytkownia w choinkę przy każdej zmianie danych.
Upewnijmy się, że tylko animujemy posty kiedy są nowe a nie kiedy są ponownie renderowane z powodu zmiany danych, z których korzystają. Już sprawdzamy istnienie zmiennej instancji (która jest ustawiana tylko po pierszym renderowaniu szablonu), zarem wystarczy wrócić do naszego callbacka `rendered` i dodać blok `else`:
~~~js
Template.postItem.helpers({
//...
});
Template.postItem.rendered = function(){
// animacja postu na nową pozycję
var instance = this;
var rank = instance.data._rank;
var $this = $(this.firstNode);
var postHeight = 80;
var newPosition = rank * postHeight;
// jeżeli element posiada currentPosition (tzn. nie jest pierwszy raz renderowany)
if (typeof(instance.currentPosition) !== 'undefined') {
var previousPosition = instance.currentPosition;
// oblicz różnicę między starą i nową pozycją i wyślij tam element
var delta = previousPosition - newPosition;
$this.css("top", delta + "px");
} else {
// jest to pierwsze renderowanie, więc ukryj element
$this.addClass("invisible");
}
// rysuj w starej pozycji..
Meteor.defer(function() {
instance.currentPosition = newPosition;
// przenieś element na pozycję początkową
$this.css("top", "0px").removeClass("invisible");
});
};
Template.postItem.events({
//...
});
~~~
<%= caption "/client/views/posts/post_item.js" %>
<%= highlight "19~22,28" %>
<%= commit "14-2", "Ściemnianie elementów podczas ich rysowania." %>
Zauważ, że że `removeClass("invisible")` którą dodaliśmy w funkcji `defer()` będzie uruchamiana podczas każdego renderowania. Ale będzie robiła cokolwiek wyłącznie gdy klasa `.invisible` jest obecna dla elementu, a będzie to prawdziwe wyłącznie podczas pierwszego renderowania elementu.
<% note do %>
### CSS i JavaScript
Jak już zapewne zauważyłeś, używamy klasy CSS `.invisible` aby wyzwolić animację zamiast bezpośrednio animować własność CSS `opacity` jak robiliśmy w przypadku `top`. Jest to spowodowane tym, że dla `top` potrzebna była animacja własności do określonej wartości, która zależała na danych instancji.
Z innej strony, chcemy wyłącznie pokazać element niezależnie od jego danych. Ponieważ przechowywanie CSS z dala od kodu JavaScript jest dobrą praktyką, dodamy wyłącznie tutaj klasę tutaj a ustalimy szczegóły animacji w akruszu stylów (kodzie CSS).
<% end %>
W końcowym efekcie powinniśmy mieć taką animację, jaką chcieliśmy. Teraz uruchom własną aplikację i spróbuj jak to działa! Możesz również zmieniać klasy `.posts` i `posts.invisible` aby sprawdzić, czy istnieją inne ciekawe przejścia animacji.
Wskazówka: [CSS easing functions](http://matthewlein.com/ceaser/) jest dobrym miejscem na start!