تُرجمت هذه الصفحة آليًا وقد تحتوي على أخطاء. اقرأ الأصل الإنجليزي

التوثيق

التمرير

عرض التمرير (scroll() في Rust، و<Scroll> في TSX) هو عرض التمرير الأصلي للمنصة (UIScrollView على iOS، وScrollView على Android): الدفع السريع (fling) والارتداد ومؤشر التمرير والسحب للتحديث كلها أصلية، وكذلك الالتقاط (snapping). يصلك خبر التمرير عبر الأحداث، ويمكنك ربط الإزاحة بـقيمة متحركة لإنتاج تأثيرات تتبع الإصبع إطارًا بإطار.

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);

استخدم عرض التمرير للمحتوى ذي الحجم المعروف والمعتدل: صفحة إعدادات، أو نموذج، أو مقال، أو صف من الشرائح (chips). أما البيانات الطويلة أو غير المحدودة فاستخدم القائمة، فهي تعيد تدوير الصفوف وتقبل خصائص التمرير نفسها.

#عرض تمرير

عرض التمرير هو نافذة العرض (viewport)، وأبناؤه هم المحتوى. تحتاج نافذة العرض إلى حجم من العنصر الأب (عادةً 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()) وصفًّا كابن له:

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 + العجلة للتمرير أفقيًا)، ولوحة اللمس، ولوحة المفاتيح، ومؤشر التمرير الذي يظهر عند تحريك الماوس فوق العرض ويُسحب كشريط تمرير؛ أما سحب الماوس فيذهب إلى إيماءات السحب (pans) والمستجيبات (responders) وتحديد النص في التطبيق. وتسحب شاشات اللمس على أجهزة سطح المكتب عروض التمرير كما تفعل الأصابع. وتحتفظ العروض الأفقية بمؤشرها هناك حتى مع scrollbar={false}، لأنه الوسيلة التي يحرّكها بها ماوس بلا عجلة أفقية.

#خصائص السلوك

الخاصيةRustالنوعالافتراضيالوصف
horizontal.horizontal()booleanfalseيتمرر جانبيًا.
scrollable.scrollable(..)booleantrueيستطيع المستخدم تمريره. ويظل التمرير من الكود يعمل.
bounces.bounces(..)booleantrueالتمرير الزائد (overscroll) خلف الحواف.
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 هي القيمة الافتراضية للمنصة).

#التقليب والالتقاط

يُحسم الالتقاط أصليًا عند رفع الإصبع، اعتمادًا على إزاحة لحظة الرفع والانزلاق المتوقع والسرعة؛ ثم ينزلق عرض التمرير إلى نقطة التوقف المختارة دون أي تدخل من كودك.

الخاصيةRustالنوعالوصف
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لا يتجاوز الدفع السريع نقطة توقف واحدة.

مقلِّب صفحات (pager): صفحات بعرض عرض التمرير نفسه، ومؤشر يتحدث عند انتهاء الانزلاق.

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))
}

شريط بطاقات (carousel) تلتقط بطاقاته عند المنتصف، بطاقة واحدة لكل دفع سريع:

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))

تقبل القوائم خصائص الالتقاط نفسها، مما يجعلها الخيار الأفضل لشريط بطاقات طويل.

#أحداث التمرير

الخاصيةRustالحمولةمتى
onScroll.on_scroll(..)ScrollEventتغيرت الإزاحة. مرة واحدة كحد أقصى في كل إطار؛ ويباعد scrollEventThrottle بينها أكثر.
scrollEventThrottle.event_throttle(ms)numberالحد الأدنى بالميلي ثانية بين أحداث onScroll.
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). أما الأحداث الأخرى فتحمل الإزاحة: { offset } مع Point في TSX، وPoint<Points> في Rust.

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)))

تُقيَّد الاستيفاءات (interpolations) افتراضيًا خارج نطاق مدخلاتها، فعند السحب إلى ما بعد الأعلى (إزاحة سالبة) يبقى التأثير عند نهايته؛ مرّر extrapolate: "extend" (interpolate_with(.., Interpolation::extrapolate(Extrapolate::Extend)) في Rust) للحصول على ترويسة مطاطية تكبر كلما سحبت.

#السحب للتحديث

يضيف refreshing وonRefresh (.refreshing(..) و.on_refresh(..)) عنصر السحب للتحديث الخاص بالمنصة. يظهر العنصر ما دام refreshing صحيحًا؛ اضبطه في onRefresh وأعده إلى الحالة السابقة عند انتهاء العمل.

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 من Mac Catalyst يعرض NSH مؤشر دوران خاصًا به بالسلوك نفسه. تقبل القوائم الخصائص نفسها. على سطح المكتب وفي المتصفح يكون السحب إيماءة شاشة لمس (أو إيماءة لوحة تتبع macOS التي تسحب إلى ما بعد الأعلى)، لأن الماوس يمرر دون سحب؛ فوفّر هناك طريقة أخرى للتحديث، مثل زر أو اختصار لوحة مفاتيح.

#مؤشر خاص بك

