このページは機械翻訳のため、誤りを含む場合があります。 英語の原文を読む

ドキュメント

スクロール

スクロールビュー(Rust では scroll()、TSX では <Scroll>)は、プラットフォーム標準のスクロールビュー(iOS では UIScrollView、Android では ScrollView)です。フリング、バウンス、スクロールインジケーター、プルトゥリフレッシュ、スナップはすべてネイティブで動作します。コードではイベントを通じてスクロールを検知できます。また、オフセットをアニメーション値にバインドすると、指の動きにフレーム単位で追従するエフェクトを作れます。

guide-scrolling.tsx
guide-scrolling.tsxLive
import { mount, Scroll, type ScrollRef, signal, Text, View } from "nsh";
import { createAnimatedValue } from "nsh";
import { button, buttonText, desktop, size, tint, ui, wide } from "./palette";

// Tints of the info, violet, danger, warn and up colours under the text colour.
const hues = ["info", "violet", "down", "warn", "up", "faint"] as const;
const hue = (i: number) => tint(ui()[hues[i % hues.length]], 0.28);

/** Pages as wide as the scroll view, with a page indicator. */
function Pager() {
  const width = signal(340);
  const page = signal(0);
  return (
    <View style={{ gap: 8 }}>
      <Scroll
        horizontal
        pagingEnabled
        scrollbar={false}
        style={{ height: 110, borderRadius: 12 }}
        onLayout={({ bounds }) => width.set(bounds.size.width)}
        onMomentumEnd={({ offset }) => page.set(Math.round(offset.x / width()))}
        onScrollEnd={({ offset }) => page.set(Math.round(offset.x / width()))}
      >
        <View style={{ flexDirection: "row" }}>
          <Each of={[0, 1, 2, 3]}>
            {(i) => (
              <View style={{ width: width(), height: 110, backgroundColor: hue(i()), alignItems: "center", justifyContent: "center" }}>
                <Text style={{ color: ui().text, fontSize: 24, fontWeight: "700" }}>Page {i() + 1}</Text>
              </View>
            )}
          </Each>
        </View>
      </Scroll>
      <View style={{ flexDirection: "row", justifyContent: "center", gap: 6 }}>
        <Each of={[0, 1, 2, 3]}>
          {(i) => <View style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: page() === i() ? ui().accent : ui().border }} />}
        </Each>
      </View>
    </View>
  );
}

/** Cards that snap: one stop every card plus gap. */
function Cards() {
  return (
    <Scroll horizontal scrollbar={false} snapToInterval={152} decelerationRate="fast" style={{ height: 90 }}>
      <View style={{ flexDirection: "row", gap: 12, paddingHorizontal: 4 }}>
        <Each of={Array.from({ length: 12 }, (_, i) => i)}>
          {(i) => (
            <View
              style={{
                width: 140,
                height: 90,
                borderRadius: 14,
                backgroundColor: hue(i()),
                boxShadow: "inset 0 1px 0 rgba(255,255,255,0.08)",
                alignItems: "center",
                justifyContent: "center",
              }}
            >
              <Text style={{ color: ui().text, fontWeight: "700" }}>Card {i() + 1}</Text>
            </View>
          )}
        </Each>
      </View>
    </Scroll>
  );
}

