Эта страница переведена машинным способом и может содержать ошибки. Читать оригинал на английском

Элементы

Scroll

Scroll — это нативный scroll view (UIScrollView на iOS и Mac Catalyst, ScrollView на Android). Он размещает все свои дочерние элементы как содержимое и прокручивает их нативно: прокрутка, fling, bounce и snapping выполняются на стороне платформы с частотой дисплея. В Rust его создают с помощью scroll(), в TSX — с помощью <Scroll> из nsh.

el-scroll.tsx
el-scroll.tsxLive
import { mount, Scroll, type ScrollRef, signal, Text, View } from "nsh";
import { button, buttonText, desktop, Inspector, Panel, size, ui, wide } from "./palette";

// The page colors, read when they render: they follow the theme.
const colors = (): string[] => [ui().accent, ui().info, ui().violet, ui().down, ui().warn, ui().up];

function App() {
  const page = signal(0);
  // A page is as wide as the scroll view: 390 on a phone, wider on a desktop.
  const width = signal(390);
  const y = signal(0);
  let scroller!: ScrollRef;
  const rows = Array.from({ length: 40 }, (_, i) => i + 1);

  /** Pages as wide as the scroll view; `inset`: each page's margin. */
  const pager = (inset: number, style: object) => (
    <Scroll
      horizontal
      pagingEnabled
      scrollbar={false}
      style={style}
      onLayout={({ bounds }) => width.set(bounds.size.width)}
      onMomentumEnd={({ offset }) => page.set(Math.round(offset.x / width()))}
    >
      <Each of={colors()}>
        {(color, index) => (
          <View style={{ width: width(), padding: inset }}>
            <View style={{ flex: 1, borderRadius: 16, backgroundColor: color(), alignItems: "center", justifyContent: "center" }}>
              <Text style={{ color: ui().page, fontSize: 32, fontWeight: "700" }}>{index() + 1}</Text>
            </View>
          </View>
        )}
      </Each>
    </Scroll>
  );
  const toTop = (compact: boolean) => (
    <View
      onPress={() => scroller.scrollTo({ y: 0, animated: true })}
      {...button("secondary", { size: compact ? "md" : "sm" })}
    >
      <Text style={buttonText()}>scrollTo top</Text>
    </View>
  );

  return (
    <View style={{ flex: 1, backgroundColor: ui().page, paddingTop: 24, gap: 12 }}>
      {() =>
        wide() ? (
          <View style={{ flex: 1, paddingHorizontal: 20, paddingBottom: 20, gap: 14 }}>
            <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>scroll</Text>
            <Text style={{ color: ui().muted }}>
              {desktop ? "Shift+wheel or swipe the trackpad across the pages; the wheel scrolls the rows" : "Swipe the pages and the rows"}
            </Text>
            <View style={{ flex: 1, flexDirection: "row", gap: 16 }}>
              <View style={{ flex: 1, minWidth: 0, gap: 12 }}>
                {pager(0, { height: 150, flexGrow: 0, borderRadius: 16 })}
                {/* The scroll view fills its region, so its scrollbar sits at the region's edge. */}
                <View style={{ flex: 1, borderRadius: 16, borderWidth: 1, borderColor: ui().line, overflow: "hidden" }}>
                  <Scroll ref={scroller} style={{ flex: 1 }} scrollEventThrottle={16} onScroll={(e) => y.set(e.offset.y)}>
                    <View style={{ padding: 12, gap: 6 }}>
                      <Each of={rows}>
                        {(n) => (
                          <View style={{ ...ui().row, borderRadius: 8, height: size.row, paddingHorizontal: 12, justifyContent: "center" }}>
                            <Text style={{ color: ui().text, fontSize: size.label }}>Row {n()}</Text>
                          </View>
                        )}
                      </Each>
                    </View>
                  </Scroll>
                </View>
              </View>
              <Inspector>
                <Panel title="Horizontal, pagingEnabled · onMomentumEnd">
                  <Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>
                    page {page() + 1} of {colors().length}
                  </Text>
                </Panel>
                <Panel title="Vertical · onScroll">
                  <Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>y = {Math.round(y())}</Text>
                </Panel>
                {toTop(true)}
              </Inspector>
            </View>
          </View>
        ) : (
          <>
            <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text, paddingHorizontal: 20 }}>scroll</Text>

            <Text style={{ color: ui().muted, paddingHorizontal: 20 }}>
              Horizontal, pagingEnabled · page {page() + 1} · {desktop ? "Shift+wheel or swipe the trackpad" : "swipe"}
            </Text>
            {pager(12, { height: 140, flexGrow: 0 })}

            <View style={{ width: "100%", maxWidth: 640, alignSelf: "center", flexDirection: "row", justifyContent: "space-between", alignItems: "center", paddingHorizontal: 20 }}>
              <Text style={{ color: ui().muted }}>Vertical · y = {Math.round(y())}</Text>
              {toTop(false)}
            </View>
            <Scroll ref={scroller} style={{ flex: 1 }} scrollEventThrottle={16} onScroll={(e) => y.set(e.offset.y)}>
              <View style={{ width: "100%", maxWidth: 640, alignSelf: "center", paddingHorizontal: 20, paddingBottom: 20, gap: 8 }}>
                <Each of={rows}>
                  {(n) => (
                    <View style={{ ...ui().row, borderRadius: 12, padding: 14 }}>
                      <Text style={{ color: ui().text }}>Row {n()}</Text>
                    </View>
                  )}
                </Each>
              </View>
            </Scroll>
          </>
        )
      }
    </View>
  );
}

