مفاتيح تعديل التمرير

يوفّر المعدِّلان verticalScroll و horizontalScroll أبسط طريقة للسماح للمستخدم بتمرير عنصر عندما تكون حدود محتوياته أكبر من قيود الحجم الأقصى. باستخدام المعدِّلين verticalScroll وhorizontalScroll، لن تحتاجوا إلى ترجمة المحتويات أو إزاحتها.

@Composable
private fun ScrollBoxes() {
    Column(
        modifier = Modifier
            .background(Color.LightGray)
            .size(100.dp)
            .verticalScroll(rememberScrollState())
    ) {
        repeat(10) {
            Text("Item $it", modifier = Modifier.padding(2.dp))
        }
    }
}

قائمة عمودية بسيطة تستجيب لإيماءات التمرير
الشكل 1. قائمة عمودية بسيطة تستجيب لإيماءات التمرير

تتيح لكم ScrollState تغيير موضع التمرير أو الحصول على حالته الحالية. لإنشائها باستخدام المَعلمات التلقائية، استخدِموا rememberScrollState().

@Composable
private fun ScrollBoxesSmooth() {
    // Smoothly scroll 100px on first composition
    val state = rememberScrollState()
    LaunchedEffect(Unit) { state.animateScrollTo(100) }

    Column(
        modifier = Modifier
            .background(Color.LightGray)
            .size(100.dp)
            .padding(horizontal = 8.dp)
            .verticalScroll(state)
    ) {
        repeat(10) {
            Text("Item $it", modifier = Modifier.padding(2.dp))
        }
    }
}

معدِّل المنطقة القابلة للتمرير

المعدِّل scrollableArea هو وحدة أساسية لإنشاء حاويات مخصّصة قابلة للتمرير. يوفّر تجريدًا أعلى مستوى من عنصر التعديل scrollable، ويتعامل مع المتطلبات الشائعة، مثل تفسير التغييرات في الإيماءات واقتصاص المحتوى وتأثيرات تجاوز حد التمرير.

في حين أنّ scrollableArea يُستخدَم في عمليات التنفيذ المخصّصة، ننصحكم عمومًا بتفضيل الحلول الجاهزة، مثل verticalScroll أو horizontalScroll أو العناصر القابلة للإنشاء، مثل LazyColumn لقوائم التمرير العادية. تكون هذه المكوّنات ذات المستوى الأعلى أبسط لحالات الاستخدام الشائعة، وقد تم إنشاؤها باستخدام scrollableArea.

الفرق بين المعدِّلين scrollableArea وscrollable

يكمن الاختلاف الرئيسي بين scrollableArea وscrollable في طريقة تفسير إيماءات التمرير التي يجريها المستخدم:

  • scrollable (دلتا أولية): تعكس الدلتا مباشرةً الحركة الفعلية لإدخال المستخدم (مثل سحب المؤشر) على الشاشة.
  • scrollableArea (التغيير الموجّه نحو المحتوى): يتم عكس delta دلاليًا لتمثيل التغيير المحدّد في موضع التمرير لجعل المحتوى يبدو وكأنّه يتحرّك مع إيماءة المستخدم، وهو عادةً عكس حركة المؤشر.

يمكنكم التفكير في الأمر على النحو التالي: scrollable يخبركم بكيفية تحرّك المؤشر، بينما scrollableArea يترجم حركة المؤشر هذه إلى كيفية تحرّك الـ محتوى داخل طريقة عرض عادية قابلة للتمرير. هذا العكس هو السبب في أنّ scrollableArea يبدو أكثر طبيعية عند تنفيذ حاوية عادية قابلة للتمرير.

يلخّص الجدول التالي علامات التغيير في السيناريوهات الشائعة:

إيماءة المستخدم

التغيير الذي يتم إرساله إلى dispatchRawDelta بواسطة scrollable

التغيير الذي يتم إرساله إلى dispatchRawDelta بواسطة scrollableArea*

تحرّك المؤشر للأعلى

سالب

موجب

تحرّك المؤشر للأسفل

موجب

سالب

تحرّك المؤشر لليسار

سالب

موجب (سالب للغة العربية)

تحرّك المؤشر لليمين

موجب

سالب (موجب للغة العربية)

(*) ملاحظة حول علامة التغيير في scrollableArea: ليست علامة التغيير من scrollableArea مجرد عكس بسيط. فهي تأخذ في الاعتبار بذكاء ما يلي:

  1. الاتجاه: عمودي أو أفقي
  2. LayoutDirection: من اليمين إلى اليسار أو من اليسار إلى اليمين (مهم بشكل خاص للتمرير الأفقي)
  3. العلامة reverseScrolling: ما إذا كان اتجاه التمرير معكوسًا

بالإضافة إلى عكس التغيير في التمرير، يقتصّ scrollableArea أيضًا المحتوى ضمن حدود التنسيق ويتعامل مع عرض تأثيرات تجاوز حد التمرير. تلقائيًا، يستخدم التأثير الذي يوفّره LocalOverscrollFactory. يمكنكم تخصيص هذا التأثير أو إيقافه باستخدام التحميل الزائد لـ scrollableArea الذي يقبل مَعلمة OverscrollEffect.