function App() {
  // The vertical offset, written natively in the frame it changes.
  const offset = createAnimatedValue(0);
  const y = signal(0);
  const refreshing = signal(false);
  const refreshed = signal(0);
  let scroll!: ScrollRef;
  const refresh = () => {
    refreshing.set(true);
    setTimeout(() => {
      refreshed.update((n) => n + 1);
      refreshing.set(false);
    }, 1200);
  };
  return (
    <View style={{ flex: 1, backgroundColor: ui().page, paddingTop: 24 }}>
      {/* A bar that fades in and shrinks its title as the page scrolls: no JS per frame. */}
      <View
        style={{
          height: 48,
          paddingHorizontal: 16,
          justifyContent: "center",
          backgroundColor: offset.interpolate({ inputRange: [0, 80], outputRange: [ui().page, ui().card.backgroundColor] }),
          borderBottomWidth: 1,
          borderBottomColor: ui().line,
        }}
      >
        <Text
          style={{
            fontSize: 22,
            fontWeight: "700",
            color: ui().text,
            transformOrigin: "left center",
            scale: offset.interpolate({ inputRange: [0, 80], outputRange: [1, 0.8] }),
          }}
        >
          Scrolling
        </Text>
      </View>
      <Scroll
        ref={scroll}
        style={{ flex: 1 }}
        scrollValue={offset}
        scrollEventThrottle={100}
        onScroll={(e) => y.set(Math.round(e.offset.y))}
        refreshing={refreshing()}
        onRefresh={refresh}
        keyboardDismissMode="on-drag"
      >
        <View style={{ padding: 16, gap: 18, maxWidth: wide() ? 720 : undefined }}>
          <Text style={{ color: ui().muted }}>
            {desktop ? `Scroll with the wheel or the trackpad. onScroll: y = ${y()}` : `Pull down to refresh (${refreshed()}×). onScroll: y = ${y()}`}
          </Text>
          <Text style={{ fontWeight: "600", color: ui().text }}>pagingEnabled</Text>
          <Pager />
          <Text style={{ fontWeight: "600", color: ui().text }}>snapToInterval</Text>
          <Cards />
          <View style={{ gap: desktop ? 8 : 18 }}>
            <Each of={Array.from({ length: 24 }, (_, i) => i + 1)}>
              {(n) => (
                <View
                  style={
                    desktop
                      ? { ...ui().row, height: size.row, justifyContent: "center", paddingHorizontal: 12, borderRadius: 8 }
                      : { ...ui().row, padding: 14, borderRadius: 12 }
                  }
                >
                  <Text style={{ color: ui().text, fontSize: desktop ? size.label : undefined }}>Row {n()}</Text>
                </View>
              )}
            </Each>
          </View>
          <View
            onPress={() => scroll.scrollTo({ y: 0 })}
            {...button("primary")}
          >
            <Text style={buttonText("primary")}>Back to the top (ref.scrollTo)</Text>
          </View>
        </View>
      </Scroll>
    </View>
  );
}

mount(App);

スクロールビューは、設定画面、フォーム、記事、チップの横並びなど、サイズが既知で中程度のコンテンツに使います。長いデータや上限のないデータにはリストを使ってください。リストは行を再利用し、同じスクロール用 props を受け付けます。

#スクロールビュー

スクロールビューはビューポートであり、その子要素がコンテンツになります。ビューポートのサイズは親から与える必要があります(通常は flex: 1 や .flex_1())。コンテンツは、ビューポートの幅(縦スクロール)または高さ(横スクロール)に合わせて配置され、長さは必要なだけ伸びます。

#[component]
pub fn App() -> impl IntoElement {
    scroll().flex_1().bg(rgb(0xf6f8fa)).child(
        view().p_4().gap_3().children((1..=50).map(|n| {
            view().p_4().rounded(px(10.0)).bg(white()).child(text(format!("Row {n}")))
        })),
    )
}

上の子ビューはスクロールビューのコンテンツコンテナです。TSX では contentStyle(または contentClass)を指定すると、そのスタイルを持つビューで子要素が自動的にラップされ、一緒にスクロールします。flexGrow: 1(.flex_grow(1.0))を指定するとコンテンツの高さがビューポート以上になるため、短いコンテンツは中央に配置でき、長いコンテンツは引き続きスクロールできます。

scroll().flex_1().child(
    view().flex_grow(1.0).justify_center().p_4().child(
        when(move || messages.with(|m| !m.is_empty()), move || {
            each(messages, |m| m.id, |m, _| text(move || m.with(|m| m.text.clone())))
        })
        .otherwise(|| text("No messages yet")),
    ),
)

横スクロールのビューには horizontal(.horizontal())を指定し、子として行(row)を渡します。

scroll().horizontal().scrollbar(false).child(
    view().flex_row().gap_2().px_4().children(["Today", "This week", "This month", "This year"].map(|label| {
        text(label).px(px(14.0)).py_2().rounded(px(16.0)).overflow_hidden().bg(white())
    })),
)

右から左へ表示する言語(デバイスと環境を参照)では、横スクロールビューは右端から始まり、右端からの距離を保ちます。