mount(App);
use nsh_core::prelude::*;

scroll().flex_1().child(
    view().p_4().gap_2().child(each(
        rows,
        |row: &Row| row.id,
        |row, _index| text(move || row.get().title),
    )),
)

Scroll view создаёт всех своих потомков, что подходит для страниц умеренного размера. Для длинных или неограниченных данных используйте list: он создаёт строки рядом с видимой областью и переиспользует их.

Scroll view нужен ограниченный размер вдоль его оси: задайте ему flex: 1 (.flex_1()) или высоту внутри контейнера, у которого она есть. Его содержимое размещается по ширине scroll view (по высоте, если он горизонтальный), а по другой оси имеет столько, сколько ему нужно.

#Props

Помимо общих props (где style имеет тип ViewStyle). В Rust props прокрутки являются методами трейта Scrollable, общего для списков.

#Scrolling

PropRustTypeDefaultDescription
horizontal.horizontal()booleanfalseПрокручивается по горизонтали.
scrollable.scrollable(..)booleantrueПользователь может его прокручивать.
scrollbar.scrollbar(..)boolean | ScrollbarThemetrueПоказывается индикатор прокрутки: платформенный или встроенная полоса приложения, если оно задало тему; объект — это тема самого представления. На десктопе и в десктопных браузерах горизонтальное представление сохраняет его: это полоса прокрутки мыши.
bounces.bounces(..)booleantrueПрокрутка за границы (overscroll).
pagingEnabled.paging(..)booleanfalseОстановка на кратных размеру видимой области позициях.
snapToInterval.snap_interval(px(..))number0Остановка на позициях, кратных этой длине (0: выключено).
snapToOffsets.snap_offsets(..)number[]Остановка на этих смещениях.
snapToAlignment.snap_alignment(SnapAlignment::Center)"start" | "center" | "end""start"Какая точка элемента выравнивается с видимой областью при snapToInterval.
disableIntervalMomentum.disable_interval_momentum(..)booleanfalseFling сдвигает не более чем на одну точку привязки.
decelerationRate.deceleration(rate)"normal" | "fast" | numberплатформенноеКак быстро затухает fling: доля скорости, сохраняемая за миллисекунду (normal 0.998, fast 0.99).
contentInset.content_inset(EdgeInsets { .. })Partial<Edges>Отступ вокруг содержимого.
contentOffset.content_offset(point(..)){ x?, y? }Откуда начинается прокрутка.
contentStyleсобственные стили content viewViewStyleСтиль представления вокруг дочерних элементов, которое прокручивается вместе с ними; flexGrow: 1 делает его не ниже видимой области (по центру — пустое состояние).
contentClassсобственные стили content viewstringТо же, что classes, например "grow justify-center p-4".
keyboardDismissMode.keyboard_dismiss_mode(KeyboardDismissMode::OnDrag)"none" | "on-drag" | "interactive""none"Скрывает ли перетаскивание клавиатуру.
keyboardShouldPersistTaps.keyboard_taps(KeyboardTaps::Handled)"never" | "always" | "handled" | boolean"never"Скрывает ли касание клавиатуру: never (касание только скрывает её), always (она остаётся), handled (она остаётся, если касание что-то обрабатывает).
scrollValue.scroll_value(&value)AnimatedValueАнимируемое значение, получающее смещение прокрутки (по главной оси), для анимаций, связанных с прокруткой.
scrollEventThrottle.event_throttle(ms)numberМинимальное число миллисекунд между событиями onScroll (в любом случае не чаще одного за кадр).

#Pull to refresh

