Data publikacji: 17 sierpnia 2021 r., ostatnia aktualizacja: 25 września 2024 r.
Gdy przejście widoku jest uruchamiane w jednym dokumencie, nazywa się je przejściem widoku w tym samym dokumencie. Zwykle ma to miejsce w aplikacjach jednostronicowych, w których do aktualizowania DOM używa się JavaScriptu. Przejścia widoku w tym samym dokumencie są obsługiwane w Chrome od wersji 111.
Aby wywołać przejście do widoku w tym samym dokumencie, wywołaj funkcję document.startViewTransition
:
function handleClick(e) {
// Fallback for browsers that don't support this API:
if (!document.startViewTransition) {
updateTheDOMSomehow();
return;
}
// With a View Transition:
document.startViewTransition(() => updateTheDOMSomehow());
}
Po wywołaniu przeglądarka automatycznie przechwytuje zrzuty wszystkich elementów, w których zadeklarowano właściwość CSS view-transition-name
.
Następnie wykonuje przekazane wywołanie zwrotne, które aktualizuje DOM, po czym robi zrzuty nowego stanu.
Zrzuty są następnie rozmieszczane w drzewie pseudoelementów i animowane za pomocą animacji CSS. Pary migawek ze starego i nowego stanu płynnie przechodzą ze starego położenia i rozmiaru do nowego miejsca, a ich zawartość przenika się. Jeśli chcesz, możesz dostosować animacje za pomocą CSS.
Domyślne przejście: przenikanie
Domyślne przejście widoku to przenikanie, więc jest to dobre wprowadzenie do interfejsu API:
function spaNavigate(data) {
// Fallback for browsers that don't support this API:
if (!document.startViewTransition) {
updateTheDOMSomehow(data);
return;
}
// With a transition:
document.startViewTransition(() => updateTheDOMSomehow(data));
}
gdzie updateTheDOMSomehow
zmienia DOM na nowy stan. Możesz to zrobić w dowolny sposób. Możesz na przykład dodawać i usuwać elementy, zmieniać nazwy klas lub zmieniać style.
Strony zostaną płynnie przełączone:
OK, przenikanie nie jest aż tak imponujące. Na szczęście przejścia można dostosowywać, ale najpierw musisz zrozumieć, jak działa podstawowe przenikanie.
Jak działają te przejścia
Zaktualizujmy poprzedni przykładowy kod.
document.startViewTransition(() => updateTheDOMSomehow(data));
Gdy wywoływana jest funkcja .startViewTransition()
, interfejs API rejestruje bieżący stan strony. Obejmuje to zrobienie zrzutu.
Po zakończeniu wywoływana jest funkcja zwrotna przekazana do .startViewTransition()
. W tym miejscu zmienia się DOM. Następnie interfejs API rejestruje nowy stan strony.
Po zarejestrowaniu nowego stanu interfejs API tworzy drzewo pseudoelementów w ten sposób:
::view-transition
└─ ::view-transition-group(root)
└─ ::view-transition-image-pair(root)
├─ ::view-transition-old(root)
└─ ::view-transition-new(root)
::view-transition
znajduje się w nakładce, nad wszystkimi innymi elementami na stronie. Jest to przydatne, jeśli chcesz ustawić kolor tła przejścia.
::view-transition-old(root)
to zrzut ekranu ze starego widoku, a ::view-transition-new(root)
to aktywna reprezentacja nowego widoku. Oba są renderowane jako „zastąpiona treść” CSS (np. <img>
).
Stary widok animuje się z opacity: 1
do opacity: 0
, a nowy widok animuje się z opacity: 0
do opacity: 1
, tworząc efekt przenikania.
Cała animacja jest wykonywana za pomocą animacji CSS, więc można ją dostosować za pomocą CSS.
Dostosowywanie przejścia
Wszystkie pseudoelementy przejścia widoku można kierować za pomocą CSS, a ponieważ animacje są definiowane za pomocą CSS, można je modyfikować za pomocą istniejących właściwości animacji CSS. Na przykład:
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 5s;
}
Po tej zmianie przejście jest bardzo powolne:
OK, to nadal nie robi wrażenia. Zamiast tego poniższy kod implementuje przejście z użyciem wspólnej osi w Material Design:
@keyframes fade-in {
from { opacity: 0; }
}
@keyframes fade-out {
to { opacity: 0; }
}
@keyframes slide-from-right {
from { transform: translateX(30px); }
}
@keyframes slide-to-left {
to { transform: translateX(-30px); }
}
::view-transition-old(root) {
animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}
::view-transition-new(root) {
animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}
Oto wynik:
Przejście wielu elementów
W poprzedniej wersji demonstracyjnej cała strona jest objęta przejściem z użyciem wspólnej osi. W przypadku większości strony to działa, ale w przypadku nagłówka nie wygląda to dobrze, ponieważ nagłówek wysuwa się, a potem znowu wsuwa.
Aby tego uniknąć, możesz wyodrębnić nagłówek z reszty strony, aby można było go animować oddzielnie. W tym celu przypisz do elementu view-transition-name
.
.main-header {
view-transition-name: main-header;
}
Wartość view-transition-name
może być dowolna (z wyjątkiem none
, co oznacza, że nie ma nazwy przejścia). Służy do jednoznacznego identyfikowania elementu w trakcie przejścia.
W efekcie:
Teraz nagłówek pozostaje na miejscu i przenika.
Ta deklaracja CSS spowodowała zmianę drzewa pseudoelementów:
::view-transition
├─ ::view-transition-group(root)
│ └─ ::view-transition-image-pair(root)
│ ├─ ::view-transition-old(root)
│ └─ ::view-transition-new(root)
└─ ::view-transition-group(main-header)
└─ ::view-transition-image-pair(main-header)
├─ ::view-transition-old(main-header)
└─ ::view-transition-new(main-header)
Są teraz 2 grupy przejścia. Jeden dla nagłówka, a drugi dla reszty. Można je kierować niezależnie za pomocą CSS i nadawać im różne przejścia. W tym przypadku main-header
pozostawiono domyślne przejście, czyli przenikanie.
Domyślne przejście to nie tylko przenikanie, ale też przejście ::view-transition-group
:
- Pozycjonowanie i przekształcanie (za pomocą
transform
) - Szerokość
- Wysokość
Do tej pory nie miało to znaczenia, ponieważ nagłówek ma taki sam rozmiar i pozycję po obu stronach zmiany DOM. Możesz też wyodrębnić tekst z nagłówka:
.main-header-text {
view-transition-name: main-header-text;
width: fit-content;
}
fit-content
, aby element miał rozmiar tekstu, a nie rozciągał się na pozostałą szerokość. Bez tego strzałka wstecz zmniejsza rozmiar elementu tekstowego nagłówka, a nie zachowuje tego samego rozmiaru na obu stronach.
Mamy teraz 3 elementy, z którymi możemy się pobawić:
::view-transition
├─ ::view-transition-group(root)
│ └─ …
├─ ::view-transition-group(main-header)
│ └─ …
└─ ::view-transition-group(main-header-text)
└─ …
Ale znowu, po prostu używając ustawień domyślnych:
Tekst nagłówka przesuwa się w bok, aby zrobić miejsce na przycisk Wstecz.
Animowanie wielu pseudoelementów w ten sam sposób za pomocą view-transition-class
Załóżmy, że masz przejście widoku z wieloma kartami, ale też tytuł na stronie. Aby animować wszystkie karty z wyjątkiem tytułu, musisz napisać selektor, który będzie kierowany na każdą kartę z osobna.
h1 {
view-transition-name: title;
}
::view-transition-group(title) {
animation-timing-function: ease-in-out;
}
#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }
…
#card20 { view-transition-name: card20; }
::view-transition-group(card1),
::view-transition-group(card2),
::view-transition-group(card3),
::view-transition-group(card4),
…
::view-transition-group(card20) {
animation-timing-function: var(--bounce);
}
Masz 20 elementów? Musisz napisać 20 selektorów. Dodawanie nowego elementu Następnie musisz też rozwinąć selektor, który stosuje style animacji. Nie jest to zbyt skalowalne.
Symbol view-transition-class
można stosować w pseudo-elementach przejścia widoku, aby zastosować tę samą regułę stylu.
#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }
#card5 { view-transition-name: card5; }
…
#card20 { view-transition-name: card20; }
#cards-wrapper > div {
view-transition-class: card;
}
html::view-transition-group(.card) {
animation-timing-function: var(--bounce);
}
Przykład kart poniżej wykorzystuje poprzedni fragment kodu CSS. Do wszystkich kart, w tym nowo dodanych, można zastosować ten sam czas trwania za pomocą jednego selektora: html::view-transition-group(.card)
.
view-transition-class
stosuje ten sam animation-timing-function
do wszystkich kart z wyjątkiem dodanych lub usuniętych.Debugowanie przejść
Przejścia widoku są oparte na animacjach CSS, więc do debugowania przejść idealnie nadaje się panel Animacje w Narzędziach deweloperskich w Chrome.
W panelu Animacje możesz wstrzymać następną animację, a następnie przewijać ją do przodu i do tyłu. W tym czasie pseudoelementy przejścia można znaleźć w panelu Elementy.
Elementy przejściowe nie muszą być tym samym elementem DOM
Do tej pory używaliśmy view-transition-name
, aby utworzyć osobne elementy przejścia dla nagłówka i tekstu w nagłówku. To koncepcyjnie ten sam element przed zmianą DOM i po niej, ale możesz tworzyć przejścia, w których tak nie jest.
Na przykład głównemu umieszczonemu filmowi można przypisać wartość view-transition-name
:
.full-embed {
view-transition-name: full-embed;
}
Następnie, gdy klikniesz miniaturę, możesz przypisać jej ten sam atrybut view-transition-name
, ale tylko na czas przejścia:
thumbnail.onclick = async () => {
thumbnail.style.viewTransitionName = 'full-embed';
document.startViewTransition(() => {
thumbnail.style.viewTransitionName = '';
updateTheDOMSomehow();
});
};
Wynik:
Miniatura przekształci się teraz w obraz główny. Mimo że są to różne elementy (w sensie koncepcyjnym i dosłownym), interfejs API przejścia traktuje je jako to samo, ponieważ mają ten sam identyfikator view-transition-name
.
Prawdziwy kod tej zmiany jest nieco bardziej skomplikowany niż w poprzednim przykładzie, ponieważ obsługuje też powrót do strony z miniaturami. Pełną implementację znajdziesz w źródle.
Niestandardowe przejścia otwierania i zamykania
Przyjrzyj się temu przykładowi:
Pasek boczny jest częścią przejścia:
.sidebar {
view-transition-name: sidebar;
}
Jednak w przeciwieństwie do nagłówka z poprzedniego przykładu pasek boczny nie pojawia się na wszystkich stronach. Jeśli oba stany mają pasek boczny, pseudoelementy przejścia wyglądają tak:
::view-transition
├─ …other transition groups…
└─ ::view-transition-group(sidebar)
└─ ::view-transition-image-pair(sidebar)
├─ ::view-transition-old(sidebar)
└─ ::view-transition-new(sidebar)
Jeśli jednak pasek boczny znajduje się tylko na nowej stronie, pseudoelement ::view-transition-old(sidebar)
nie będzie tam występował. Ponieważ nie ma „starego” obrazu paska bocznego, para obrazów będzie zawierać tylko ::view-transition-new(sidebar)
. Podobnie, jeśli pasek boczny znajduje się tylko na starej stronie, para obrazów będzie miała tylko symbol ::view-transition-old(sidebar)
.
W poprzedniej wersji demonstracyjnej pasek boczny przechodzi w inny sposób w zależności od tego, czy pojawia się, znika czy jest obecny w obu stanach. Pojawia się, wsuwając się z prawej strony i stopniowo stając się widoczny, znika, wsuwając się w prawo i stopniowo stając się niewidoczny, a gdy jest obecny w obu stanach, pozostaje na swoim miejscu.
Aby utworzyć konkretne przejścia wejścia i wyjścia, możesz użyć :only-child
pseudoklasy, aby kierować reklamy na stare lub nowe pseudoelementy, gdy są one jedynym elementem podrzędnym w parze obrazów:
/* Entry transition */
::view-transition-new(sidebar):only-child {
animation: 300ms cubic-bezier(0, 0, 0.2, 1) both fade-in,
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}
/* Exit transition */
::view-transition-old(sidebar):only-child {
animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right;
}
W tym przypadku nie ma konkretnego przejścia, gdy pasek boczny jest obecny w obu stanach, ponieważ domyślny stan jest idealny.
Asynchroniczne aktualizacje DOM i oczekiwanie na treści
Wywołanie przekazane do funkcji .startViewTransition()
może zwracać obietnicę, co umożliwia asynchroniczne aktualizacje DOM i oczekiwanie na przygotowanie ważnych treści.
document.startViewTransition(async () => {
await something;
await updateTheDOMSomehow();
await somethingElse;
});
Przejście nie rozpocznie się, dopóki obietnica nie zostanie spełniona. W tym czasie strona jest zamrożona, więc opóźnienia powinny być jak najmniejsze. W szczególności pobieranie danych z sieci powinno odbywać się przed wywołaniem funkcji .startViewTransition()
, gdy strona jest w pełni interaktywna, a nie w ramach wywołania zwrotnego .startViewTransition()
.
Jeśli zdecydujesz się poczekać, aż obrazy lub czcionki będą gotowe, użyj agresywnego limitu czasu:
const wait = ms => new Promise(r => setTimeout(r, ms));
document.startViewTransition(async () => {
updateTheDOMSomehow();
// Pause for up to 100ms for fonts to be ready:
await Promise.race([document.fonts.ready, wait(100)]);
});
W niektórych przypadkach lepiej jednak całkowicie uniknąć opóźnienia i użyć treści, które już masz.
Wykorzystaj w pełni potencjał treści, które już masz
W przypadku, gdy miniatura przechodzi w większy obraz:
Domyślne przejście to przenikanie, co oznacza, że miniatura może przenikać z jeszcze niezaładowanym pełnym obrazem.
Jednym ze sposobów na rozwiązanie tego problemu jest poczekanie na załadowanie całego obrazu przed rozpoczęciem przejścia. Najlepiej zrobić to przed wywołaniem funkcji .startViewTransition()
, aby strona pozostała interaktywna i można było wyświetlić spinner, który poinformuje użytkownika, że trwa wczytywanie. W tym przypadku jest jednak lepsze rozwiązanie:
::view-transition-old(full-embed),
::view-transition-new(full-embed) {
/* Prevent the default animation,
so both views remain opacity:1 throughout the transition */
animation: none;
/* Use normal blending,
so the new view sits on top and obscures the old view */
mix-blend-mode: normal;
}
Miniatura nie znika, tylko znajduje się pod pełnym obrazem. Oznacza to, że jeśli nowy widok nie został wczytany, miniatura jest widoczna przez cały czas przejścia. Oznacza to, że przejście może rozpocząć się od razu, a pełny obraz może wczytać się w odpowiednim czasie.
Nie zadziałałoby to, gdyby nowy widok był przezroczysty, ale w tym przypadku wiemy, że tak nie jest, więc możemy zastosować tę optymalizację.
Obsługa zmian formatu obrazu
Wygodnie jest, że wszystkie dotychczasowe przejścia dotyczyły elementów o tym samym współczynniku proporcji, ale nie zawsze tak będzie. Co się stanie, jeśli miniatura ma proporcje 1:1, a obraz główny 16:9?
W domyślnym przejściu grupa jest animowana od rozmiaru początkowego do końcowego. Stary i nowy widok mają szerokość 100% grupy i automatyczną wysokość, co oznacza, że zachowują format niezależnie od rozmiaru grupy.
To dobra opcja domyślna, ale w tym przypadku nie jest odpowiednia. Przykłady:
::view-transition-old(full-embed),
::view-transition-new(full-embed) {
/* Prevent the default animation,
so both views remain opacity:1 throughout the transition */
animation: none;
/* Use normal blending,
so the new view sits on top and obscures the old view */
mix-blend-mode: normal;
/* Make the height the same as the group,
meaning the view size might not match its aspect-ratio. */
height: 100%;
/* Clip any overflow of the view */
overflow: clip;
}
/* The old view is the thumbnail */
::view-transition-old(full-embed) {
/* Maintain the aspect ratio of the view,
by shrinking it to fit within the bounds of the element */
object-fit: contain;
}
/* The new view is the full image */
::view-transition-new(full-embed) {
/* Maintain the aspect ratio of the view,
by growing it to cover the bounds of the element */
object-fit: cover;
}
Oznacza to, że miniatura pozostaje na środku elementu, gdy szerokość się zwiększa, ale pełny obraz „odcina” się, gdy przechodzi z formatu 1:1 na 16:9.
Więcej informacji znajdziesz w artykule Przejścia widoku: obsługa zmian współczynnika proporcji.
Używanie zapytań o multimedia do zmiany przejść w różnych stanach urządzenia
Możesz używać różnych przejść na urządzeniach mobilnych i komputerach, np. w tym przykładzie, w którym na urządzeniach mobilnych występuje pełne przesunięcie z boku, a na komputerach – subtelniejsze przesunięcie:
Można to osiągnąć za pomocą zwykłych zapytań o media:
/* Transitions for mobile */
::view-transition-old(root) {
animation: 300ms ease-out both full-slide-to-left;
}
::view-transition-new(root) {
animation: 300ms ease-out both full-slide-from-right;
}
@media (min-width: 500px) {
/* Overrides for larger displays.
This is the shared axis transition from earlier in the article. */
::view-transition-old(root) {
animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}
::view-transition-new(root) {
animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}
}
Możesz też zmienić elementy, którym przypisujesz view-transition-name
, w zależności od pasujących zapytań o media.
Reagowanie na ustawienie „Mniej animacji”
Użytkownicy mogą wskazać, że wolą ograniczone animacje, w systemie operacyjnym, a to ustawienie jest widoczne w CSS.
Możesz uniemożliwić tym użytkownikom przejścia:
@media (prefers-reduced-motion) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
Preferowanie „ograniczonego ruchu” nie oznacza jednak, że użytkownik chce całkowitego braku ruchu. Zamiast powyższego fragmentu możesz wybrać bardziej subtelną animację, która nadal wyraża relację między elementami i przepływ danych.
Obsługa wielu stylów przejść widoku za pomocą typów przejść widoku
Czasami przejście z jednego widoku do drugiego powinno być specjalnie dostosowane. Na przykład podczas przechodzenia do następnej lub poprzedniej strony w sekwencji paginacji możesz chcieć przesuwać treści w innym kierunku w zależności od tego, czy przechodzisz do strony o wyższym czy niższym numerze w sekwencji.
W tym celu możesz użyć typów przejść widoku, które pozwalają przypisać co najmniej 1 typ do aktywnego przejścia widoku. Na przykład podczas przechodzenia do wyższej strony w sekwencji podziału na strony użyj typu forwards
, a podczas przechodzenia do niższej strony użyj typu backwards
. Te typy są aktywne tylko podczas rejestrowania lub wykonywania przejścia, a każdy z nich można dostosować za pomocą CSS, aby używać różnych animacji.
Aby użyć typów w przejściu widoku w tym samym dokumencie, przekaż wartość types
do metody startViewTransition
. Aby to umożliwić, funkcja document.startViewTransition
akceptuje też obiekt: update
to funkcja wywołania zwrotnego, która aktualizuje DOM, a types
to tablica z typami.
const direction = determineBackwardsOrForwards();
const t = document.startViewTransition({
update: updateTheDOMSomehow,
types: ['slide', direction],
});
Aby odpowiadać na te typy, użyj selektora :active-view-transition-type()
. Przekaż do selektora type
, na które chcesz kierować reklamy. Dzięki temu możesz oddzielić od siebie style wielu przejść widoku, tak aby deklaracje jednego nie kolidowały z deklaracjami drugiego.
Typy są stosowane tylko podczas przechwytywania lub wykonywania przejścia, więc możesz użyć selektora, aby ustawić lub usunąć view-transition-name
w elemencie tylko w przypadku przejścia widoku z tym typem.
/* Determine what gets captured when the type is forwards or backwards */
html:active-view-transition-type(forwards, backwards) {
:root {
view-transition-name: none;
}
article {
view-transition-name: content;
}
.pagination {
view-transition-name: pagination;
}
}
/* Animation styles for forwards type only */
html:active-view-transition-type(forwards) {
&::view-transition-old(content) {
animation-name: slide-out-to-left;
}
&::view-transition-new(content) {
animation-name: slide-in-from-right;
}
}
/* Animation styles for backwards type only */
html:active-view-transition-type(backwards) {
&::view-transition-old(content) {
animation-name: slide-out-to-right;
}
&::view-transition-new(content) {
animation-name: slide-in-from-left;
}
}
/* Animation styles for reload type only (using the default root snapshot) */
html:active-view-transition-type(reload) {
&::view-transition-old(root) {
animation-name: fade-out, scale-down;
}
&::view-transition-new(root) {
animation-delay: 0.25s;
animation-name: fade-in, scale-up;
}
}
W poniższym demo paginacji zawartość strony przesuwa się do przodu lub do tyłu w zależności od numeru strony, do której przechodzisz. Typy są określane na podstawie kliknięcia, po którym są przekazywane do document.startViewTransition
.
Aby kierować reklamy na dowolne aktywne przejście widoku, niezależnie od jego typu, możesz zamiast tego użyć selektora pseudoklasy :active-view-transition
.
html:active-view-transition {
…
}
Obsługa wielu stylów przejścia widoku za pomocą nazwy klasy w elemencie głównym przejścia widoku
Czasami przejście z jednego typu widoku do innego powinno być specjalnie dostosowane. Nawigacja „wstecz” powinna różnić się od nawigacji „do przodu”.
Przed wprowadzeniem typów przejść w takich przypadkach tymczasowo ustawiano nazwę klasy w elemencie głównym przejścia. Podczas wywoływania document.startViewTransition
tym elementem głównym przejścia jest element <html>
, do którego można uzyskać dostęp za pomocą document.documentElement
w JavaScript:
if (isBackNavigation) {
document.documentElement.classList.add('back-transition');
}
const transition = document.startViewTransition(() =>
updateTheDOMSomehow(data)
);
try {
await transition.finished;
} finally {
document.documentElement.classList.remove('back-transition');
}
Aby usunąć klasy po zakończeniu przejścia, w tym przykładzie użyto transition.finished
, czyli obietnicy, która jest spełniana, gdy przejście osiągnie stan końcowy. Pozostałe właściwości tego obiektu znajdziesz w dokumentacji API.
Teraz możesz użyć tej nazwy klasy w CSS, aby zmienić przejście:
/* 'Forward' transitions */
::view-transition-old(root) {
animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}
::view-transition-new(root) {
animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in, 300ms
cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}
/* Overrides for 'back' transitions */
.back-transition::view-transition-old(root) {
animation-name: fade-out, slide-to-right;
}
.back-transition::view-transition-new(root) {
animation-name: fade-in, slide-from-left;
}
Podobnie jak w przypadku zapytań o media, obecność tych klas może być też używana do zmiany elementów, które otrzymują view-transition-name
.
Uruchamianie przejść bez zawieszania innych animacji
Obejrzyj tę prezentację zmiany pozycji filmu:
Czy zauważyłeś w nim coś nieprawidłowego? Jeśli nie, nie martw się. Oto ten sam fragment w zwolnionym tempie:
Podczas przejścia film wydaje się zamrożony, a następnie pojawia się odtwarzana wersja filmu. Wynika to z tego, że ::view-transition-old(video)
to zrzut ekranu starego widoku, a ::view-transition-new(video)
to aktualny obraz nowego widoku.
Możesz to naprawić, ale najpierw zastanów się, czy warto to robić. Jeśli nie widzisz „problemu” podczas odtwarzania przejścia z normalną szybkością, nie musisz go zmieniać.
Jeśli naprawdę chcesz to naprawić, nie wyświetlaj znaku ::view-transition-old(video)
, tylko od razu przejdź do znaku ::view-transition-new(video)
. Możesz to zrobić, zastępując domyślne style i animacje:
::view-transition-old(video) {
/* Don't show the frozen old view */
display: none;
}
::view-transition-new(video) {
/* Don't fade the new view in */
animation: none;
}
To wszystko.
Teraz film będzie odtwarzany przez cały czas przejścia.
Integracja z interfejsem Navigation API (i innymi platformami)
Przejścia widoku są określone w taki sposób, aby można je było zintegrować z innymi platformami lub bibliotekami. Jeśli na przykład Twoja aplikacja jednostronicowa (SPA) korzysta z routera, możesz dostosować mechanizm aktualizacji routera, aby aktualizować treść za pomocą przejścia widoku.
W tym fragmencie kodu z tej demonstracji paginacji procedura obsługi przechwytywania interfejsu Navigation API jest dostosowana do wywoływania funkcji document.startViewTransition
, gdy obsługiwane są przejścia widoku.
navigation.addEventListener("navigate", (e) => {
// Don't intercept if not needed
if (shouldNotIntercept(e)) return;
// Intercept the navigation
e.intercept({
handler: async () => {
// Fetch the new content
const newContent = await fetchNewContent(e.destination.url, {
signal: e.signal,
});
// The UA does not support View Transitions, or the UA
// already provided a Visual Transition by itself (e.g. swipe back).
// In either case, update the DOM directly
if (!document.startViewTransition || e.hasUAVisualTransition) {
setContent(newContent);
return;
}
// Update the content using a View Transition
const t = document.startViewTransition(() => {
setContent(newContent);
});
}
});
});
Niektóre przeglądarki zapewniają własne przejście, gdy użytkownik wykonuje gest przesuwania, aby się poruszać. W takim przypadku nie należy uruchamiać własnego przejścia widoku, ponieważ może to pogorszyć wrażenia użytkownika lub wprowadzić go w błąd. Użytkownik zobaczy 2 przejścia – jedno zapewnione przez przeglądarkę, a drugie przez Ciebie – następujące po sobie.
Dlatego zalecamy, aby nie rozpoczynać przejścia widoku, gdy przeglądarka zapewnia własne przejście wizualne. Aby to zrobić, sprawdź wartość właściwości hasUAVisualTransition
instancji NavigateEvent
. Właściwość ma wartość true
, gdy przeglądarka zapewnia przejście wizualne. Ta właściwość hasUIVisualTransition
występuje też w instancjach PopStateEvent
.
W poprzednim fragmencie kodu sprawdzanie, które określa, czy uruchomić przejście widoku, uwzględnia tę właściwość. Jeśli przejścia widoku w tym samym dokumencie nie są obsługiwane lub przeglądarka już zapewnia własne przejście, przejście widoku jest pomijane.
if (!document.startViewTransition || e.hasUAVisualTransition) {
setContent(newContent);
return;
}
Na poniższym nagraniu użytkownik przesuwa palcem, aby wrócić na poprzednią stronę. Zrzut ekranu po lewej stronie nie zawiera sprawdzenia flagi hasUAVisualTransition
. Nagranie po prawej stronie zawiera to sprawdzenie, dzięki czemu pomija ręczne przejście widoku, ponieważ przeglądarka zapewniła przejście wizualne.
hasUAVisualTransition
Animowanie za pomocą JavaScriptu
Do tej pory wszystkie przejścia były definiowane za pomocą CSS, ale czasami to nie wystarcza:
Kilku części tego przejścia nie można osiągnąć za pomocą samego CSS:
- Animacja rozpoczyna się w miejscu kliknięcia.
- Animacja kończy się, gdy okrąg ma promień do najdalszego rogu. Mamy jednak nadzieję, że w przyszłości będzie to możliwe w przypadku usługi porównywania cen.
Na szczęście możesz tworzyć przejścia za pomocą interfejsu Web Animation API.
let lastClick;
addEventListener('click', event => (lastClick = event));
function spaNavigate(data) {
// Fallback for browsers that don't support this API:
if (!document.startViewTransition) {
updateTheDOMSomehow(data);
return;
}
// Get the click position, or fallback to the middle of the screen
const x = lastClick?.clientX ?? innerWidth / 2;
const y = lastClick?.clientY ?? innerHeight / 2;
// Get the distance to the furthest corner
const endRadius = Math.hypot(
Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y)
);
// With a transition:
const transition = document.startViewTransition(() => {
updateTheDOMSomehow(data);
});
// Wait for the pseudo-elements to be created:
transition.ready.then(() => {
// Animate the root's new view
document.documentElement.animate(
{
clipPath: [
`circle(0 at ${x}px ${y}px)`,
`circle(${endRadius}px at ${x}px ${y}px)`,
],
},
{
duration: 500,
easing: 'ease-in',
// Specify which pseudo-element to animate
pseudoElement: '::view-transition-new(root)',
}
);
});
}
W tym przykładzie użyto transition.ready
, czyli obietnicy, która jest spełniana po utworzeniu pseudoelementów przejścia. Pozostałe właściwości tego obiektu znajdziesz w dokumentacji API.
Przejścia jako ulepszenie
Interfejs View Transition API został zaprojektowany tak, aby „opakowywać” zmiany w DOM i tworzyć dla nich przejścia. Przejście powinno być jednak traktowane jako ulepszenie, co oznacza, że aplikacja nie powinna przechodzić w stan „błędu”, jeśli zmiana DOM się powiedzie, ale przejście nie. Przejście nie powinno się nie udać, ale jeśli tak się stanie, nie powinno to pogorszyć wrażeń użytkownika.
Aby traktować przejścia jako ulepszenie, nie używaj obietnic przejścia w sposób, który spowoduje, że aplikacja zgłosi błąd w przypadku niepowodzenia przejścia.
async function switchView(data) { // Fallback for browsers that don't support this API: if (!document.startViewTransition) { await updateTheDOM(data); return; } const transition = document.startViewTransition(async () => { await updateTheDOM(data); }); await transition.ready; document.documentElement.animate( { clipPath: [`inset(50%)`, `inset(0)`], }, { duration: 500, easing: 'ease-in', pseudoElement: '::view-transition-new(root)', } ); }
Problem z tym przykładem polega na tym, że switchView()
odrzuci żądanie, jeśli przejście nie może osiągnąć stanu ready
, ale nie oznacza to, że widok nie został przełączony. DOM mógł zostać zaktualizowany, ale wystąpiły zduplikowane elementy view-transition-name
, więc przejście zostało pominięte.
Zamiast tego:
async function switchView(data) { // Fallback for browsers that don't support this API: if (!document.startViewTransition) { await updateTheDOM(data); return; } const transition = document.startViewTransition(async () => { await updateTheDOM(data); }); animateFromMiddle(transition); await transition.updateCallbackDone; } async function animateFromMiddle(transition) { try { await transition.ready; document.documentElement.animate( { clipPath: [`inset(50%)`, `inset(0)`], }, { duration: 500, easing: 'ease-in', pseudoElement: '::view-transition-new(root)', } ); } catch (err) { // You might want to log this error, but it shouldn't break the app } }
W tym przykładzie użyto transition.updateCallbackDone
, aby poczekać na aktualizację DOM i odrzucić ją, jeśli się nie powiedzie. switchView
nie odrzuca już żądania, jeśli przejście się nie powiedzie. Rozwiązuje problem po zakończeniu aktualizacji DOM i odrzuca żądanie, jeśli się nie powiedzie.
Jeśli chcesz, aby funkcja switchView
została wywołana, gdy nowy widok się „ustabilizuje”, czyli gdy animowane przejście zostanie zakończone lub pominięte, zastąp transition.updateCallbackDone
wartością transition.finished
.
Nie jest to polyfill, ale…
Nie jest to łatwa funkcja do wypełnienia. Ta funkcja pomocnicza znacznie ułatwia jednak pracę w przeglądarkach, które nie obsługują przejść widoku:
function transitionHelper({
skipTransition = false,
types = [],
update,
}) {
const unsupported = (error) => {
const updateCallbackDone = Promise.resolve(update()).then(() => {});
return {
ready: Promise.reject(Error(error)),
updateCallbackDone,
finished: updateCallbackDone,
skipTransition: () => {},
types,
};
}
if (skipTransition || !document.startViewTransition) {
return unsupported('View Transitions are not supported in this browser');
}
try {
const transition = document.startViewTransition({
update,
types,
});
return transition;
} catch (e) {
return unsupported('View Transitions with types are not supported in this browser');
}
}
Można go używać w ten sposób:
function spaNavigate(data) {
const types = isBackNavigation ? ['back-transition'] : [];
const transition = transitionHelper({
update() {
updateTheDOMSomehow(data);
},
types,
});
// …
}
W przeglądarkach, które nie obsługują przejść widoku, funkcja updateDOM
będzie nadal wywoływana, ale nie będzie animowanego przejścia.
Możesz też podać pewne classNames
, aby dodać je do <html>
podczas przejścia, co ułatwi zmianę przejścia w zależności od typu nawigacji.
Możesz też przekazać true
do skipTransition
, jeśli nie chcesz animacji, nawet w przeglądarkach obsługujących przejścia widoku. Jest to przydatne, jeśli w witrynie użytkownik może wyłączyć przejścia.
Praca z platformami
Jeśli korzystasz z biblioteki lub platformy, która ukrywa zmiany DOM, trudność polega na tym, aby wiedzieć, kiedy zmiana DOM została zakończona. Oto zestaw przykładów z użyciem pomocniczego kodu w różnych platformach.
- React – kluczową kwestią jest tutaj
flushSync
, które stosuje zestaw zmian stanu synchronicznie. Tak, jest duże ostrzeżenie dotyczące używania tego interfejsu API, ale Dan Abramov zapewnia, że w tym przypadku jest to odpowiednie rozwiązanie. Jak zwykle w przypadku Reacta i kodu asynchronicznego, podczas korzystania z różnych obietnic zwracanych przezstartViewTransition
upewnij się, że kod działa w odpowiednim stanie. - Vue.js – kluczowym elementem jest tutaj
nextTick
, który jest spełniany po zaktualizowaniu DOM. - Svelte – bardzo podobny do Vue, ale metoda oczekiwania na następną zmianę to
tick
. - Lit – kluczowe jest tu
this.updateComplete
w komponentach, które jest spełniane po zaktualizowaniu DOM. - Angular – kluczowe jest tu
applicationRef.tick
, które usuwa oczekujące zmiany DOM. Od wersji 17 Angulara możesz używaćwithViewTransitions
, który jest dołączony do@angular/router
.
Dokumentacja API
const viewTransition = document.startViewTransition(update)
Rozpocznij nowy
ViewTransition
.update
to funkcja, która jest wywoływana po przechwyceniu bieżącego stanu dokumentu.Gdy obietnica zwrócona przez
updateCallback
zostanie spełniona, przejście rozpocznie się w następnej klatce. Jeśli obietnica zwrócona przezupdateCallback
zostanie odrzucona, przejście zostanie przerwane.const viewTransition = document.startViewTransition({ update, types })
Rozpocznij nowy
ViewTransition
z określonymi typamiupdate
jest wywoływana po zarejestrowaniu bieżącego stanu dokumentu.types
ustawia aktywne typy przejścia podczas przechwytywania lub wykonywania przejścia. Początkowo jest ona pusta. Więcej informacji znajdziesz w sekcjiviewTransition.types
poniżej.
Instancje ViewTransition
:
viewTransition.updateCallbackDone
Obietnica, która zostanie spełniona, gdy spełni się obietnica zwrócona przez funkcję
updateCallback
, lub odrzucona, gdy zostanie odrzucona.Interfejs View Transition API otacza zmianę DOM i tworzy przejście. Czasami jednak nie zależy Ci na tym, czy animacja przejścia zakończy się powodzeniem, tylko chcesz wiedzieć, czy i kiedy nastąpi zmiana w DOM.
updateCallbackDone
jest przeznaczony do tego przypadku użycia.viewTransition.ready
Obietnica, która zostanie spełniona po utworzeniu pseudoelementów przejścia i gdy animacja będzie się miała rozpocząć.
Odrzuca, jeśli przejście nie może się rozpocząć. Może to być spowodowane błędną konfiguracją, np. duplikowaniem
view-transition-name
, lub jeśliupdateCallback
zwraca odrzuconą obietnicę.Jest to przydatne w przypadku animowania pseudoelementów przejścia za pomocą JavaScriptu.
viewTransition.finished
Obietnica, która jest spełniana, gdy stan końcowy jest w pełni widoczny i interaktywny dla użytkownika.
Odrzuca tylko wtedy, gdy
updateCallback
zwraca odrzuconą obietnicę, ponieważ oznacza to, że stan końcowy nie został utworzony.W przeciwnym razie, jeśli przejście nie rozpocznie się lub zostanie pominięte, stan końcowy zostanie osiągnięty, więc warunek
finished
zostanie spełniony.viewTransition.types
Obiekt
Set
podobny do obiektu, który zawiera typy aktywnej zmiany widoku. Aby manipulować wpisami, użyj metod instancjiclear()
,add()
idelete()
.Aby odpowiedzieć na określony typ w CSS, użyj selektora pseudoklasy
:active-view-transition-type(type)
w elemencie głównym przejścia.Typy są automatycznie usuwane po zakończeniu przejścia widoku.
viewTransition.skipTransition()
Pomiń animację przejścia.
Nie spowoduje to pominięcia wywołania funkcji
updateCallback
, ponieważ zmiana DOM jest oddzielona od przejścia.
Informacje o domyślnym stylu i przejściu
::view-transition
- Pseudoelement główny, który wypełnia obszar widoku i zawiera każdy element
::view-transition-group
. ::view-transition-group
Pozycjonowanie bezwzględne.
Przejścia
width
iheight
między stanem „przed” a „po”.Przejścia
transform
między czworobokiem w przestrzeni widocznego obszaru „przed” i „po”.::view-transition-image-pair
Element jest pozycjonowany bezwzględnie, aby wypełnić grupę.
Ma
isolation: isolate
, aby ograniczyć wpływmix-blend-mode
na stare i nowe widoki.::view-transition-new
i::view-transition-old
Pozycjonowanie bezwzględne w lewym górnym rogu elementu opakowującego.
Wypełnia 100% szerokości grupy, ale ma automatyczną wysokość, więc zachowuje współczynnik proporcji zamiast wypełniać grupę.
Ma
mix-blend-mode: plus-lighter
, aby umożliwić prawdziwe przenikanie.Stary widok przechodzi z
opacity: 1
naopacity: 0
. Nowy widok przechodzi zopacity: 0
naopacity: 1
.
Prześlij opinię
Opinie deweloperów są zawsze mile widziane. Aby to zrobić, zgłoś problem grupie roboczej CSS w GitHub, podając sugestie i pytania. Dodaj do problemu prefiks [css-view-transitions]
.
Jeśli napotkasz błąd, zgłoś go w Chromium.