スクロールビューは指のドラッグで操作します。デスクトップ(macOS、Windows、Linux)とブラウザーでは、マウスはデスクトップアプリと同じようにスクロールします。ホイール(Shift + ホイールで横スクロール)、タッチパッド、キーボード、スクロールインジケーターが使えます。インジケーターはマウスがビュー上にあるあいだ表示され、スクロールバーとしてドラッグできます。マウスのドラッグはアプリのパン、レスポンダー、テキスト選択に渡されます。デスクトップのタッチスクリーンでは、指と同じようにスクロールビューをドラッグできます。横スクロールのビューでは、scrollbar={false} を指定してもインジケーターが残ります。横ホイールのないマウスでは、これが横スクロールの手段になるためです。

#動作に関する props

PropRust型デフォルト説明
horizontal.horizontal()booleanfalse横方向にスクロールします。
scrollable.scrollable(..)booleantrueユーザーがスクロールできるかどうかを指定します。コードからのスクロールは引き続き動作します。
bounces.bounces(..)booleantrue端を越えるオーバースクロールを許可します。
scrollbar.scrollbar(..)booleantrueスクロールインジケーターを表示します。デスクトップとデスクトップブラウザーでは、横スクロールのビューはインジケーターを保持します。これはマウス用のスクロールバーです。
contentInset.content_inset(EdgeInsets { .. })Partial<Edges>コンテンツの周囲の余白で、その中までスクロールできます。
contentOffset.content_offset(point(..)){ x?, y? }スクロールの開始位置です。
decelerationRate.deceleration(rate)"normal" | "fast" | numberプラットフォームの値フリングが減速する速さです。"normal"、"fast"、またはミリ秒あたりに保持される速度を指定します(0.998 が標準、0.99 が高速、0 はプラットフォームのデフォルト)。

#ページングとスナップ

スナップは、指を離した時点で、離した位置のオフセット、予測される慣性移動量、速度をもとにネイティブ側で決定されます。その後、スクロールビューは選ばれた停止位置まで滑らかに移動します。コードの介入は不要です。

PropRust型説明
pagingEnabled.paging(true)booleanビューポートのサイズの倍数の位置で停止します。
snapToInterval.snap_interval(px(..))numberこの長さの倍数の位置で停止します(0 でオフ)。
snapToOffsets.snap_offsets(vec![..])number[]指定したオフセットで停止します。
snapToAlignment.snap_alignment(SnapAlignment::Center)"start" | "center" | "end"snapToInterval と併用し、アイテムのどの位置をビューポートに揃えるかを指定します。
disableIntervalMomentum.disable_interval_momentum(true)booleanフリングで移動できるのは最大で 1 つの停止位置までです。

ページャーの例です。スクロールビューと同じ幅のページと、スクロールの慣性移動が終わったときに更新されるインジケーターを使います。

const COLORS: [u32; 4] = [0x0969da, 0x8250df, 0xbf3989, 0x1a7f37];

pub fn pager() -> impl IntoElement {
    let width = signal(0.0_f32);
    let page = signal(0_usize);
    let settle = move |e: &Point<Points>| {
        let width = width.get_untracked();
        if width > 0.0 {
            page.set((e.x.0 / width).round() as usize);
        }
    };
    let pages = COLORS.iter().enumerate().map(|(i, &color)| {
        view()
            .w(move || px(width.get()))
            .h(px(160.0))
            .bg(rgb(color))
            .items_center()
            .justify_center()
            .child(text(format!("Page {}", i + 1)).text_color(white()).text_2xl().font_weight(FontWeight::BOLD))
    });
    let dots = (0..COLORS.len()).map(|i| {
        view()
            .size_2()
            .rounded(px(4.0))
            .bg(move || if page.get() == i { rgb(0x0969da) } else { rgb(0xd0d7de) })
    });
    view()
        .gap_2()
        .p_4()
        .child(
            scroll()
                .horizontal()
                .paging(true)
                .scrollbar(false)
                .h(px(160.0))
                .rounded(px(12.0))
                .on_layout(move |frame: &Bounds<Points>| width.set(frame.size.width.0))
                .on_scroll_end(settle)
                .child(view().flex_row().children(pages)),
        )
        .child(view().flex_row().justify_center().gap(px(6.0)).children(dots))
}

中央にスナップするカードのカルーセルの例です。1 回のフリングで 1 枚ずつ進みます。

scroll()
    .horizontal()
    .snap_interval(px(232.0))          // card width (220) + gap (12)
    .snap_alignment(SnapAlignment::Center)
    .deceleration(0.99)
    .disable_interval_momentum(true)
    .scrollbar(false)
    .child(view().flex_row().gap_3().px_4().children(cards))

