UIScreen и Router
UIScreen — корень каждого UI-дерева — он всегда заполняет устройство и ведёт себя как UIColumn.
Экран никогда не прокручивается; прокрутка живёт в ребёнке (см. ниже).
Одноэкранное приложение вызывает screen.open(); многостраничное отдаёт свои экраны Router и
навигирует через push / pop / replace.
Обзор
const detail = UIScreen(
UIText("Detail").style({ fontSize: 24, fontWeight: 700, color: "white" }),
).style({ bgColor: "black", p: 20, pt: "max(safe-top, 24px)" })
.onBackPressed(() => Router.pop())
const home = UIScreen(
UIText("Home").style({ fontSize: 24, fontWeight: 700, color: "white" }),
UIButton(UIText("Open detail").style({ color: "white" }))
.style({ bgColor: "#FF4032", borderRadius: 12, p: 16 })
.onClick(() => Router.push(detail)),
).style({ bgColor: "black", p: 20, pt: "max(safe-top, 24px)", gap: 16 })
Router.init(home)UIScreen
UIScreen(...children: (UINodeChild | UINodeChild[])[]): UIScreen
screen.open(): void // показать этот экран напрямую (одноэкранные приложения; прячет активный Router)
screen.close(): void // закрыть напрямую открытый экранПолная поверхность контейнера: .append / .insert / .remove / .setContent, .style,
.animateTo / .animateFrom, .onLayout.
экран всегда заполняет устройство — стили размера на нём (width, height, flexGrow,
flexShrink, position) — no-op. Стилизуй его отступы, фон и раскладку детей. Фон по умолчанию
чёрный; всегда задавай bgColor и цвета текста явно.
Жизненный цикл — onOpen / onClose
screen.onOpen(cb: () => void): this // экран стал активным
screen.onClose(cb: () => void): this // экран перестал быть активнымОни срабатывают на каждую активацию, не только на первую: уход с экрана вызывает его onClose,
возврат к нему — снова onOpen. Всё запущенное в onOpen должно останавливаться в onClose —
циклы, интервалы, сокеты — иначе продолжит работать за другими экранами. См.
Жизненный цикл.
let loopId: number | null = null
screen
.onOpen(() => { loopId = setLoop(dt => tick(dt)) })
.onClose(() => { if (loopId !== null) { clearLoop(loopId); loopId = null } })Касания и кнопка «назад»
screen.onTouchStart(ev => …) // срабатывает на касания где угодно на экране (полноэкранные жесты)
screen.onBackPressed(cb: () => void) // аппаратный/жестовый «назад» Android — обычно Router.pop()onTouchStart поддерживает ev.track() как кнопка — см.
События указателя.
Нажатие «назад» разрешается по порядку: onBackPressed самого верхнего открытого виджета →
onBackPressed текущего экрана/сцены → возврат из вложенной вкладки пейджера → pop роутера →
глобальный запасной app.onBackPressed. Если не обработал никто, нажатие игнорируется —
приложение не выходит, как и жест «назад» в iOS в корне стека.
Глобальный запасной обработчик — место для политики на всё приложение: он срабатывает в самом конце цепочки, поэтому навигация во всех остальных местах работает нетронутой. Классическое «нажми дважды, чтобы выйти»:
let armedAt = 0
app.onBackPressed(() => {
if (Date.now() - armedAt < 2000) { app.quit(); return }
armedAt = Date.now()
toast("Press back again to exit")
})app.quit() возвращает в лаунчер внутри вьювера LeCodes; собранное standalone-приложение уходит
в платформу (Android сворачивает его, iOS игнорирует вызов).
Экраны никогда не прокручиваются
Экран — фиксированный корень раскладки: у него нет позиции прокрутки, нет скролл-событий, нет
pull-to-refresh. Прокрутка всегда живёт в ребёнке. Канонический экран — фиксированный хром (шапка,
таб-бар) плюс одно прокручиваемое тело с flexGrow: 1:
UIScreen(
Header("Profile", BackButton()), // фиксированный хром — стоит на месте
UIScrollable(content) // ОДНО прокручиваемое тело
.style({ flexGrow: 1, px: 24, gap: 16 })
.onRefresh(async () => { await load() }), // pull-to-refresh живёт на скроллируемом
).style({ bgColor: "black", pt: "safe-top" })Скролл-события (onScroll / onScrollRelease / onOverscroll), pull-to-refresh (onRefresh)
и стиль refreshControlColor принадлежат UIScrollable — и
UIVirtualizedList для длинных данных.
Router
Router.init(homePage, opts?: { showDefaultBackButton?: boolean }) // вызови один раз в файле входа
Router.push(screen) // положить в стек, экран становится активным
Router.pop(to?: number) // по умолчанию -1 = на экран назад
Router.replace(screen, opts?: { transition }) // подменить верхний экран
Router.current // вершина стека роутера (геттер)
Router.hide() // спрятать весь роутер (стек остаётся живым)
Router.restore() // вернуть его, реактивировав верхний экран
Presentable.current // то, что реально на экране прямо сейчас (или null)Router.current — вершина стека — он остаётся осмысленным, даже пока роутер приостановлен прямым
open(). Presentable.current — реально видимый сейчас пункт назначения (UIScreen, Scene, …,
или null до первого открытия) — к нему прикрепляется UIWidget.show().
pop(to)— отрицательные значения относительны (-2= на два экрана назад);0и положительные значения — абсолютный индекс в стеке (0= домашний экран). Экраны, покидающие стек, уничтожаются.- Переходы
replace:"slide-from-left" | "slide-from-right" | "slide-from-top" | "slide-from-bottom" | "zoom" | "zoom-out" | "zoom-in" | "fade" | "none"— по умолчанию"none", темируется на всё приложение:theme({ replaceTransition: "fade" })(любое имя или кастомная спецификация; явныйtransitionв вызове всё равно побеждает,nullсбрасывает — см. theme.md). - Замечание про платформы: desktop-хост пока не анимирует переходы — любой push/replace/pop
подменяет экран мгновенно, какой бы переход ни передали (имя или спецификация). Хореография
появления через
onOpen+animateFromработает везде и сегодня остаётся переносимой альтернативой. showDefaultBackButtonуinit(по умолчаниюfalse) показывает предоставленную хостом кнопку «назад».
Как экраны живут в стеке
Экраны ниже верхнего остаются смонтированными — их деревья элементов и состояние выживают, они
просто не рендерятся. Навигация запускает колбэки жизненного цикла: push вызывает onClose
уходящего экрана и onOpen входящего; pop вызывает их в обратном порядке, уничтожает нативные
деревья снятых экранов и снова вызывает onOpen на экране, куда ты приземляешься. Снятый объект
UIScreen всё ещё пригоден — повторный push его перемонтирует. Router.hide() вызывает onClose
верхнего экрана; restore() снова вызывает onOpen. Вызов screen.open(), пока роутер активен,
прячет роутер (его стек остаётся восстановимым).
События смены маршрута
Router.addEventListener("change", (screen: UIScreen) => …) // срабатывает после каждой навигации
Router.removeEventListener("change", cb)Колбэк получает вновь активный экран — полезно для общего таб-бара или аналитики.
Подводные камни
// ✗ скролл-методы на экране — экраны никогда не прокручиваются
UIScreen(...).onRefresh(load) // метода нет на экране
// ✓ прокрутка и pull-to-refresh живут на теле UIScrollable
UIScreen(UIScrollable(...).style({ flexGrow: 1 }).onRefresh(load))
// ✗ цикл, запущенный в onOpen, никогда не остановлен — продолжает работать после Router.push()
screen.onOpen(() => setLoop(update))
// ✓ спаруй его в onClose (срабатывает на каждый уход навигацией)Смотрите также
- UIWidget — глобальные оверлеи над всем (
show()) или прикреплённые к странице (attachTo(owner)). - Контейнеры — правила раскладки
UIColumn,UIScrollable. - Соглашения § Жизненный цикл.
- События указателя и жесты.