UIScreen и Router
UIScreen — корень каждого UI-дерева — он всегда заполняет устройство и ведёт себя как UIColumn
(или как скролл-контейнер после .makeScrollable()). Одноэкранное приложение вызывает
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(): UIScreen
UIScreen(children: 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() как кнопка — см.
События указателя.
Прокрутка — makeScrollable()
Экран не прокручивается по умолчанию. makeScrollable() превращает весь экран в один
вертикальный скролл-поток и открывает три метода, которых иначе нет:
const screen = UIScreen([...])
.makeScrollable() // возвращает тот же экран, теперь прокручиваемый
.onRefresh(async () => { await load() }) // pull-to-refresh; спиннер прячется, когда промис разрешается
screen.onScroll(pos => { }) // позиция прокрутки в px
screen.onOverscroll(pos => { }) // протягивание за крайиз трёх только onRefresh объявлен чейнящимся (возвращает экран); вызывай onScroll / onOverscroll
как операторы.
refreshControlColor (стиль экрана) тонирует нативный спиннер pull-to-refresh.
pull-to-refresh есть только на makeScrollable() — дочерний UIScrollable не может его дать.
Правило: весь экран — один поток (статья, лента, длинная форма) → makeScrollable(); скролл-область
внутри фиксированной раскладки (список под закреплённой шапкой) → дочерний UIScrollable.
Router
Router.init(homePage, opts?: { showDefaultBackButton?: boolean }) // вызови один раз в файле входа
Router.push(screen) // положить в стек, экран становится активным
Router.pop(to?: number) // по умолчанию -1 = на экран назад
Router.replace(screen, opts?: { transition }) // подменить верхний экран
Router.current // активный UIScreen (геттер)
Router.hide() // спрятать весь роутер (стек остаётся живым)
Router.restore() // вернуть его, реактивировав верхний экран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"— по умолчанию"fade". 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)Колбэк получает вновь активный экран — полезно для общей таб-панели или аналитики.
Подводные камни
// ✗ onRefresh / onScroll без makeScrollable()
UIScreen([...]).onRefresh(load) // метода нет на обычном экране
// ✓
UIScreen([...]).makeScrollable().onRefresh(load)
// ✗ цикл, запущенный в onOpen, никогда не остановлен — продолжает работать после Router.push()
screen.onOpen(() => setLoop(update))
// ✓ спаруй его в onClose (срабатывает на каждый уход навигацией)Смотрите также
- UIWidget — оверлеи, плавающие над стеком экранов.
- Контейнеры — правила раскладки
UIColumn,UIScrollable. - Соглашения § Жизненный цикл.
- События указателя и жесты.