リストも同じスナップ用 props を受け付けるため、長いカルーセルにはリストのほうが適しています。

#スクロールイベント

PropRustペイロードタイミング
onScroll.on_scroll(..)ScrollEventオフセットが変化したとき。1 フレームにつき最大 1 回です。scrollEventThrottle で間隔をさらに空けられます。
scrollEventThrottle.event_throttle(ms)numberonScroll イベント間の最小間隔(ミリ秒)です。
onScrollDragStart.on_scroll_drag_start(..){ offset }ユーザーがドラッグを開始したとき。
onScrollDragEnd.on_scroll_drag_end(..){ offset }ユーザーが指を離したとき。
onMomentumStart.on_momentum_start(..){ offset }フリング後の慣性移動が始まったとき。
onMomentumEnd.on_momentum_end(..){ offset }慣性移動が終わったとき(新たなドラッグで止められた場合も含みます)。
onScrollEnd.on_scroll_end(..){ offset }ユーザーによるスクロールが静止したとき。慣性移動の後、または慣性移動なしでドラッグが終わった時点です。

ScrollEvent には、オフセット(offset、型は Point。Rust でも同じ)、コンテンツのサイズ、ズーム後のサイズ(contentSize、型は Size。content_size)、ビューポート(size)、zoomScale(zoom_scale)が含まれます。その他のイベントにはオフセットが含まれます。TSX では { offset } が Point として、Rust では Point<Points> として渡されます。

let at_bottom = signal(false);

scroll()
    .flex_1()
    .event_throttle(100.0)
    .on_scroll(move |e: &ScrollEvent| {
        at_bottom.set(e.offset.y + e.size.height >= e.content_size.height - px(20.0))
    })
    .child(content)

#スクロール連動エフェクト: scrollValue

scrollValue(.scroll_value(&value))はアニメーション値を受け取り、コンテンツが動くのと同じフレームで、(主軸方向の)スクロールオフセットをネイティブ側でその値に書き込みます。この値、またはそこから interpolate、diffClamp(diff_clamp)や算術演算で派生させたノードを、スタイルにバインドします。

#[component]
pub fn App() -> impl IntoElement {
    let offset = animated_value(0.0);
    view()
        .flex_1()
        .child(
            view()
                .h(px(56.0))
                .justify_center()
                .px_4()
                // Transparent at the top, white after 80 points.
                .bind_background(&offset.interpolate_colors(&[0.0, 80.0], &[rgba(0xffffff00), rgba(0xffffffff)]))
                .child(
                    text("Inbox")
                        .text_size(px(22.0))
                        .font_weight(FontWeight::BOLD)
                        .origin_left()
                        .bind_scale(&offset.interpolate(&[0.0, 80.0], &[1.0, 0.8])),
                ),
        )
        .child(scroll().flex_1().scroll_value(&offset).child(view().p_4().gap_2()))
}

上方向にスクロールすると隠れ、下方向にスクロールするとすぐに戻るヘッダーは、オフセットの diffClamp で実現できます。

let offset = animated_value(0.0);
let header = offset.diff_clamp(0.0, 56.0);

view()
    .flex_1()
    .child(scroll().flex_1().scroll_value(&offset).child(view().pt(px(56.0))))
    .child(view().absolute().top_0().left_0().right_0().h(px(56.0)).bind_translate_y(&header.mul(-1.0)))

補間はデフォルトで入力範囲の外側をクランプします。そのため、上端を越えて引っ張った場合(負のオフセット)でも、エフェクトは終端の状態のままです。引っ張ると伸びるヘッダーにしたい場合は extrapolate: "extend"(Rust では interpolate_with(.., Interpolation::extrapolate(Extrapolate::Extend)))を渡してください。

#プルトゥリフレッシュ

refreshing と onRefresh(.refreshing(..) と .on_refresh(..))を指定すると、プラットフォーム標準のプルトゥリフレッシュコントロールが追加されます。コントロールは refreshing が true のあいだ表示されます。onRefresh 内で true に設定し、処理が終わったら解除してください。

let refreshing = signal(false);
let reload = move || {
    refreshing.set(true);
    spawn(async move {
        if let Ok(response) = fetch("https://api.example.com/items").await {
            items.set(parse_items(&response.text()));
        }
        refreshing.set(false);
    });
};

