Data publikacji: 17 sierpnia 2021 r., ostatnia aktualizacja: 25 września 2024 r.
Gdy przejście widoku jest uruchamiane w ramach jednego dokumentu, nazywa się przejściem widoku tego samego dokumentu. Zwykle tak się dzieje w przypadku aplikacji jednostronicowych (SPA), w których do aktualizowania DOM służy JavaScript. Przejścia między widokiem tego samego dokumentu są obsługiwane w Chrome od wersji Chrome 111.
Aby wywołać przejście do widoku tego samego dokumentu, 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 rejestruje migawki wszystkich elementów, w których deklarowano właściwość CSS view-transition-name
.
Następnie wykonuje przekazaną funkcję z argumentem zwrotnym, która aktualizuje DOM, a potem wykonuje zrzuty ekranu nowego stanu.
Następnie te migawki są układane w drzewo pseudoelementów i animowane za pomocą animacji CSS. Pary migawek ze starego i nowego stanu przechodzą płynnie ze starej pozycji i rozmiaru do nowej lokalizacji, a ich zawartość jest płynnie przekształcana. Jeśli chcesz, możesz dostosować animacje za pomocą kodu CSS.
Domyślne przejście: przejście płynne
Domyślnym przejściem jest przejście krzyżowe, które stanowi 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 czy zmieniać style.
I tak oto strony przechodzą w się nawzajem:
Przenikanie nie jest aż tak imponujące. Na szczęście przejścia można dostosowywać, ale najpierw musisz zrozumieć, jak działa to podstawowe przejście krzyżowe.
Jak działają te przejścia
Zaktualizujmy poprzedni przykład kodu.
document.startViewTransition(() => updateTheDOMSomehow(data));
Po wywołaniu .startViewTransition()
interfejs API przechwytuje bieżący stan strony. Dotyczy to też robienia zrzutów ekranu.
Po zakończeniu wywołanie zwrotne przekazane do .startViewTransition()
jest wywoływane. Właśnie wtedy 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 przeźroczystej nakładce na wszystkie inne elementy na stronie. Jest to przydatne, jeśli chcesz ustawić kolor tła dla przejścia.
::view-transition-old(root)
to zrzut ekranu starego widoku, a ::view-transition-new(root)
to opublikowana reprezentacja nowego widoku. Oba renderują się jako „zastąpiona treść” w CSS (jak element <img>
).
Stary widok animuje się z poziomu opacity: 1
do opacity: 0
, a nowy – z poziomu opacity: 0
do opacity: 1
, tworząc efekt przejścia.
Cała animacja jest wykonywana za pomocą animacji CSS, więc można je dostosować za pomocą CSS.
Dostosowywanie przejścia
Wszystkie pseudoelementy przejść widoku można kierować za pomocą CSS, a ponieważ animacje są definiowane za pomocą CSS, możesz 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;
}
Dzięki tej zmianie przejście jest teraz bardzo powolne:
To nadal nie robi wrażenia. Zamiast tego kod implementuje przejście z użyciem wspólnej osi w ramach 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:
Przechodzenie między wieloma elementami
W poprzedniej prezentacji ruch obejmuje całą stronę. Sprawdza się to w przypadku większości stron, ale nie wygląda to dobrze w przypadku nagłówka, ponieważ wysuwa się, aby z powrotem wsunąć się z powrotem.
Aby tego uniknąć, możesz wyodrębnić nagłówek z pozostałej części strony, aby można było animować go osobno. Aby to zrobić, 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 podczas przejścia.
Efekt:
Teraz nagłówek pozostaje na miejscu i przechodzi w kolejny.
Ta deklaracja CSS spowodowała zmianę pseudoelementu w drzewie:
::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)
Obecnie są 2 grupy przejścia. Jeden dla nagłówka, a drugi dla reszty. Można je kierować niezależnie za pomocą usługi porównywania cen i przypisać do nich różne przejścia. Choć w tym przypadku main-header
pozostał z domyślnym przejściem, czyli przenikaniem.
Domyślne przejście to nie tylko przejście krzyżowe, ale też:::view-transition-group
- Położenie i transformacja (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 w nagłówku:
.main-header-text {
view-transition-name: main-header-text;
width: fit-content;
}
fit-content
jest używany, dzięki czemu element ma rozmiar tekstu, zamiast rozciągać się do pozostałej szerokości. Bez tego strzałka wstecz zmniejsza rozmiar elementu tekstu nagłówka, a nie zachowuje ten sam rozmiar na obu stronach.
Mamy więc 3 elementy:
::view-transition
├─ ::view-transition-group(root)
│ └─ …
├─ ::view-transition-group(main-header)
│ └─ …
└─ ::view-transition-group(main-header-text)
└─ …
Ale znowu, użyj domyślnych ustawień:
Teraz tekst nagłówka przesuwa się, aby zrobić miejsce dla przycisku Wstecz.
Dzięki view-transition-class
możesz animować wiele pseudoelementów w taki sam sposób
Obsługa przeglądarek
Załóżmy, że masz przejście w widoku z kilkoma kartami i tytułem na stronie. Aby animować wszystkie karty oprócz tytułu, musisz napisać selektor, który będzie kierować na każdą kartę.
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? To 20 selektorów, które musisz napisać. Dodawanie nowego elementu Musisz też rozszerzyć selektor, który stosuje style animacji. Niezbyt skalowalny.
Wartości view-transition-class
można używać w pseudoelementach 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);
}
Poniższy przykład kart wykorzystuje poprzedni fragment kodu CSS. Wszystkie karty, w tym nowo dodane, mają takie samo ustawienie czasu, które jest stosowane za pomocą jednego selektora: html::view-transition-group(.card)
.
Debugowanie przejść
Ponieważ przejścia widoku są tworzone na podstawie animacji CSS, panel Animacje w Narzędziach deweloperskich w Chrome doskonale nadaje się do debugowania przejść.
W panelu Animacje możesz wstrzymać kolejną animację, a potem przewinąć ją do przodu i do tyłu. W tym czasie pseudoelementy przejścia będą widoczne w panelu Elementy.
Elementy w przechodzeniu nie muszą być tym samym elementem DOM
Do tej pory użyliśmy view-transition-name
do utworzenia oddzielnych elementów przejścia dla nagłówka i tekstu w nagłówku. Zasadniczo są to te same elementy przed zmianą DOM i po niej, ale można tworzyć przejścia, gdy jest inaczej.
Na przykład główny element wideo może mieć view-transition-name
:
.full-embed {
view-transition-name: full-embed;
}
Gdy klikniesz miniaturę, możesz przypisać do niej ten sam 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 przechodzi teraz w obraz główny. Mimo że są to elementy o różnym charakterze (i dosłownie), interfejs API przejścia traktuje je jako ten sam element, ponieważ mają ten sam identyfikator view-transition-name
.
Prawdziwy kod tego przejścia jest nieco bardziej skomplikowany niż w poprzednim przykładzie, ponieważ obsługuje też przejście z powrotem na stronę miniatur. Pełne informacje o wdrożeniu znajdziesz w źródle kodu.
Niestandardowe przejścia
Rozważ ten przykład:
Pasek boczny jest częścią procesu przejścia:
.sidebar {
view-transition-name: sidebar;
}
W przeciwieństwie do nagłówka w poprzednim przykładzie pasek boczny nie jest jednak widoczny na wszystkich stronach. Jeśli w obu stanach jest 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, pseudoelementu ::view-transition-old(sidebar)
tam nie będzie. Ponieważ nie ma „starego” obrazu na potrzeby 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 zawierać tylko ::view-transition-old(sidebar)
.
W poprzednim pokazie pasek boczny zmieniał się w różny sposób w zależności od tego, czy był widoczny, czy nie, lub czy był widoczny i zmieniał się. Wchodzi, przesuwa się od prawej strony i zanika. Znika, przesuwając w prawo i zanika. Pozostaje na swoim miejscu, jeśli występuje w obu stanach.
Aby utworzyć określone przejścia wejścia i wyjścia, możesz użyć pseudoklasy :only-child
, aby ukierunkować stare lub nowe pseudoelementy, gdy jest to jedyny element podrzędny 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, w którym pasek boczny jest dostępny w obu stanach, ponieważ ustawienie domyślne jest idealne.
Asynchroniczne aktualizacje DOM i oczekiwanie na treści
Callback przekazany do .startViewTransition()
może zwrócić obietnicę, która umożliwia asynchroniczne aktualizacje DOM i czekanie na przygotowanie ważnych treści.
document.startViewTransition(async () => {
await something;
await updateTheDOMSomehow();
await somethingElse;
});
Przenoszenie nie rozpocznie się, dopóki obietnica nie zostanie zrealizowana. W tym czasie strona jest zablokowana, więc opóźnienia powinny być jak najmniejsze. W szczególności pobieranie danych z sieci należy wykonać przed wywołaniem funkcji .startViewTransition()
, gdy strona jest wciąż w pełni interaktywna, a nie w ramach wywołania zwrotnego .startViewTransition()
.
Jeśli zdecydujesz się poczekać na obrazy lub czcionki, użyj krótkiego 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łkiem tego uniknąć i wykorzystać treści, które już masz.
Wykorzystanie potencjału treści, które już masz
Gdy miniatura przechodzi w większy obraz:
Domyślnym przejściem jest przenikanie, co oznacza, że miniatura może zanikać z jeszcze nie wczytanym pełnym obrazem.
Możesz na przykład poczekać, aż załaduje się cały obraz, zanim rozpoczniesz przejście. Najlepiej zrobić to przed wywołaniem funkcji .startViewTransition()
, aby strona pozostała interaktywna. Można też wyświetlić wskaźnik postępu, aby poinformować użytkownika, że dane się wczytują. W tym przypadku istnieje jednak lepszy sposób:
::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;
}
Teraz miniatura nie znika, tylko znajduje się pod pełnym obrazem. Oznacza to, że jeśli nowy widok nie został jeszcze załadowany, miniatura jest widoczna przez cały czas przełączania. Oznacza to, że przejście może się rozpocząć od razu, a pełny obraz może się wczytać w swoim tempie.
To nie zadziałałoby, gdyby nowy widok zawierał przejrzystość. Tym razem wiemy, że tak nie jest, dlatego możemy wprowadzić tę optymalizację.
Obsługa zmian formatu obrazu
Dla wygody wszystkie dotychczasowe przejścia obejmowały elementy o tym samym współczynniku proporcji, ale nie zawsze tak jest. Co zrobić, jeśli miniatura jest w formacie 1:1, a zdjęcie główne ma proporcje 16:9?
W domyślnym przejściu grupa animuje się od rozmiaru przed do rozmiaru po. Stary i nowy widok mają 100% szerokości grupy i automatyczną wysokość, co oznacza, że współczynnik proporcji jest utrzymywany niezależnie od wielkości grupy.
Jest to dobra domyślna wartość, ale w tym przypadku nie jest ona 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 w środku elementu, gdy jego szerokość się zwiększa, ale pełny obraz nie jest „przycinany” podczas przejścia z formatu 1:1 na 16:9.
Więcej informacji znajdziesz w artykule Przejścia między widokami: obsługa zmian formatu obrazu.
Używaj zapytań o multimedia do zmiany przejść dla różnych stanów urządzenia
Na urządzeniach mobilnych i komputerach możesz używać różnych przejść. W tym przykładzie na urządzeniu mobilnym występuje pełne przesunięcie z boków, a na komputerze – bardziej subtelne przesunięcie:
Można to osiągnąć za pomocą zwykłych zapytań dotyczących multimediów:
/* 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, do których przypisujesz view-transition-name
w zależności od dopasowania zapytań dotyczących multimediów.
Reagowanie na ustawienie „Mniej animacji”
Użytkownicy mogą wskazać, że wolą ograniczoną animację w systemie operacyjnym, a ta preferencja jest wyświetlana w CSS.
Możesz uniemożliwić przejścia tym użytkownikom:
@media (prefers-reduced-motion) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
Ustawienie „Ograniczony ruch” nie oznacza jednak, że użytkownik chce, aby brakowało ruchu. Zamiast poprzedniego fragmentu możesz wybrać bardziej subtelną animację, która nadal odzwierciedla relacje między elementami i przepływ danych.
Obsługa wielu stylów przejść z użyciem typów przejść widoku
Obsługa przeglądarek
Czasami przejście z jednego widoku do innego powinno być dopasowane do innych. Na przykład podczas przechodzenia do następnej lub poprzedniej strony w sekwencji stron możesz przesuwać zawartość w różnym kierunku w zależności od tego, czy przechodzisz do strony wyższej czy niższej w sekwencji.
Możesz do tego użyć typów przejścia między widokami, które umożliwiają przypisanie co najmniej 1 typu do aktywnego przejścia między widokami. Jeśli na przykład przechodzisz do wyższej strony w sekwencji podziału na strony, użyj typu forwards
, a jeśli przechodzisz 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ą kodu CSS, aby używać różnych animacji.
Aby używać typów w przejściu do widoku tego samego dokumentu, przekazujesz parametr types
do metody startViewTransition
. Aby to umożliwić, funkcja document.startViewTransition
przyjmuje też obiekt: update
to funkcja wywołania zwrotnego, która aktualizuje DOM, a types
to tablica typów.
const direction = determineBackwardsOrForwards();
const t = document.startViewTransition({
update: updateTheDOMSomehow,
types: ['slide', direction],
});
Aby odpowiedzieć na te rodzaje zapytań, użyj selektora :active-view-transition-type()
. Przekaż do selektora type
, na który chcesz kierować reklamy. Dzięki temu możesz zachować style wielu przejść między widokami oddzielnie od siebie, bez nakładania się deklaracji jednych na deklaracje innych.
Typy są stosowane tylko podczas rejestrowania lub wykonywania przejścia, więc za pomocą selektora możesz ustawić lub odznaczyć view-transition-name
w elemencie tylko w przypadku przejścia widoku tego typu.
/* 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 tym demo paginacji zawartość strony przesuwa się do przodu lub do tyłu w zależności od tego, do której strony się przenosisz. Typy reklam są określane na podstawie kliknięcia, po którym są przekazywane do parametru document.startViewTransition
.
Aby kierować reklamy na dowolne przejście do aktywnego widoku, niezależnie od jego typu, możesz użyć selektora pseudoklasy :active-view-transition
.
html:active-view-transition {
…
}
Obsługa wielu stylów przejścia między widokami za pomocą nazwy klasy w korzenia przejścia między widokami
Czasami przejście z jednego typu widoku na inny powinno być specjalnie dostosowane. Inną opcją jest zastosowanie innej ikony dla przycisku „Wstecz” niż dla przycisku „Dalej”.
Przed typami przejścia sposobem obsługi takich przypadków było tymczasowe ustawienie nazwy klasy w katalogu głównym przejścia. Podczas wywoływania funkcji document.startViewTransition
ten element <html>
jest elementem źródeł przejścia, do którego można uzyskać dostęp w JavaScript za pomocą funkcji document.documentElement
:
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, przykład używa transition.finished
, czyli obietnicy, która jest wykonywana po zakończeniu przejścia. Inne właściwości tego obiektu opisane są w dokumentacji interfejsu API.
Teraz możesz użyć tej nazwy klasy w kodzie 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 multimedia, obecność tych klas może służyć do zmiany elementów, które mają mieć view-transition-name
.
Przechodzenie między przejściami bez zamrażania innych animacji
Obejrzyj ten film pokazujący przenoszenie pozycji w filmie:
Czy zauważyłeś/zauważyłaś coś nieprawidłowego? Nie martw się, jeśli nie masz. Tutaj jest spowolnione:
Podczas przejścia film wydaje się być zamrożony, a potem powoli zaczyna się odtwarzać. Dzieje się tak, ponieważ ::view-transition-old(video)
to zrzut ekranu starego widoku, a ::view-transition-new(video)
to żywy 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ą prędkością, nie będę go zmieniać.
Jeśli naprawdę chcesz to naprawić, nie pokazuj ::view-transition-old(video)
, tylko od razu przejdź do ::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.
Film jest teraz odtwarzany podczas przejścia.
Integracja z interfejsem API Nawigacji (i innymi frameworkami)
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 aplikacja jednostronicowa (SPA) korzysta z przekaźnika, możesz dostosować mechanizm aktualizacji przekaźnika, aby aktualizować treści za pomocą przejścia widoku.
W poniższym fragmencie kodu pobranym z tej demonstracji o podziale na strony moduł obsługi przechwytywania danych w interfejsie Navigation API został dostosowany tak, aby wywoływać document.startViewTransition
, gdy obsługiwane są przejścia między widokami.
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 (ale nie wszystkie) oferują własne przejścia, gdy użytkownik przesuwa palcem, aby się poruszać. W takim przypadku nie należy uruchamiać własnego przejścia, ponieważ może to spowodować nieprzyjemne lub dezorientujące wrażenia użytkownika. Użytkownik zobaczy 2 przejścia – jedna po drugiej przez przeglądarkę i druga przez Ciebie.
Dlatego zalecamy, aby nie rozpoczynać przejścia między widokami, gdy przeglądarka już je wyświetla. Aby to zrobić, sprawdź wartość właściwości hasUAVisualTransition
instancji NavigateEvent
. Właściwość jest ustawiona na wartość true
, gdy przeglądarka zapewniła wizualne przejście. Właściwość hasUIVisualTransition
występuje też w przypadku instancji PopStateEvent
.
W poprzednim fragmencie kodu sprawdzanie, które decyduje, czy należy wykonać przejście widoku, uwzględnia tę właściwość. Jeśli nie ma obsługi przejść w tym samym dokumencie lub przeglądarka już dostarczyła własne przejście, to przejście jest pomijane.
if (!document.startViewTransition || e.hasUAVisualTransition) {
setContent(newContent);
return;
}
W tym nagraniu użytkownik przesuwa palcem, aby wrócić do poprzedniej strony. Zrzut po lewej stronie nie zawiera sprawdzania flagi hasUAVisualTransition
. Nagrywanie po prawej stronie zawiera sprawdzenie, dzięki czemu pominięto przejście do ręcznego widoku, ponieważ przeglądarka zapewniła wizualny przejście.
Animacja za pomocą JavaScript
Do tej pory wszystkie przejścia były definiowane za pomocą CSS, ale czasami CSS nie wystarcza:
Niektórych elementów tej zmiany nie można wdrożyć za pomocą samego CSS:
- Animacja rozpoczyna się w miejscu kliknięcia.
- Na końcu animacji okrąg ma promień do najdalszego rogu. Mamy jednak nadzieję, że w przyszłości będzie to możliwe w usłudze 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 wykorzystano obietnicę transition.ready
, która wygasa po utworzeniu pseudoelementów przejścia. Inne właściwości tego obiektu opisane są w dokumentacji interfejsu API.
Przejścia jako ulepszenie
Interfejs ViewTransition API służy do „opakowania” zmiany DOM i utworzenia dla niej przejścia. Jednak przejście powinno być traktowane jako ulepszenie, ponieważ aplikacja nie powinna przechodzić w stan „błąd”, jeśli zmiana DOM-u się powiedzie, ale przejście nie powiedzie się. W idealnej sytuacji przeniesienie nie powinno się zakończyć niepowodzeniem, ale jeśli do niego dojdzie, nie powinno to negatywnie wpłynąć na wrażenia użytkownika.
Aby traktować przejścia jako ulepszenie, uważaj, aby nie korzystać z obietnic przejścia w sposób, który mógłby spowodować, że w razie niepowodzenia przejścia aplikacja się zakończy.
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()
zostanie odrzucone, jeśli przejście nie może osiągnąć stanu ready
, ale nie oznacza to, że nie udało się przełączyć widoku. DOM mógł zostać zaktualizowany, ale przejście zostało pominięte, bo elementy view-transition-name
były zduplikowane.
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 funkcji transition.updateCallbackDone
, aby zaczekać na aktualizację DOM i odrzucić ją, jeśli się nie powiedzie. Moduł switchView
nie odrzuca już konwersji w przypadku niepowodzenia, koryguje po zakończeniu aktualizacji DOM i odrzuca w przypadku niepowodzenia.
Jeśli chcesz, by reguła switchView
uwzględniała tylko zmiany w nowym widoku (np. wszystkie animowane przejścia zostały ukończone lub pominięte do końca), zastąp transition.updateCallbackDone
elementem transition.finished
.
Nie jest to polyfill, ale…
Nie jest to łatwa funkcja do zastąpienia. Jednak funkcja pomocnicza znacznie ułatwia sprawę w przeglądarkach, które nie obsługują przejść między widokami:
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ść między widokami, funkcja updateDOM
będzie nadal wywoływana, ale nie będzie animowanego przejścia.
Możesz też podać classNames
, które zostaną dodane do <html>
podczas przejścia, co ułatwi zmianę przejścia w zależności od typu nawigacji.
Jeśli nie chcesz animacji, możesz też przekazać true
do skipTransition
, nawet w przypadku przeglądarek, które obsługują przejścia widoku. Jest to przydatne, jeśli w Twojej witrynie użytkownicy mogą wyłączyć przejścia.
Praca z ramami
Jeśli korzystasz z biblioteki lub frameworka, który abstrahuje od zmian DOM, trudnością jest ustalenie, kiedy zmiana DOM została zakończona. Oto kilka przykładów, w których wykorzystano pomagacza opisanego powyżej w różnych ramach.
- React – klucz to
flushSync
, który stosuje synchronicznie zbiór zmian stanu. Tak, jest tam duże ostrzeżenie dotyczące używania tego interfejsu API, ale Dan Abramov zapewnia, że w tym przypadku jest ono odpowiednie. Jak zwykle w przypadku kodu React i kodu asynchronicznego podczas korzystania z różnych obietnic zwracanych przez funkcjęstartViewTransition
sprawdź, czy kod działa w prawidłowym stanie. - Vue.js – kluczowym elementem jest tutaj
nextTick
, który jest wykonywany po zaktualizowaniu DOM. - Svelte – bardzo podobne do Vue, ale metoda oczekiwania na następną zmianę to
tick
. - Lit – kluczowe znaczenie ma tutaj obietnica
this.updateComplete
w komponentach, która jest realizowana po zaktualizowaniu DOM. - Angular – kluczowym elementem jest tutaj
applicationRef.tick
, który usuwa oczekujące zmiany DOM. Od wersji 17 Angulara możesz korzystać z bibliotekiwithViewTransitions
, która jest dołączona do@angular/router
.
Dokumentacja API
const viewTransition = document.startViewTransition(update)
Utwórz nowe
ViewTransition
.update
to funkcja, która jest wywoływana po zarejestrowaniu bieżącego stanu dokumentu.Następnie, gdy obietnica zwracana przez
updateCallback
zostanie spełniona, przejście rozpocznie się w następnej klatce. Jeśli obietnica zwrócona przezupdateCallback
jest odrzucana, przejście zostaje anulowane.const viewTransition = document.startViewTransition({ update, types })
utworzyć nowe
ViewTransition
z wybranymi typami,Funkcja
update
jest wywoływana po przechwyceniu bieżącego stanu dokumentu.types
określa aktywne typy przejścia podczas ich przechwytywania. Początkowo jest puste. Więcej informacji znajdziesz w sekcjiviewTransition.types
.
Użytkownicy instancji ViewTransition
:
viewTransition.updateCallbackDone
Obietnica, która jest spełniona, gdy obietnicę zwracaną przez
updateCallback
spełni się, 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 się powiedzie, czy nie, a tylko chcesz wiedzieć, czy i kiedy nastąpi zmiana DOM.
updateCallbackDone
jest przeznaczony do tego zastosowania.viewTransition.ready
Obietnica, która zostanie spełniona, gdy zostaną utworzone pseudoelementy przejścia i animacja zacznie się odtwarzać.
Odrzuca, jeśli przejście nie może się rozpocząć. Może to być spowodowane błędną konfiguracją, na przykład duplikatem
view-transition-name
, lub jeśliupdateCallback
zwraca odrzuconą obietnicę.Jest to przydatne w przypadku animowania pseudoelementów przejścia za pomocą JavaScriptu.
viewTransition.finished
Obietnice, które są spełnione, gdy stan końcowy jest w pełni widoczny i interaktywny dla użytkownika.
Odrzuca tylko wtedy, gdy
updateCallback
zwraca odrzuconą obietnicę, co oznacza, że stan końcowy nie został utworzony.Jeśli jednak przejście nie zostanie rozpoczęte lub zostanie pominięte, stan końcowy zostanie osiągnięty, więc
finished
zostanie spełnione.viewTransition.types
Obiekt podobny do obiektu
Set
, który zawiera typy przejść 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 korzeniach przejścia.Typy są automatycznie usuwane po zakończeniu przejścia między widokami.
viewTransition.skipTransition()
Pomiń część przejścia z animacji.
Nie spowoduje to pominięcia wywołania
updateCallback
, ponieważ zmiana DOM jest niezależna od przejścia.
Informacje o domyślnym stylu i przejściu
::view-transition
- Pseudoelement rdzeń, który wypełnia obszar widoku i zawiera poszczególne elementy
::view-transition-group
. ::view-transition-group
Właściwe pozycjonowanie.
Przejścia
width
iheight
między stanami „przed” i „po”.Przejścia
transform
między kwadratem „przed” i „po” w przestrzeni widoku.::view-transition-image-pair
Pozycjonowane, aby wypełnić grupę.
Musisz
isolation: isolate
, aby ograniczyć wpływmix-blend-mode
na stare i nowe wyświetlenia.::view-transition-new
i::view-transition-old
Absolutnie umieszczone w lewym górnym rogu opakowania.
Wypełnia 100% szerokości grupy, ale ma automatyczną wysokość, więc zachowa proporcje, zamiast wypełniać grupę.
Musi zawierać
mix-blend-mode: plus-lighter
, aby umożliwić płynne przejście.Stary widok zmieni się z
opacity: 1
naopacity: 0
. Nowy widok danych zmieni się zopacity: 0
naopacity: 1
.
Prześlij opinię
Zawsze chętnie przyjmujemy opinie deweloperów. Aby to zrobić, prześlij zgłoszenie do grupy roboczej CSS na GitHubie, podając sugestie i pytania. Dodaj do problemu prefiks [css-view-transitions]
.
Jeśli napotkasz błąd, zgłoś go w Chromium.