LeCodesdocs

UIScreen и Router

UIScreen — корень каждого UI-дерева — он всегда заполняет устройство и ведёт себя как UIColumn (или как скролл-контейнер после .makeScrollable()). Одноэкранное приложение вызывает screen.open(); многостраничное отдаёт свои экраны Router и навигирует через push / pop / replace.

Обзор

TypeScript
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

TypeScript
UIScreen(): UIScreen
UIScreen(children: UINodeChild[]): UIScreen

screen.open(): void     // показать этот экран напрямую (одноэкранные приложения; прячет активный Router)
screen.close(): void    // закрыть напрямую открытый экран

Полная поверхность контейнера: .append / .insert / .remove / .setContent, .style, .animateTo / .animateFrom, .onLayout.

Note

экран всегда заполняет устройство — стили размера на нём (width, height, flexGrow, flexShrink, position) — no-op. Стилизуй его отступы, фон и раскладку детей. Фон по умолчанию чёрный; всегда задавай bgColor и цвета текста явно.

Жизненный цикл — onOpen / onClose

TypeScript
screen.onOpen(cb: () => void): this    // экран стал активным
screen.onClose(cb: () => void): this   // экран перестал быть активным

Они срабатывают на каждую активацию, не только на первую: уход с экрана вызывает его onClose, возврат к нему — снова onOpen. Всё запущенное в onOpen должно останавливаться в onClose — циклы, интервалы, сокеты — иначе продолжит работать за другими экранами. См. Жизненный цикл.

TypeScript
let loopId: number | null = null
screen
  .onOpen(() => { loopId = setLoop(dt => tick(dt)) })
  .onClose(() => { if (loopId !== null) { clearLoop(loopId); loopId = null } })

Касания и кнопка «назад»

TypeScript
screen.onTouchStart(ev => …)          // срабатывает на касания где угодно на экране (полноэкранные жесты)
screen.onBackPressed(cb: () => void)  // аппаратный/жестовый «назад» Android — обычно Router.pop()

onTouchStart поддерживает ev.track() как кнопка — см. События указателя.

Прокрутка — makeScrollable()

Экран не прокручивается по умолчанию. makeScrollable() превращает весь экран в один вертикальный скролл-поток и открывает три метода, которых иначе нет:

TypeScript
const screen = UIScreen([...])
  .makeScrollable()                          // возвращает тот же экран, теперь прокручиваемый
  .onRefresh(async () => { await load() })   // pull-to-refresh; спиннер прячется, когда промис разрешается
screen.onScroll(pos => { })                  // позиция прокрутки в px
screen.onOverscroll(pos => { })              // протягивание за край
Note

из трёх только onRefresh объявлен чейнящимся (возвращает экран); вызывай onScroll / onOverscroll как операторы.

refreshControlColor (стиль экрана) тонирует нативный спиннер pull-to-refresh.

Note

pull-to-refresh есть только на makeScrollable() — дочерний UIScrollable не может его дать. Правило: весь экран — один поток (статья, лента, длинная форма) → makeScrollable(); скролл-область внутри фиксированной раскладки (список под закреплённой шапкой) → дочерний UIScrollable.

Router

TypeScript
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(), пока роутер активен, прячет роутер (его стек остаётся восстановимым).

События смены маршрута

TypeScript
Router.addEventListener("change", (screen: UIScreen) => …)   // срабатывает после каждой навигации
Router.removeEventListener("change", cb)

Колбэк получает вновь активный экран — полезно для общей таб-панели или аналитики.

Подводные камни

TypeScript
// ✗ onRefresh / onScroll без makeScrollable()
UIScreen([...]).onRefresh(load)          // метода нет на обычном экране
// ✓
UIScreen([...]).makeScrollable().onRefresh(load)

// ✗ цикл, запущенный в onOpen, никогда не остановлен — продолжает работать после Router.push()
screen.onOpen(() => setLoop(update))
// ✓ спаруй его в onClose (срабатывает на каждый уход навигацией)

Смотрите также