متى يجب استخدام المعدِّل scrollableArea؟

يجب استخدام المعدِّل scrollableArea عندما تحتاجون إلى إنشاء مكوّن تمرير مخصّص لا يلبّي احتياجاتكم بشكل كافٍ من خلال المعدِّلين horizontalScroll أو verticalScroll أو تنسيقات Lazy. غالبًا ما يتضمّن ذلك الحالات التالية:

  • منطق التنسيق المخصّص: عندما يتغيّر ترتيب العناصر ديناميكيًا استنادًا إلى موضع التمرير
  • التأثيرات المرئية الفريدة: تطبيق عمليات التحويل أو تغيير الحجم أو التأثيرات الأخرى على العناصر الثانوية أثناء تمريرها
  • التحكّم المباشر: الحاجة إلى تحكّم دقيق في آليات التمرير بما يتجاوز ما يعرضه verticalScroll أو تنسيقات Lazy

إنشاء قوائم مخصّصة تشبه العجلة باستخدام scrollableArea

يعرض المثال التالي استخدام scrollableArea لإنشاء قائمة عمودية مخصّصة يتم فيها تصغير حجم العناصر كلما ابتعدت عن المنتصف، ما يؤدي إلى إنشاء تأثير مرئي "يشبه العجلة". هذا النوع من عمليات التحويل المستندة إلى التمرير هو حالة استخدام مثالية لـ scrollableArea.

الشكل 2. قائمة عمودية مخصّصة باستخدام scrollableArea

@Composable
private fun ScrollableAreaSample() {
    // ...
    Layout(
        modifier =
            Modifier
                .size(150.dp)
                .scrollableArea(scrollState, Orientation.Vertical)
                .background(Color.LightGray),
        // ...
    ) { measurables, constraints ->
        // ...
        // Update the maximum scroll value to not scroll beyond limits and stop when scroll
        // reaches the end.
        scrollState.maxValue = (totalHeight - viewportHeight).coerceAtLeast(0)

        // Position the children within the layout.
        layout(constraints.maxWidth, viewportHeight) {
            // The current vertical scroll position, in pixels.
            val scrollY = scrollState.value
            val viewportCenterY = scrollY + viewportHeight / 2

            var placeableLayoutPositionY = 0
            placeables.forEach { placeable ->
                // This sample applies a scaling effect to items based on their distance
                // from the center, creating a wheel-like effect.
                // ...
                // Place the item horizontally centered with a layer transformation for
                // scaling to achieve wheel-like effect.
                placeable.placeRelativeWithLayer(
                    x = constraints.maxWidth / 2 - placeable.width / 2,
                    // Offset y by the scroll position to make placeable visible in the viewport.
                    y = placeableLayoutPositionY - scrollY,
                ) {
                    scaleX = scaleFactor
                    scaleY = scaleFactor
                }
                // Move to the next item's vertical position.
                placeableLayoutPositionY += placeable.height
            }
        }
    }
}
// ...

معدِّل قابل للتمرير

يختلف المعدِّل scrollable عن معدِّلات التمرير الأخرى في أنّه scrollable يرصد إيماءات التمرير ويسجّل التغييرات، ولكنّه لا يزيح محتوياته تلقائيًا. بدلاً من ذلك، يتم تفويض هذه المهمة إلى المستخدم من خلال ScrollableState ، وهو مطلوب لكي يعمل هذا المعدِّل بشكل صحيح.

عند إنشاء ScrollableState، يجب توفير دالة consumeScrollDelta التي سيتم استدعاؤها في كل خطوة تمرير (من خلال إدخال الإيماءة أو التمرير السلس أو التحريك السريع) مع التغيير بالبكسل. يجب أن تعرض هذه الدالة مقدار مسافة التمرير المستهلكة، لضمان نشر الحدث بشكل صحيح في الحالات التي تتضمّن عناصر متداخلة تحتوي على المعدِّل scrollable.

يرصد الرمز التالي الإيماءات ويعرض قيمة رقمية للإزاحة، ولكنّه لا يزيح أي عناصر:

@Composable
private fun ScrollableSample() {
    // actual composable state
    var offset by remember { mutableFloatStateOf(0f) }
    Box(
        Modifier
            .size(150.dp)
            .scrollable(
                orientation = Orientation.Vertical,
                // Scrollable state: describes how to consume
                // scrolling delta and update offset
                state = rememberScrollableState { delta ->
                    offset += delta
                    delta
                }
            )
            .background(Color.LightGray),
        contentAlignment = Alignment.Center
    ) {
        Text(offset.toString())
    }
}

عنصر في واجهة المستخدم يرصد ضغطة الإصبع ويعرض القيمة الرقمية لموضع الإصبع
الشكل 3. عنصر في واجهة المستخدم يرصد ضغطة الإصبع ويعرض القيمة الرقمية لموضع الإصبع