scroll().flex_1().refreshing(refreshing).on_refresh(reload).child(content)

iOS ではコントロールとして UIRefreshControl が使われます。Mac Catalyst の Mac イディオムでは、NSH が同じ動作を持つ独自のスピナーを表示します。リストも同じ props を受け付けます。デスクトップとブラウザーでは、マウスはドラッグせずにスクロールするため、プルはタッチスクリーン(または上端を越えて引っ張れる macOS のトラックパッド)のジェスチャーになります。そのため、ボタンやキーボードショートカットなど、別の更新手段も用意してください。

#独自のインジケーター

プルの見た目はアプリ側で自由に決められます。refreshIndicator={{ height }} を指定すると、アプリが独自のインジケーターを描画することになります。height はポイント単位の高さです。プラットフォームのスピナーは非表示になり、プルが height に達すると更新が行われ、更新中はインジケーター用に height がコンテンツの上に確保されます。pullValue は、コンテンツが上端を越えて引っ張られた距離をアニメーション値に供給します。静止時は 0、引っ張るにつれて増加し、更新中は height になります。インジケーターはスクロールビューの上端に重ねて描画し、その値からスタイルを補間してください。スタイルは UI スレッド上で指に追従し、フレームごとの JS 実行は不要です。

let pull = animated_value(0.0);

view()
    .flex_1()
    .child(
        scroll()
            .flex_1()
            .refreshing(refreshing)
            .on_refresh(reload)
            .pull_value(&pull)
            .refresh_indicator(Some(px(72.0)))
            .child(content),
    )
    .child(
        view()
            .pointer_events(PointerEvents::None)
            .absolute()
            .top_0()
            .left_0()
            .right_0()
            .h(px(72.0))
            .items_center()
            .justify_center()
            .bind_opacity(&pull.interpolate(&[0.0, 36.0], &[0.0, 1.0]))
            .bind_rotate(&pull.interpolate(&[36.0, 72.0], &[0.0, 180.0]))
            .child(arrow()),
    )

height はビューポートの半分以内に収めてください。上端を越えると、引っ張るほどコンテンツが指に対して強く抵抗するため、遠すぎる高さには到達できません。iOS では、アプリ独自のインジケーターは UIRefreshControl に代わり、Mac イディオムと同様に NSH 自身が検出したプルを使います。

#ズーム

スクロールビュー(リストのスケールは 1 のままです)は、最大ズームスケールが最小ズームスケールより大きい場合にズームできます。ピンチ操作でコンテンツが拡大縮小され、拡大中は縦横両方向にスクロールできます。

PropRust型デフォルト説明
minimumZoomScale.zoom_range(min, max)number1最小のズームです。
maximumZoomScale.zoom_range(min, max)number1最大のズームです。最小より大きい場合、ピンチでズームできます。
zoomScale.zoom(scale)numberこの prop が変化したときに、このスケールへズームします。それ以外のときは、ユーザーが設定したズームが保持されます。
bouncesZoom.bounces_zoom(..)booleantrueピンチ操作で上限・下限を越えることができ、離すと元に戻ります。
zoomable.zoomable(..)booleantrueユーザーがピンチでズームできるかどうかを指定します。
centerContent.center_content(..)booleanfalseビューポートより小さいコンテンツ(縮小時)を中央に配置します。

スクロールイベントは、ズームスケールとズーム後のコンテンツサイズを報告します。コードからは、zoom_to(scale, center, animated) / zoomTo({ scale, x, y, animated }) を使うと、(ズームしていない状態のポイント単位の)コンテンツ上の点がビューポートの中央に来るようにズームできます。ダブルタップでズームする処理は、これを呼び出す press ハンドラーとして実装します。

pub fn photo() -> impl IntoElement {
    let photo = element_ref();
    let scale = signal(1.0_f32);
    scroll()
        .flex_1()
        .bg(black())
        .zoom_range(1.0, 4.0)
        .center_content(true)
        .on_scroll(move |e: &ScrollEvent| scale.set(e.zoom_scale))
        .element_ref(&photo)
        .child(
            image("https://picsum.photos/id/1015/1200/800")
                .w(px(390.0))
                .h(px(260.0))
                .on_press(move || photo.zoom_to(if scale.get_untracked() > 1.5 { 1.0 } else { 3.0 }, None, true)),
        )
}

