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

العناصر

Scroll

Scroll هو عرض تمرير أصلي (UIScrollView على iOS وMac Catalyst، وScrollView على Android). يعرض جميع عناصره الفرعية بوصفها محتواه ويمرّرها أصليًا: التمرير والقذف (fling) والارتداد والالتصاق تعمل على المنصة نفسها، وبمعدل تحديث الشاشة. في 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),
    )),
)

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

يحتاج عرض التمرير إلى حجم محدود على محوره: امنحه flex: 1 (.flex_1()) أو ارتفاعًا داخل حاوية لها ارتفاع محدد. يُعرض محتواه بعرض عرض التمرير (وبارتفاعه عند التمرير الأفقي) وبالطول (العرض) الذي يحتاجه.

#الخصائص

إضافةً إلى الخصائص المشتركة (مع كتابة style بوصفها ViewStyle). في Rust، خصائص التمرير هي دوال في السمة Scrollable التي تشترك فيها القوائم.

#التمرير

الخاصيةRustالنوعالقيمة الافتراضيةالوصف
horizontal.horizontal()booleanfalseيمرّر أفقيًا.
scrollable.scrollable(..)booleantrueيمكن للمستخدم تمريره.
scrollbar.scrollbar(..)boolean | ScrollbarThemetrueيظهر مؤشر التمرير: مؤشر المنصة، أو الشريط المدمج في التطبيق متى عيّن التطبيق سمة؛ والكائن هو سمة خاصة بالعرض نفسه. على سطح المكتب وفي متصفحات سطح المكتب يحتفظ العرض الأفقي به: فهو شريط تمرير الفأرة.
bounces.bounces(..)booleantrueالتمرير الزائد خلف الحواف.
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(..)booleanfalseلا يتجاوز القذف نقطة التصاق واحدة.
decelerationRate.deceleration(rate)"normal" | "fast" | numberالمنصةسرعة تباطؤ القذف: السرعة المحتفظ بها لكل ميلي ثانية (normal 0.998، fast 0.99).
contentInset.content_inset(EdgeInsets { .. })Partial<Edges>مسافة حول المحتوى.
contentOffset.content_offset(point(..)){ x?, y? }موضع البداية.
contentStyleأنماط عرض المحتوى نفسهViewStyleنمط عرض يحيط بالعناصر الفرعية ويتمرر معها؛ flexGrow: 1 يجعله بارتفاع منطقة العرض على الأقل (حالة فارغة متمركزة).
contentClassأنماط عرض المحتوى نفسهstringمثل 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 (حدث واحد على الأكثر لكل إطار على أي حال).

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

الخاصيةRustالنوعالوصف
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())

#التكبير

عند تعيين maximumZoomScale إلى قيمة أكبر من minimumZoomScale (.zoom_range(min, max) في Rust) يصبح المحتوى قابلًا للتكبير بالقرص (pinch)؛ وعند التكبير يتمرر في الاتجاهين. القوائم تحتفظ بمقياسها.

الخاصيةRustالنوعالقيمة الافتراضيةالوصف
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)),
    )

#الأحداث

الحدثRustالبياناتمتى يقع
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 ما لم يكن العرض يكبّر).

#دوال المرجع

الدالةRustالوصف
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);

#لوحة المفاتيح

تُبقى حقول الإدخال داخل عرض التمرير بعيدة عن لوحة المفاتيح أصليًا: يحصل عرض التمرير على إزاحة سفلية ويمرّر إلى حقل الإدخال المركَّز من تلقاء نفسه. أما للمحتوى خارج عروض التمرير فاستخدم env(keyboard-inset-height) في حشوة أو هامش سفلي. راجع النماذج ولوحة المفاتيح.

#من اليمين إلى اليسار

تبدأ عروض التمرير الأفقية من الطرف الأيمن في تخطيطات اليمين إلى اليسار وتحافظ على بعدها عنه عند تغيّر المحتوى؛ وتنعكس عناصرها الفرعية.

#ملاحظات المنصات

  • Android: يرسم NSH السحب للتحديث (ويبقى التمرير الزائد أصليًا)، ويُطبَّق contentInset بوصفه حشوة، وdecelerationRate يشكّل انزلاقات الالتصاق.
  • يكبّر Android باستخدام عرض التمرير الخاص بـ NSH، الذي يقيس المحتوى حول زاويته العلوية اليسرى ويتمرر في الاتجاهين. يبدأ القرص بعد الحد الأدنى للمسافة بين الإصبعين في Android (نحو 27 مم).
  • على Mac Catalyst (نمط Mac)، يكتشف NSH السحب ويعرض مؤشره الدوار.
  • يعيد iOS رسم النصوص واللوحات (canvases) بدقة التكبير عند انتهائه.