Опубликовано: 17 августа 2021 г., Последнее обновление: 25 сентября 2024 г.
Когда переход представления выполняется для одного документа, он называется переходом представления того же документа . Обычно это происходит в одностраничных приложениях (SPA), где для обновления DOM используется JavaScript. Переходы между представлениями одного и того же документа поддерживаются в Chrome, начиная с Chrome 111.
Чтобы инициировать переход представления того же документа, вызовите 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());
}
При вызове браузер автоматически делает снимки всех элементов, для которых объявлено CSS-свойство view-transition-name
.
Затем он выполняет переданный обратный вызов, который обновляет DOM, после чего он делает снимки нового состояния.
Эти снимки затем упорядочиваются в виде дерева псевдоэлементов и анимируются с использованием возможностей CSS-анимации. Пары снимков из старого и нового состояния плавно переходят из старого положения и размера в новое, а их содержимое плавно исчезает. Если хотите, вы можете использовать CSS для настройки анимации.
Переход по умолчанию: Cross-fade.
Переход представления по умолчанию представляет собой плавное затухание, поэтому он служит хорошим введением в 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));
}
Где updateTheDOMSomehow
то образом переводит DOM в новое состояние. Это можно сделать как угодно. Например, вы можете добавлять или удалять элементы, изменять имена классов или стили.
И вот так страницы плавно исчезают:
Ладно, перекрестное затухание не так уж и впечатляет. К счастью, переходы можно настроить, но сначала вам нужно понять, как работает это базовое плавное затухание.
Как работают эти переходы
Давайте обновим предыдущий пример кода.
document.startViewTransition(() => updateTheDOMSomehow(data));
Когда вызывается .startViewTransition()
, API фиксирует текущее состояние страницы. Это включает в себя создание моментального снимка.
После завершения вызывается обратный вызов, переданный в .startViewTransition()
. Вот где меняется DOM. Затем API фиксирует новое состояние страницы.
Как только новое состояние зафиксировано, API создает дерево псевдоэлементов следующим образом:
::view-transition
└─ ::view-transition-group(root)
└─ ::view-transition-image-pair(root)
├─ ::view-transition-old(root)
└─ ::view-transition-new(root)
::view-transition
находится в наложении поверх всего остального на странице. Это полезно, если вы хотите установить цвет фона для перехода.
::view-transition-old(root)
— это снимок экрана старого представления, а ::view-transition-new(root)
— живое представление нового представления. Оба отображаются как «замененный контент» CSS (например, <img>
).
Старое представление анимируется от opacity: 1
до opacity: 0
, а новое представление анимируется от opacity: 0
до opacity: 1
, создавая плавное затухание.
Вся анимация выполняется с использованием анимации CSS, поэтому ее можно настроить с помощью CSS.
Настройте переход
На все псевдоэлементы перехода представления можно настроить CSS, а поскольку анимация определяется с помощью CSS, вы можете изменить их, используя существующие свойства анимации CSS. Например:
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 5s;
}
Благодаря этому одному изменению затухание теперь стало очень медленным:
Ладно, это все еще не впечатляет. Вместо этого следующий код реализует переход общей оси 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;
}
И вот результат:
Переход нескольких элементов
В предыдущей демонстрации вся страница участвовала в переходе по общей оси. Это работает для большей части страницы, но не совсем подходит для заголовка, поскольку он выдвигается, чтобы снова вставиться обратно.
Чтобы избежать этого, вы можете извлечь заголовок из остальной части страницы, чтобы его можно было анимировать отдельно. Это делается путем присвоения элементу view-transition-name
.
.main-header {
view-transition-name: main-header;
}
Значение view-transition-name
может быть любым (за исключением none
, что означает отсутствие имени перехода). Он используется для уникальной идентификации элемента в переходе.
И результат этого:
Теперь заголовок остается на месте и плавно исчезает.
Это объявление CSS привело к изменению дерева псевдоэлементов:
::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)
Теперь есть две переходные группы. Один для заголовка, другой для остального. На них можно нацеливаться независимо с помощью CSS и с разными переходами. Хотя в этом случае в main-header
остался переход по умолчанию, который представляет собой перекрестное затухание.
Ну да ладно, переход по умолчанию — это не просто плавное затухание, переходы ::view-transition-group
также выполняются:
- Позиционирование и преобразование (с помощью
transform
) - Ширина
- Высота
До сих пор это не имело значения, поскольку заголовок имеет одинаковый размер и положение с обеих сторон изменения DOM. Но вы также можете извлечь текст в заголовке:
.main-header-text {
view-transition-name: main-header-text;
width: fit-content;
}
fit-content
используется таким образом, чтобы размер элемента соответствовал размеру текста, а не растягивался до оставшейся ширины. Без этого стрелка назад уменьшает размер текстового элемента заголовка, а не делает его одинаковым на обеих страницах.
Итак, теперь у нас есть три части, с которыми можно поиграть:
::view-transition
├─ ::view-transition-group(root)
│ └─ …
├─ ::view-transition-group(main-header)
│ └─ …
└─ ::view-transition-group(main-header-text)
└─ …
Но опять же, просто используя настройки по умолчанию:
Теперь текст заголовка немного сдвигается, освобождая место для кнопки «Назад».
Анимируйте несколько псевдоэлементов одинаково с помощью view-transition-class
Поддержка браузера
Предположим, у вас есть переход просмотра с кучей карточек и заголовком на странице. Чтобы анимировать все карточки, кроме заголовка, вам нужно написать селектор, нацеленный на каждую отдельную карточку.
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);
}
Получилось 20 элементов? Вам нужно написать 20 селекторов. Добавляем новый элемент? Затем вам также необходимо расширить селектор, который применяет стили анимации. Не совсем масштабируемо.
view-transition-class
можно использовать в псевдоэлементах view-transition для применения того же правила стиля.
#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);
}
В следующем примере карточек используется предыдущий фрагмент CSS. Ко всем карточкам, включая недавно добавленные, применяется одинаковое время с помощью одного селектора: html::view-transition-group(.card)
.
Отладка переходов
Поскольку переходы между представлениями создаются на основе анимации CSS, панель «Анимации» в Chrome DevTools отлично подходит для отладки переходов.
Используя панель «Анимация» , вы можете приостановить следующую анимацию, а затем прокручивать ее вперед и назад. При этом псевдоэлементы перехода можно найти на панели «Элементы» .
Переходные элементы не обязательно должны быть одним и тем же элементом DOM.
До сих пор мы использовали view-transition-name
для создания отдельных элементов перехода для заголовка и текста в заголовке. Концептуально это один и тот же элемент до и после изменения DOM, но вы можете создавать переходы там, где это не так.
Например, основному встроенному видео можно присвоить view-transition-name
:
.full-embed {
view-transition-name: full-embed;
}
Затем при щелчке по миниатюре ей можно присвоить то же view-transition-name
, только на время перехода:
thumbnail.onclick = async () => {
thumbnail.style.viewTransitionName = 'full-embed';
document.startViewTransition(() => {
thumbnail.style.viewTransitionName = '';
updateTheDOMSomehow();
});
};
И результат:
Миниатюра теперь переходит в основное изображение. Несмотря на то, что это концептуально (и буквально) разные элементы, API перехода рассматривает их как одно и то же, поскольку они имеют одно и то же view-transition-name
.
Реальный код этого перехода немного сложнее, чем предыдущий пример, поскольку он также обрабатывает переход обратно на страницу миниатюр. Полную реализацию смотрите в исходном коде .
Пользовательские входные и выходные переходы
Посмотрите на этот пример:
Боковая панель является частью перехода:
.sidebar {
view-transition-name: sidebar;
}
Но, в отличие от заголовка в предыдущем примере, боковая панель отображается не на всех страницах. Если в обоих состояниях есть боковая панель, псевдоэлементы перехода выглядят так:
::view-transition
├─ …other transition groups…
└─ ::view-transition-group(sidebar)
└─ ::view-transition-image-pair(sidebar)
├─ ::view-transition-old(sidebar)
└─ ::view-transition-new(sidebar)
Однако если боковая панель находится только на новой странице, псевдоэлемента ::view-transition-old(sidebar)
там не будет. Поскольку для боковой панели нет «старого» изображения, пара изображений будет иметь только ::view-transition-new(sidebar)
. Аналогично, если боковая панель находится только на старой странице, пара изображений будет иметь только ::view-transition-old(sidebar)
.
В предыдущей демонстрации боковая панель менялась по-разному в зависимости от того, входит ли она, выходит или присутствует в обоих состояниях. Он входит, скользя вправо и постепенно появляясь, выходит, скользя вправо и исчезая, и остается на месте, когда присутствует в обоих состояниях.
Чтобы создать определенные переходы входа и выхода, вы можете использовать псевдокласс :only-child
для нацеливания на старые или новые псевдоэлементы, когда он является единственным дочерним элементом в паре изображений:
/* 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;
}
В этом случае не существует специального перехода, когда боковая панель присутствует в обоих состояниях, поскольку значение по умолчанию идеально.
Асинхронные обновления DOM и ожидание контента
Обратный вызов, передаваемый в .startViewTransition()
может возвращать обещание, которое позволяет выполнять асинхронные обновления DOM и ожидать готовности важного контента.
document.startViewTransition(async () => {
await something;
await updateTheDOMSomehow();
await somethingElse;
});
Переход не начнется, пока обещание не будет выполнено. В это время страница зависает, поэтому задержки здесь должны быть сведены к минимуму. В частности, выборку из сети следует выполнять до вызова .startViewTransition()
, пока страница все еще полностью интерактивна, а не выполнять ее как часть обратного вызова .startViewTransition()
.
Если вы решите дождаться готовности изображений или шрифтов, обязательно используйте агрессивный тайм-аут:
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)]);
});
Однако в некоторых случаях лучше вообще избежать задержки и использовать уже имеющийся контент.
Используйте по максимуму контент, который у вас уже есть
В случае, когда миниатюра переходит в увеличенное изображение:
По умолчанию используется плавное затухание, что означает, что миниатюра может плавно переходить из еще не загруженного полного изображения.
Один из способов справиться с этим — дождаться загрузки полного изображения, прежде чем начинать переход. В идеале это должно быть сделано до вызова .startViewTransition()
, чтобы страница оставалась интерактивной, и можно было показать счетчик, указывающий пользователю, что что-то загружается. Но в этом случае есть лучший способ:
::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;
}
Теперь миниатюра не исчезает, а просто находится под полным изображением. Это означает, что если новое представление не загрузилось, миниатюра будет видна на протяжении всего перехода. Это означает, что переход может начаться сразу, и полное изображение может загрузиться в свое время.
Это не сработало бы, если бы новое представление имело прозрачность, но в данном случае мы знаем, что это не так, поэтому мы можем провести такую оптимизацию.
Обработка изменений соотношения сторон
Удобно, что до сих пор все переходы осуществлялись к элементам с одинаковым соотношением сторон, но так будет не всегда. Что делать, если миниатюра имеет формат 1:1, а основное изображение — 16:9?
При переходе по умолчанию группа анимируется от размера «до» к размеру «после». Старое и новое представления имеют 100% ширину группы и автоматическую высоту, что означает, что они сохраняют соотношение сторон независимо от размера группы.
Это хорошее значение по умолчанию, но в данном случае это не то, что нужно. Так:
::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;
}
Это означает, что миниатюра остается в центре элемента при увеличении ширины, но полное изображение «обрезается» при переходе от 1:1 к 16:9.
Для получения более подробной информации ознакомьтесь с переходами просмотра: обработка изменений соотношения сторон.
Используйте медиа-запросы для изменения переходов для разных состояний устройства.
Возможно, вы захотите использовать разные переходы на мобильных устройствах и на настольных компьютерах, например, в этом примере выполняется полный слайд сбоку на мобильном телефоне, но более тонкий слайд на настольном компьютере:
Этого можно добиться с помощью обычных медиа-запросов:
/* 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;
}
}
Вы также можете изменить, каким элементам вы назначаете view-transition-name
в зависимости от соответствующих медиа-запросов.
Реагируйте на предпочтение «ограниченного движения»
Пользователи могут указать, что они предпочитают ограниченное движение в своей операционной системе, и это предпочтение отображается в CSS .
Вы можете запретить любые переходы для этих пользователей:
@media (prefers-reduced-motion) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
Однако предпочтение «ограниченного движения» не означает, что пользователь не хочет никакого движения . Вместо предыдущего фрагмента вы можете выбрать более тонкую анимацию, но такую, которая по-прежнему выражает связь между элементами и потоком данных.
Обработка нескольких стилей перехода видов с помощью типов перехода видов
Поддержка браузера
Иногда переход от одного конкретного представления к другому должен иметь специально адаптированный переход. Например, при переходе к следующей или предыдущей странице в последовательности страниц вы можете захотеть переместить содержимое в другом направлении в зависимости от того, переходите ли вы на более высокую или нижнюю страницу последовательности.
Для этого вы можете использовать типы перехода видов, которые позволяют назначить один или несколько типов активному переходу вида. Например, при переходе на более высокую страницу в последовательности страниц используйте тип forwards
, а при переходе на страницу ниже — тип backwards
. Эти типы активны только при захвате или выполнении перехода, и каждый тип можно настроить с помощью CSS для использования различных анимаций.
Чтобы использовать типы при переходе между представлениями одного и того же документа, вы передаете types
в метод startViewTransition
. Чтобы это сделать, document.startViewTransition
также принимает объект: update
— это функция обратного вызова, обновляющая DOM, а types
— это массив с типами.
const direction = determineBackwardsOrForwards();
const t = document.startViewTransition({
update: updateTheDOMSomehow,
types: ['slide', direction],
});
Чтобы ответить на эти типы, используйте селектор :active-view-transition-type()
. Передайте type
на который вы хотите ориентироваться, в селектор. Это позволяет вам хранить стили нескольких переходов представлений отдельно друг от друга, при этом объявления одного не мешают объявлениям другого.
Поскольку типы применяются только при захвате или выполнении перехода, вы можете использовать селектор, чтобы установить или отключить view-transition-name
для элемента только для перехода представления с этим типом.
/* 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;
}
}
В следующей демонстрации нумерации страниц содержимое страницы перемещается вперед или назад в зависимости от номера страницы, на которую вы переходите. Типы определяются при нажатии, при котором они передаются в document.startViewTransition
.
Чтобы настроить таргетинг на любой переход активного вида, независимо от его типа, вместо этого вы можете использовать селектор псевдокласса :active-view-transition
.
html:active-view-transition {
…
}
Обработка нескольких стилей перехода представления с использованием имени класса в корне перехода представления.
Иногда переход от одного конкретного типа представления к другому должен иметь специально адаптированный переход. Или навигация «назад» должна отличаться от навигации «вперед».
До появления типов переходов в таких случаях можно было временно установить имя класса в корне перехода. При вызове document.startViewTransition
этим корнем перехода является элемент <html>
, доступный с помощью document.documentElement
в 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');
}
Чтобы удалить классы после завершения перехода, в этом примере transition.finished
— обещание, которое разрешается, как только переход достигнет своего конечного состояния. Остальные свойства этого объекта описаны в справочнике по API .
Теперь вы можете использовать это имя класса в своем CSS, чтобы изменить переход:
/* '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;
}
Как и в случае с медиа-запросами, наличие этих классов также можно использовать для изменения того, какие элементы получают view-transition-name
.
Запускайте переходы, не замораживая другие анимации.
Взгляните на эту демонстрацию положения перехода видео:
Вы видели в этом что-то неправильное? Не волнуйтесь, если вы этого не сделали. Здесь оно замедлено:
Во время перехода видео зависает, затем появляется воспроизводимая версия видео. Это связано с тем, что ::view-transition-old(video)
является скриншотом старого вида, тогда как ::view-transition-new(video)
— живое изображение нового представления.
Вы можете это исправить, но сначала спросите себя, стоит ли это исправлять. Если бы вы не заметили «проблему», когда переход воспроизводился на нормальной скорости, я бы не стал его менять.
Если вы действительно хотите это исправить, не показывайте ::view-transition-old(video)
; переключитесь прямо на ::view-transition-new(video)
. Вы можете сделать это, переопределив стили и анимацию по умолчанию:
::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;
}
И все!
Теперь видео воспроизводится на протяжении всего перехода.
Интеграция с API навигации (и другими платформами)
Переходы представлений заданы таким образом, чтобы их можно было интегрировать с другими платформами или библиотеками. Например, если ваше одностраничное приложение (SPA) использует маршрутизатор, вы можете настроить механизм обновления маршрутизатора для обновления содержимого с помощью перехода представления.
В следующем фрагменте кода, взятом из этой демонстрации разбиения на страницы, обработчик перехвата API навигации настроен на вызов document.startViewTransition
, когда поддерживаются переходы между представлениями.
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);
});
}
});
});
Некоторые, но не все, браузеры предоставляют собственный переход, когда пользователь выполняет жест смахивания для навигации. В этом случае вам не следует запускать собственный переход представления, поскольку это может привести к ухудшению или путанице взаимодействия с пользователем. Пользователь увидит два перехода — один предоставленный браузером, а другой — вами, которые выполняются последовательно.
Поэтому рекомендуется предотвратить запуск перехода представления, когда браузер предоставил собственный визуальный переход. Для этого проверьте значение свойства hasUAVisualTransition
экземпляра NavigateEvent
. Свойству присвоено значение true
, когда браузер предоставил визуальный переход. Это свойство hasUIVisualTransition
также существует в экземплярах PopStateEvent
.
В предыдущем фрагменте проверка, определяющая, следует ли запускать переход представления, учитывает это свойство. Если нет поддержки переходов представлений одного и того же документа или когда браузер уже предоставил собственный переход, переход представления пропускается.
if (!document.startViewTransition || e.hasUAVisualTransition) {
setContent(newContent);
return;
}
В следующей записи пользователь проводит пальцем, чтобы вернуться на предыдущую страницу. Захват слева не включает проверку флага hasUAVisualTransition
. Запись справа включает проверку, тем самым пропуская переход вручную, поскольку браузер предоставил визуальный переход.
Анимация с помощью JavaScript
До сих пор все переходы были определены с использованием CSS, но иногда CSS недостаточно:
Некоторые части этого перехода невозможно реализовать с помощью одного лишь CSS:
- Анимация начинается с места щелчка.
- Анимация заканчивается кругом, имеющим радиус до самого дальнего угла. Хотя, надеюсь, в будущем это станет возможным с помощью CSS.
К счастью, вы можете создавать переходы с помощью 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)',
}
);
});
}
В этом примере используется transition.ready
— обещание, которое выполняется после успешного создания псевдоэлементов перехода. Остальные свойства этого объекта описаны в справочнике по API .
Переходы как улучшение
API перехода просмотра предназначен для «обертывания» изменения DOM и создания для него перехода. Однако переход следует рассматривать как улучшение, поскольку ваше приложение не должно переходить в состояние «ошибки», если изменение DOM прошло успешно, но переход завершился неудачно. В идеале переход не должен завершиться неудачей, но если это произойдет, он не должен нарушить остальную часть пользовательского опыта.
Чтобы рассматривать переходы как усовершенствование, старайтесь не использовать обещания перехода таким образом, чтобы ваше приложение выдало ошибку в случае сбоя перехода.
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)', } ); }
Проблема с этим примером заключается в том, что switchView()
отклонит переход, если переход не может достичь состояния ready
, но это не означает, что представление не удалось переключиться. Возможно, DOM был успешно обновлен, но в нем были повторяющиеся имена view-transition-name
, поэтому переход был пропущен.
Вместо:
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 } }
В этом примере transition.updateCallbackDone
используется для ожидания обновления DOM и отклонения в случае сбоя. switchView
больше не отклоняет, если переход не удался, он разрешается после завершения обновления DOM и отклоняется в случае сбоя.
Если вы хотите, чтобы switchView
разрешался, когда новое представление «установилось», например, любой анимированный переход завершился или пропущен до конца, замените transition.updateCallbackDone
на transition.finished
.
Не полифилл, но…
Это непростая функция для полифилла. Однако эта вспомогательная функция значительно упрощает работу в браузерах, которые не поддерживают переходы между представлениями:
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');
}
}
И его можно использовать следующим образом:
function spaNavigate(data) {
const types = isBackNavigation ? ['back-transition'] : [];
const transition = transitionHelper({
update() {
updateTheDOMSomehow(data);
},
types,
});
// …
}
В браузерах, которые не поддерживают переходы представлений, updateDOM
все равно будет вызываться, но анимированного перехода не будет.
Вы также можете указать некоторые classNames
для добавления в <html>
во время перехода, что упрощает изменение перехода в зависимости от типа навигации .
Вы также можете передать true
в skipTransition
, если вам не нужна анимация, даже в браузерах, которые поддерживают переходы между представлениями. Это полезно, если на вашем сайте пользователь предпочитает отключать переходы.
Работа с фреймворками
Если вы работаете с библиотекой или платформой, которая абстрагирует изменения DOM, сложнее всего узнать, когда изменение DOM будет завершено. Вот набор примеров использования приведенного выше помощника в различных средах.
- React — ключевой момент здесь
flushSync
, который синхронно применяет набор изменений состояния. Да, есть серьезное предупреждение об использовании этого API, но Дэн Абрамов уверяет меня, что в данном случае это уместно. Как обычно с React и асинхронным кодом, при использовании различных промисов, возвращаемыхstartViewTransition
, позаботьтесь о том, чтобы ваш код работал с правильным состоянием. - Vue.js — ключевым моментом здесь является
nextTick
, который выполняется после обновления DOM. - Svelte — очень похож на Vue, но метод ожидания следующего изменения —
tick
. - Горит — ключевым моментом здесь является обещание
this.updateComplete
внутри компонентов, которое выполняется после обновления DOM. - Angular — ключевой момент здесь —
applicationRef.tick
, который сбрасывает ожидающие изменения DOM. Начиная с версии Angular 17, вы можете использоватьwithViewTransitions
, который поставляется с@angular/router
.
Справочник по API
-
const viewTransition = document.startViewTransition(update)
Запустите новый
ViewTransition
.update
— это функция, которая вызывается после фиксации текущего состояния документа.Затем, когда обещание, возвращенное
updateCallback
выполняется, переход начинается в следующем кадре. Если обещание, возвращенноеupdateCallback
отклоняется, переход прекращается.-
const viewTransition = document.startViewTransition({ update, types })
Запустите новый
ViewTransition
с указанными типами.update
вызывается после фиксации текущего состояния документа.types
задают активные типы перехода при захвате или выполнении перехода. Изначально он пуст. Дополнительную информацию см. вviewTransition.types
ниже.
Члены экземпляра ViewTransition
:
-
viewTransition.updateCallbackDone
Обещание, которое выполняется, когда обещание, возвращенное
updateCallback
выполняется, или отклоняется, когда оно отклоняется.API перехода просмотра оборачивает изменение DOM и создает переход. Однако иногда вас не волнует успех или неудача анимации перехода, вы просто хотите знать, произойдет ли изменение DOM и когда это произойдет.
updateCallbackDone
предназначен для этого варианта использования.-
viewTransition.ready
Обещание, которое выполняется, когда псевдоэлементы для перехода созданы и анимация вот-вот начнется.
Он отклоняет, если переход не может начаться. Это может произойти из-за неправильной конфигурации, например, из-за дублирования
view-transition-name
или из-за того, чтоupdateCallback
возвращает отклоненное обещание.Это полезно для анимации псевдоэлементов перехода с помощью JavaScript .
-
viewTransition.finished
Обещание, которое выполняется, как только конечное состояние становится полностью видимым и интерактивным для пользователя.
Он отклоняется только в том случае, если
updateCallback
возвращает отклоненное обещание, поскольку это указывает на то, что конечное состояние не было создано.В противном случае, если переход не начинается или пропускается во время перехода, конечное состояние все равно достигается, поэтому
finished
выполняется.-
viewTransition.types
Объект , подобный
Set
, который содержит типы перехода активного представления. Чтобы манипулировать записями, используйте его методы экземпляраclearclear()
,add()
иdelete()
.Чтобы отреагировать на определенный тип в CSS, используйте селектор псевдокласса
:active-view-transition-type(type)
в корне перехода.Типы автоматически очищаются после завершения перехода представления.
-
viewTransition.skipTransition()
Пропустите анимационную часть перехода.
При этом вызов
updateCallback
не будет пропущен, поскольку изменение DOM не связано с переходом.
Стиль по умолчанию и ссылка на переход
-
::view-transition
- Корневой псевдоэлемент, который заполняет область просмотра и содержит каждую
::view-transition-group
. -
::view-transition-group
Абсолютно позиционирован.
width
иheight
перехода между состояниями «до» и «после».Переходы
transform
четырехугольник пространства окна просмотра «до» и «после».-
::view-transition-image-pair
Абсолютно готов пополнить группу.
Имеет
isolation: isolate
, чтобы ограничить влияниеmix-blend-mode
на старые и новые представления.-
::view-transition-new
и::view-transition-old
Абсолютно расположен в верхнем левом углу оболочки.
Заполняет 100 % ширины группы, но имеет автоматическую высоту, поэтому сохраняет соотношение сторон, а не заполняет группу.
Имеет
mix-blend-mode: plus-lighter
, чтобы обеспечить истинное плавное затухание.Старый вид переходит от
opacity: 1
кopacity: 0
. Новое представление переходит отopacity: 0
кopacity: 1
.
Обратная связь
Отзывы разработчиков всегда ценны. Для этого отправьте сообщение о проблеме в рабочую группу CSS на GitHub с предложениями и вопросами. Префикс проблемы с помощью [css-view-transitions]
.
Если вы столкнетесь с ошибкой, вместо этого сообщите об ошибке в Chromium .