ズームされたスクロールビュー内のテキストやキャンバスは、ズームの終了時に新しい解像度で再描画されるため、iOS では鮮明さが保たれます。Android では、NSH は独自のスクロールビューでズームを実装しています。ピンチの焦点を中心に拡大縮小し、任意の本数の指でパンでき、フリングや、端およびズーム限界でのラバーバンドにも対応します。Android では、システムのジェスチャー検出器と同様に、指の間隔が約 27 mm になった時点でピンチが始まります。

#コードからのスクロール

スクロールビューに ref を与え(Rust では element_ref() と .element_ref(&scroller)、TSX では ref)、そのメソッドを呼び出します。コマンドは同じ更新内のほかの変更が反映された後に実行されるため、追加したばかりのコンテンツへもスクロールできます。

RustTSX説明
scroll_to(point, animated)scrollTo({ x?, y?, animated? })指定したオフセットへスクロールします。TSX ではデフォルトでアニメーションします。
zoom_to(scale, center, animated)zoomTo({ scale, x?, y?, animated? })ズーム可能なスクロールビュー用です。scale へ(限界値の範囲内に収めて)、コンテンツ上の点またはビューポートの中央を基準にズームします。
let scroller = element_ref();

scroll().flex_1().element_ref(&scroller).child(content)
view().on_press(move || scroller.scroll_to(point(px(0.0), px(0.0)), true))

リストには scrollToIndex、scrollToOffset、scrollToEnd が追加されます。リストを参照してください。

#スクロールビューとキーボード

NSH は、フォーカスされた入力欄が見える状態を自動的に保ちます。キーボードが表示されると、入力欄を囲むスクロールビューにキーボードと同じ高さの下インセットが設定され、入力欄が見える位置までスクロールされます。この処理はネイティブ側で行われ、アプリ側のコードは不要です。画面下部に固定されたメッセージバーなど、スクロールビューの外にあるコンテンツには、env(keyboard-inset-height) を下パディングとして使ってください(スクロールビュー外のコンテンツ)。

スクロールやタップがキーボードとどう連動するかは、2 つの props で決まります。

PropRust型デフォルト説明
keyboardDismissMode.keyboard_dismiss_mode(KeyboardDismissMode::OnDrag)"none" | "on-drag" | "interactive""none"コンテンツのドラッグでキーボードを隠すかどうかを指定します。"interactive" は、iOS ではキーボードが指に追従して下がります。Android では "on-drag" と同じ動作になります。
keyboardShouldPersistTaps.keyboard_taps(KeyboardTaps::Handled)"never" | "always" | "handled""never"キーボードが表示されているときのタップの動作を指定します。

keyboardShouldPersistTaps:

  • "never": フォーカスされた入力欄の外をタップするとキーボードが隠れるだけで、ボタンは次のタップを受け取ります。ユーザーは、閉じるために 1 回、押すためにもう 1 回タップする必要があります。
  • "handled": タップを処理する要素(ボタンなど)へのタップはそのまま通り、キーボードは表示されたままです。何もない場所をタップするとキーボードが隠れます。フォームでは通常これが適切です。
  • "always": どのタップでもキーボードは表示されたままです。

どちらも、タップやドラッグが発生した時点でコードを待たずにネイティブ側で判断されます。コードからキーボードを隠すには、Rust では dismiss_keyboard() を、TSX では nsh の Keyboard.dismiss() を呼び出します。

#スクロールビューの入れ子

縦スクロールのビューの中に横スクロールのビューを置いても、期待どおりに動作します。それぞれが自分の軸方向のドラッグを受け取ります。同じ軸のスクロールビューも入れ子にできます(内側が先にスクロールします)。スクロールビュー内の描画キャンバスなど、指の操作を必要とするレスポンダーは、周囲のスクロールビューからその操作を奪えます。Rust では .keep_touches(true)、TSX では onResponderGrant から返す true を使います。タッチとジェスチャーを参照してください。

#関連項目

  • Scroll: エレメントリファレンス。すべての props とイベントを掲載しています。
  • リスト: 同じスクロール用 props を持つ仮想化リストです。
  • アニメーション: アニメーション値、およびスクロール連動エフェクト用の interpolate と diffClamp です。
  • フォームとキーボード: 入力欄、フォーカス、キーボードについて説明します。