Опубликовано: 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 View Transition предназначен для «обертывания» изменения 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 .
,Опубликовано: 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;
}
Благодаря этому одному изменению затухание теперь стало очень медленным:
Хорошо, это все еще не впечатляет. Вместо этого, следующий код реализует общую ось дизайна материала :
@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, чтобы применить то же правило стиля.
#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;
}
В этом случае не существует конкретного перехода, когда боковая панель присутствует в обоих состояниях, поскольку по умолчанию идеально.
Обновления Async DOM и ожидание контента
Обратный вызов, переданный .startViewTransition()
может вернуть обещание, которое позволяет обновлять асинхронные обновления и ожидание важного контента будет готово.
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;
}
Это означает, что миниатюра остается в центре элемента, когда ширина расширяется, но полное изображение «Un-Corps», когда он переходит от 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
который вы хотите нацелиться в селектор. Это позволяет вам держать стили переходов с несколькими взглядами, отделенными друг от друга, без объявлений о том, что один мешает объявлениям другого.
Поскольку типы применяются только при захвате или выполнении перехода, вы можете использовать селектор для установки-или unset-a 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 View Transition 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 и Async Code, при использовании различных обещаний, возвращаемыхstartViewTransition
, позаботьтесь о том, чтобы ваш код работает с правильным состоянием. - Vue.js - ключом здесь является
nextTick
, который выполняется после обновления DOM. - СВЕЛТ - очень похоже на VUE, но метод ожидания следующего изменения -
tick
. - Lit - Ключ здесь - это обещание
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 View Transition завершает изменение DOM и создает переход. Тем не менее, иногда вы не заботитесь об успехе или неудаче анимации перехода, вы просто хотите знать, произойдет ли и когда изменение DOM.
updateCallbackDone
предназначен для этого варианта использования.-
viewTransition.ready
Обещание, которое выполняется после создания псевдо-элементов для перехода, и анимация собирается начать.
Он отвергает, если переход не может начаться. Это может быть связано с неправильной конфигурацией, такой как дублирование
view-transition-name
S, или еслиupdateCallback
возвращает отклоненное обещание.Это полезно для анимирования псевдо-элементов перехода с помощью JavaScript .
-
viewTransition.finished
Обещание, которое выполняет, как только конечное состояние будет полностью заметно и интерактивно для пользователя.
Это отвергает только в том случае, если
updateCallback
возвращает отклоненное обещание, так как это указывает на то, что конечное состояние не создано.В противном случае, если переход не начинается или пропускается во время перехода, конечное состояние все еще достигается, поэтому
finished
выполнение.-
viewTransition.types
Set
-подобный объект, который содержит типы перехода Active View. Чтобы манипулировать записями, используйте его методы экземпляраclear()
,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]
.
Если вы столкнетесь с ошибкой, вместо этого подайте ошибку хрома .