Навигация
Почти любое приложение — это набор экранов. Переходы между ними могут быть как плоскими, так и вложенными, с возможностью возврата на предыдущий экран. Помимо этого, есть платформенные различия анимаций и стилей элементов навигации.
VKUI предоставляет компоненты для организации набора экранов, абстрагируя платформенные различия. На этой страннице рассмотрим как с помощью них:
- построить иерархию экранов внутри одного сценария;
- разделить приложение на независимые сценарии (например, по разделам или фичам).
В завершение соберём адаптивный пример с использованием всех навигационных компонентов.
Предисловие
Термины
Экран — отдельное состояние интерфейса, отображающееся в один момент времени.
Сценарий — последовательность экранов, объединённых одной задачей пользователя. Например, экран “Настройки”, откуда можно перейти на экраны “Уведомления”, “Конфиденциональность и безопасность” и другие связанные экраны.
Раздел — крупная часть приложения со своими сценариями. Например, например “Профиль” или “Сообщения”.
Предварительная настройка
Для обеспечения безопасных боковых отступов и корректную работу анимаций навигационных компонентов, рекомендуем обернуть ваше приложение в следующие компоненты:
SplitLayoutс передачей в свойствоheaderзаглушки в видеPanelHeader, чтобы компенсировать боковые отступы для видимыхPanelHeader(при использовании платформы vkcom заглушку можно не создавать).SplitColс передачей свойствstretchedOnMobileиautoSpaced. В зависимости от ширины экрана:stretchedOnMobileрастягивает колонку на всю ширину на мобильных устройствах;autoSpacedвключает автоматические боковые отступы на широких экранах, в частности, это нужно при применении компонентаGroup.
Так как эта обёртка должна быть одна на всё приложение, её достаточно разместить ближе к корню — в точке входа приложения.
import { SplitLayout, SplitCol } from '@vkontakte/vkui';
export default function App() {
return (
<SplitLayout header={<PanelHeader delimiter="none" />}>
<SplitCol stretchedOnMobile autoSpaced>
{/* ... */}
</SplitCol>
</Panel>
);
};Экран
Описывается с помощью компонента Panel. Также, чтобы пользователь знал на каком экране он находится,
рекомендуется добавлять заголовок с помощью компонента PanelHeader.
Panel
└─ PanelHeader
└─ <content>Ниже пример с описанием начального экрана приложения в отдельном файле.
import { type PanelProps, Panel, PanelHeader } from '@vkontakte/vkui';
/**
* Как пример, наследуем весь `PanelProps`, но достаточно будет
* передавать только идентификатор (`Pick<PanelProps, 'id'>`),
* а остальные свойства определять тут при необходимости.
*/
export const Home = (props: PanelProps) => {
return (
<Panel {...props}>
<PanelHeader>Главная</PanelHeader>
Привет, Мир!
</Panel>
);
};Сценарий
За переключение экранов отвечает компонент View. Принимает необходимое количество Panel
с уникальным id. Далее id с нужным экраном передаётся в свойство activePanel.
View
└─ Panel N
└─ PanelHeader
└─ <content>Представим простой пример в отдельном файле.
import { useState } from 'react';
import { View, Panel, PanelHeader, Button } from '@vkontakte/vkui';
export const Router = () => {
const [activePanel, setActivePanel] = useState('panel-1');
return (
<View activePanel={activePanel}>
<Panel id="panel-1">
<PanelHeader>Панель 1</PanelHeader>
<Button onClick={() => setActivePanel('panel-2')}>Перейти к панели 2</Button>
</Panel>
<Panel id="panel-2">
<PanelHeader>Панель 2</PanelHeader>
<Button onClick={() => setActivePanel('panel-1')}>Перейти к панели 1</Button>
</Panel>
</View>
);
};Раздел
Для создания разделов есть два компонента: Root и Epic. Использование того или другого
зависит от требований к приложению и к дизайну.
Root
Root – универсальный вариант для организации разделов. Принимает необходимое количество View
с уникальным id. Далее id с нужным сценарием передаётся в свойство activeView.
Root
└─ View N
└─ Panel N
└─ PanelHeader
└─ <content>import { useState } from 'react';
import { View, Panel, PanelHeader, PanelHeaderBack, Button } from '@vkontakte/vkui';
export const FirstScenario = ({ onBack }) => {
const [activePanel, setActivePanel] = useState('first-panel-1');
return (
<View activePanel={activePanel}>
<Panel id="first-panel-1">
<PanelHeader before={<PanelHeaderBack onClick={onBack} />}>Панель 1</PanelHeader>
<Button onClick={() => setActivePanel('first-panel-2')}>Перейти к панели 2</Button>
</Panel>
<Panel id="first-panel-2">
<PanelHeader before={<PanelHeaderBack onClick={onBack} />}>Панель 2</PanelHeader>
<Button onClick={() => setActivePanel('first-panel-1')}>Перейти к панели 1</Button>
</Panel>
</View>
);
};
export const SecondScenario = ({ onBack }) => {
const [activePanel, setActivePanel] = useState('second-panel-1');
return (
<View activePanel={activePanel}>
<Panel id="second-panel-1">
<PanelHeader before={<PanelHeaderBack onClick={onBack} />}>Панель 1</PanelHeader>
<Button onClick={() => setActivePanel('second-panel-2')}>Перейти к панели 2</Button>
</Panel>
<Panel id="second-panel-2">
<PanelHeader before={<PanelHeaderBack onClick={onBack} />}>Панель 2</PanelHeader>
<Button onClick={() => setActivePanel('second-panel-1')}>Перейти к панели 1</Button>
</Panel>
</View>
);
};import { useState } from 'react';
import { Root } from '@vkontakte/vkui';
import { FirstScenario, SecondScenario } from './scenarios';
export const Router = () => {
const [activeView, setActiveView] = useState('main');
const onBack = () => setActiveView('main');
return (
<Root activeView={activeView}>
<View id="main" activePanel="main-panel">
<Panel id="main-panel">
<PanelHeader>Главный экран</PanelHeader>
<Button onClick={() => setActiveView('first')}>Перейти к сценарию 1</Button>
<Button onClick={() => setActiveView('second')}>Перейти к сценарию 2</Button>
</Panel>
</View>
<FirstScenario id="first" onBack={onBack} />
<SecondScenario id="second" onBack={onBack} />
</Root>
);
};Epic
Epic используется для мобильных приложений, которые по дизайну требуют наличия классической нижней панели
с основными разделами. Применение можно увидеть на m.vk.com ↗ или в нативном приложении VK.
Отличия от Root следующие:
- принимает
Tabbarчерез одноимённое свойствоtabbar; - может содержать в качестве потомков не только
View, но иRootдля создания более глубокой иерархии сценариев; - переключает разделы без анимаций.
Принимает необходимое количество View и/или Root с уникальным id. Далее id с нужным
сценарием передаётся в свойство activeStory.
Epic
└─ View N
└─ Panel N
└─ PanelHeader
└─ <content>
└─ Root N
└─ View N
└─ Panel N
└─ PanelHeader
└─ <content>import { useState } from 'react';
import { View, Panel, PanelHeader, Button } from '@vkontakte/vkui';
export const FirstScenario = () => {
const [activePanel, setActivePanel] = useState('first-panel-1');
return (
<View activePanel={activePanel}>
<Panel id="first-panel-1">
<PanelHeader>Панель 1</PanelHeader>
<Button onClick={() => setActivePanel('first-panel-2')}>Перейти к панели 2</Button>
</Panel>
<Panel id="first-panel-2">
<PanelHeader>Панель 2</PanelHeader>
<Button onClick={() => setActivePanel('first-panel-1')}>Перейти к панели 1</Button>
</Panel>
</View>
);
};
export const SecondScenario = () => {
const [activePanel, setActivePanel] = useState('second-panel-1');
return (
<View activePanel={activePanel}>
<Panel id="second-panel-1">
<PanelHeader>Панель 1</PanelHeader>
<Button onClick={() => setActivePanel('second-panel-2')}>Перейти к панели 2</Button>
</Panel>
<Panel id="second-panel-2">
<PanelHeader>Панель 2</PanelHeader>
<Button onClick={() => setActivePanel('second-panel-1')}>Перейти к панели 1</Button>
</Panel>
</View>
);
};import { useState } from 'react';
import { Epic } from '@vkontakte/vkui';
import { FirstScenario, SecondScenario } from './scenarios';
export const Router = () => {
const [activeStory, setActiveStory] = useState('main');
return (
<Epic
activeStory={activeStory}
tabbar={
<Tabbar>
<TabbarItem
label="Главный экран"
selected={activeStory === id}
onClick={() => setActiveStory('main')}
>
🏠
</TabbarItem>
<TabbarItem
label="Перейти к сценарию 1"
selected={activeStory === id}
onClick={() => setActiveStory('first')}
>
1️⃣
</TabbarItem>
<TabbarItem
label="Перейти к сценарию 2"
selected={activeStory === id}
onClick={() => setActiveStory('second')}
>
2️⃣
</TabbarItem>
</Tabbar>
}
>
<View id="main" activePanel="main-panel">
<Panel id="main-panel">
<PanelHeader>Главный экран</PanelHeader>
</Panel>
</View>
<FirstScenario id="first" />
<SecondScenario id="second" />
</Epic>
);
};Интерактивный пример
Создадим приложение вот с такой структурой:
app (Epic)
└─ profile (View)
└─ profile-panel (Panel)
└─ feed (View)
└─ feed-panel (Panel)
└─ messenger (View)
└─ messenger-panel (Panel)
└─ more (Root)
└─ main-scenario (View)
└─ main-menu-panel (Panel)
└─ notification-scenario (View)
└─ notification-main-panel (Panel)
└─ notification-messenger-panel (Panel)
└─ about-scenario (View)
└─ about-version-panel (Panel)Для навигации по основным разделам на мобильных экранах используется (Tabbar), а на настольных —
(SplitCol) c Cell в роли бокового меню.