PropRustTypeDescription
refreshing.refreshing(..)booleanПоказывается индикатор обновления.
onRefresh.on_refresh(..)() => voidПользователь потянул для обновления: начните загрузку и держите refreshing, пока она не закончится.
refreshIndicator.refresh_indicator(Some(height)){ height: number }Приложение рисует собственный индикатор высотой height пунктов: платформенный спиннер скрыт, обновление срабатывает, когда натяжение достигает height, а во время обновления над содержимым сохраняется height. Высота 0 или меньше означает отсутствие индикатора.
pullValue.pull_value(&value)AnimatedValueАнимируемое значение, получающее величину натяжения представления за его верхний край (0 в покое, растёт с натяжением; место, сохраняемое над содержимым во время обновления: height при refreshIndicator), для индикатора приложения.
let refreshing = signal(false);

scroll()
    .flex_1()
    .refreshing(refreshing)
    .on_refresh(move || {
        refreshing.set(true);
        spawn(async move {
            load().await;
            refreshing.set(false);
        });
    })
    .child(content())

#Zooming

Если задать maximumZoomScale больше minimumZoomScale (.zoom_range(min, max) в Rust), содержимое можно масштабировать щипком; при увеличении оно прокручивается в обе стороны. Списки сохраняют свой масштаб.

PropRustTypeDefaultDescription
minimumZoomScale.zoom_range(min, max)number1Наименьший масштаб.
maximumZoomScale.zoom_range(min, max)number1Наибольший масштаб.
zoomScale.zoom(..)numberМасштабирует до этого значения при его изменении; в остальных случаях масштаб пользователя сохраняется.
bouncesZoom.bounces_zoom(..)booleantrueЩипок может выйти за пределы и пружинно вернуться.
zoomable.zoomable(..)booleantrueПользователь может масштабировать щипком.
centerContent.center_content(..)booleanfalseСодержимое меньше видимой области (при уменьшении) центрируется.
let photo = element_ref();

scroll()
    .element_ref(&photo)
    .zoom_range(1.0, 4.0)
    .center_content(true)
    .flex_1()
    .bg(black())
    .child(
        image(PICTURE)
            .w(px(390.0))
            .h(px(520.0))
            .on_press(move || photo.zoom_to(1.0, None, true)),
    )

#Events

EventRustPayloadWhen
onScroll.on_scrollScrollEventСмещение изменилось (не чаще одного раза за кадр и не чаще, чем scrollEventThrottle).
onScrollDragStart / onScrollDragEnd.on_scroll_drag_start / .on_scroll_drag_end{ offset } (Point<Points>)Пользователь начал / закончил перетаскивание.
onMomentumStart / onMomentumEnd.on_momentum_start / .on_momentum_end{ offset }Скольжение после перетаскивания началось / закончилось. Скольжение, прерванное новым перетаскиванием, всё равно завершается событием onMomentumEnd.
onScrollEnd.on_scroll_end{ offset }Прокрутка остановилась.
onRefresh.on_refreshнетПотянули для обновления.

ScrollEvent — это { offset: Point; contentSize: Size; size: Size; zoomScale } (в Rust offset, content_size, size, zoom_scale): смещение, размер содержимого (с учётом масштаба), размер видимой области и масштаб (1, если представление не масштабируется).

#Ref methods

MethodRustDescription
scrollTo({ x?, y?, animated? })scroll_to(point(x, y), animated)Прокручивает к смещению.
zoomTo({ scale, x?, y?, animated? })zoom_to(scale, Some(point(x, y)), animated)Масштабирует до scale (в пределах ограничений) так, чтобы точка содержимого (x, y, в немасштабированных пунктах) оказалась в середине видимой области, либо относительно середины. Например, двойное касание, увеличивающее масштаб в точке касания.
let scroller = element_ref();
scroll().element_ref(&scroller).child(content());

scroller.scroll_to(point(px(0.0), px(0.0)), true);

#Keyboard

Поля ввода внутри scroll view нативно остаются вне зоны клавиатуры: scroll view получает нижний отступ и сам прокручивается к полю в фокусе. Для содержимого вне scroll view используйте env(keyboard-inset-height) в нижнем padding или margin. См. forms and the keyboard.

#Right to left

Горизонтальные scroll view в раскладках справа налево начинаются с правого края и сохраняют расстояние до него при изменении содержимого; их дочерние элементы зеркально отражаются.

#Platform notes

  • Android: pull to refresh рисуется NSH (overscroll остаётся нативным), contentInset применяется как padding, а decelerationRate задаёт форму скольжений при snapping.
  • Android масштабирует с помощью собственного scroll view из NSH, который масштабирует содержимое относительно его левого верхнего угла и прокручивает в обе стороны. Щипок начинается после минимального расстояния между пальцами, принятого в Android (около 27 мм).
  • На Mac Catalyst (Mac idiom) NSH сам определяет натяжение и показывает свой спиннер.
  • iOS перерисовывает тексты и canvas в масштабированном разрешении, когда масштабирование заканчивается.