شكل السحب متروك لاختيار التطبيق. يعني refreshIndicator={{ height }} أن التطبيق يرسم مؤشره الخاص، وheight ارتفاعه بالنقاط: يُخفى مؤشر الدوران الخاص بالمنصة، ويبدأ التحديث بمجرد أن يبلغ السحب height، ويُبقي التحديث height فوق المحتوى للمؤشر. يغذي pullValue قيمة متحركة بمقدار سحب المحتوى إلى ما بعد الأعلى: 0 في حالة السكون، وتزداد مع السحب، وheight أثناء التحديث. ارسم المؤشر فوق أعلى عرض التمرير واستوفِ أنماطه من القيمة: فهي تتبع الإصبع على خيط واجهة المستخدم (UI thread)، دون أي 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 مع السحب الذي يكتشفه NSH بنفسه، كما في نمط Mac.

#التكبير

يكبّر عرض التمرير (تحتفظ القوائم بمقياس 1) المحتوى عندما يكون أقصى مقياس تكبير له أعلى من أدناه: يؤدي القرص (pinch) إلى تحجيم المحتوى، ويتمرر في الاتجاهين أثناء التكبير.

الخاصيةRustالنوعالافتراضيالوصف
minimumZoomScale.zoom_range(min, max)number1أصغر تكبير.
maximumZoomScale.zoom_range(min, max)number1أكبر تكبير؛ وإذا كان أعلى من الأدنى فإن القرص يكبّر.
zoomScale.zoom(scale)numberيكبّر إلى هذا المقياس عند تغيّر الخاصية؛ ويُحتفظ بتكبير المستخدم فيما عدا ذلك.
bouncesZoom.bounces_zoom(..)booleantrueيمكن للقرص تجاوز الحدود ثم يرتد.
zoomable.zoomable(..)booleantrueيستطيع المستخدم القرص للتكبير.
centerContent.center_content(..)booleanfalseيُوسَّط المحتوى الأصغر من نافذة العرض (بعد التصغير).

يبلغ حدث التمرير عن مقياس التكبير وحجم المحتوى المكبَّر. ومن الكود، يكبّر zoom_to(scale, center, animated) / zoomTo({ scale, x, y, animated }) بحيث تقع نقطة المحتوى (بالنقاط غير المكبّرة) في منتصف نافذة العرض؛ والنقر المزدوج للتكبير هو معالج ضغط (press handler) يستدعيه:

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)),
        )
}

يُعاد رسم النصوص واللوحات (canvases) داخل عرض تمرير مكبَّر بالدقة الجديدة عند انتهاء التكبير، فتبقى حادة على iOS. أما على Android فيكبّر NSH باستخدام عرض تمرير خاص به، يحجّم حول نقطة تركيز القرص، ويمرر بأي عدد من الأصابع، ويدعم الدفع السريع والارتداد المطاطي عند الحواف وحدود التكبير. ويبدأ Android القرص حين تبعد الأصابع نحو 27 ملم عن بعضها، كما يفعل كاشف الإيماءات في النظام.

#التمرير من الكود

امنح عرض التمرير مرجعًا (element_ref() و.element_ref(&scroller) في Rust، وref في TSX) واستدعِ دوالّه. تعمل الأوامر بعد بقية التغييرات في التحديث نفسه، لذا يعمل التمرير إلى محتوى أضفته للتو.

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 الحقل المركَّز مرئيًا من تلقاء نفسه: عند ظهور لوحة المفاتيح، يحصل عرض التمرير المحيط بالحقل على حشوة سفلية (inset) بارتفاع لوحة المفاتيح ويمرر الحقل إلى حيز الرؤية. يحدث هذا أصليًا، دون أي كود في تطبيقك. استخدم env(keyboard-inset-height) كحشوة سفلية (المحتوى خارج عروض التمرير) للمحتوى الواقع خارج عروض التمرير، مثل شريط رسائل مثبت في الأسفل.

تحدد خاصيتان كيف يتفاعل التمرير والنقرات مع لوحة المفاتيح:

الخاصيةRustالنوعالافتراضيالوصف
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": النقرة خارج الحقل المركَّز تخفي لوحة المفاتيح فقط، وتصل النقرة التالية إلى الأزرار. ينقر المستخدم مرة للإخفاء ومرة أخرى للضغط.
  • "handled": النقرة على ما يعالجها (زر) تمر وتبقى لوحة المفاتيح؛ والنقرة على مساحة فارغة تخفيها. وهو الخيار الصحيح عادةً للنماذج.
  • "always": تبقى لوحة المفاتيح ظاهرة مع كل نقرة.

يُحسم الأمران أصليًا لحظة النقر أو السحب، دون انتظار كودك. لإخفاء لوحة المفاتيح من الكود، استدعِ dismiss_keyboard() في Rust أو Keyboard.dismiss() من nsh في TSX.

#عروض التمرير المتداخلة

يعمل عرض تمرير أفقي داخل عرض عمودي كما هو متوقع: يأخذ كل منهما السحبات على محوره. ويمكن أيضًا تداخل عرضي تمرير على المحور نفسه (يتمرر الداخلي أولًا). أما المستجيبات التي تحتاج إلى الإصبع، مثل لوحة رسم داخل عرض تمرير، فتمنعه عن عروض التمرير المحيطة بها: .keep_touches(true) في Rust، وtrue المُعاد من onResponderGrant في TSX؛ انظر اللمس والإيماءات.

#ذو صلة

  • Scroll: مرجع العنصر، بكل خصائصه وأحداثه.
  • القوائم: قوائم افتراضية (virtualized) بخصائص التمرير نفسها.
  • الحركة: القيم المتحركة، وinterpolate وdiffClamp للتأثيرات المرتبطة بالتمرير.
  • النماذج ولوحة المفاتيح: الحقول والتركيز ولوحة المفاتيح.