Навигация
Почти любое приложение — это набор экранов. Переходы между ними могут быть как плоскими, так и вложенными, с возможностью возврата на предыдущий экран. Помимо этого, есть платформенные различия анимаций и стилей элементов навигации.
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
в роли бокового меню.