صفحة مواضع التمرير

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

يوفّر المعدّلان 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))
        }
    }
}

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

تتيح لك السمة 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))
        }
    }
}

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

يختلف المعدِّل 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())
    }
}

أحد عناصر واجهة المستخدم الذي يرصد ضغط الإصبع ويعرض القيمة العددية لموضع الإصبع

التمرير المتداخل

التمرير المتداخل هو نظام تعمل فيه مكوّنات تمرير متعدّدة مضمّنة في بعضها البعض من خلال الاستجابة لإيماءة تمرير واحدة وإبلاغ التغييرات في التمرير.

يتيح نظام التمرير المتداخل التنسيق بين المكوّنات القابلة للتمرير والمرتبطة بشكل هرمي (غالبًا من خلال مشاركة الأصل نفسه). يربط هذا النظام الحاويات التي يمكن التمرير فيها ويسمح بالتفاعل مع التغيّرات في التمرير التي يتم نشرها ومشاركتها بين الحاويات.

توفّر Compose طرقًا متعددة للتعامل مع التمرير المتداخل بين العناصر القابلة للإنشاء. من الأمثلة الشائعة على التمرير المتداخل قائمة داخل قائمة أخرى، ومن الحالات الأكثر تعقيدًا شريط أدوات قابل للطي.

التمرير المتداخل التلقائي

لا يتطلّب التمرير المتداخل البسيط أيّ إجراء من جانبك. يتم تلقائيًا نقل الإيماءات التي تبدأ إجراء التمرير من العناصر الفرعية إلى العناصر الرئيسية، وذلك على النحو التالي: عندما يتعذّر على العنصر الفرعي التمرير أكثر من ذلك، يتولّى العنصر الرئيسي معالجة الإيماءة.

تتوفّر ميزة التمرير المتداخل التلقائي وتوفّرها بعض مكوّنات ومعدِّلات Compose الجاهزة للاستخدام: verticalScroll وhorizontalScroll وscrollable وواجهات برمجة التطبيقات Lazy وTextField. هذا يعني أنّه عندما يمرّر المستخدم عنصرًا ثانويًا داخليًا من عناصر متداخلة، تنقل المعدِّلات السابقة فروق التمرير إلى العناصر الرئيسية التي تتوافق مع التمرير المتداخل.

يعرض المثال التالي عناصر تم تطبيق المعدِّل verticalScroll عليها داخل حاوية تم تطبيق المعدِّل verticalScroll عليها أيضًا.

@Composable
private fun AutomaticNestedScroll() {
    val gradient = Brush.verticalGradient(0f to Color.Gray, 1000f to Color.White)
    Box(
        modifier = Modifier
            .background(Color.LightGray)
            .verticalScroll(rememberScrollState())
            .padding(32.dp)
    ) {
        Column {
            repeat(6) {
                Box(
                    modifier = Modifier
                        .height(128.dp)
                        .verticalScroll(rememberScrollState())
                ) {
                    Text(
                        "Scroll here",
                        modifier = Modifier
                            .border(12.dp, Color.DarkGray)
                            .background(brush = gradient)
                            .padding(24.dp)
                            .height(150.dp)
                    )
                }
            }
        }
    }
}

عنصران متداخلان في واجهة المستخدم يمكن تمريرهما عموديًا، ويستجيبان للإيماءات داخل العنصر الداخلي وخارجه

استخدام المعدِّل nestedScroll

إذا كنت بحاجة إلى إنشاء تمرير متقدّم ومنسّق بين عناصر متعدّدة، يمنحك المعدِّل nestedScroll المزيد من المرونة من خلال تحديد تسلسل هرمي للتمرير المتداخل. كما ذكرنا في القسم السابق، تتوافق بعض المكوّنات مع ميزة التمرير المتداخل المضمّنة. ومع ذلك، بالنسبة إلى العناصر القابلة للإنشاء التي لا يمكن التمرير فيها تلقائيًا، مثل Box أو Column، لن يتم نشر دلتا التمرير في هذه المكوّنات في نظام التمرير المتداخل، ولن تصل الدلتا إلى NestedScrollConnection أو المكوّن الرئيسي. لحلّ هذه المشكلة، يمكنك استخدام nestedScroll لمنح هذه الميزة للمكوّنات الأخرى، بما في ذلك المكوّنات المخصّصة.

دورة التمرير المتداخل

دورة التمرير المتداخل هي تدفّق دلتا التمرير التي يتم إرسالها للأعلى وللأسفل في شجرة التسلسل الهرمي من خلال جميع المكوّنات (أو العُقد) التي تشكّل جزءًا من نظام التمرير المتداخل، مثلاً باستخدام المكوّنات والمعدِّلات القابلة للتمرير أو nestedScroll.

مراحل دورة التمرير المتداخل

عندما يرصد أحد المكوّنات القابلة للتمرير حدثًا مشغّلاً (مثل إيماءة)، يتم إرسال الفروق التي تم إنشاؤها إلى نظام التمرير المتداخل قبل حتى أن يتم تشغيل إجراء التمرير الفعلي، وتمر هذه الفروق بثلاث مراحل: التمرير المُسبَق، واستهلاك العُقدة، والتمرير اللاحق.

مراحل دورة التمرير المتداخل

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

مرحلة ما قبل التمرير سريعًا - إرسال
up

يمنح ذلك عناصر التمرير المتداخل الرئيسية (عناصر قابلة للإنشاء تستخدم nestedScroll أو معدّلات قابلة للتمرير) فرصة للتعامل مع التغيير قبل أن تتمكّن العقدة نفسها من استخدامه.

مرحلة ما قبل التمرير سريعًا - التمرير سريعًا للأسفل

في مرحلة استهلاك العقدة، ستستخدم العقدة نفسها أي فرق لم تستخدمه العقد الأصلية. وهي الفترة التي يتم فيها تنفيذ حركة التمرير وتكون مرئية.

مرحلة استهلاك العُقدة

خلال هذه المرحلة، يمكن للطفل اختيار استهلاك كل أو جزء من المحتوى المتبقي. سيتم إرسال أي بيانات متبقية مرة أخرى لتمريرها بمرحلة ما بعد التمرير.

أخيرًا، في مرحلة ما بعد التمرير، سيتم إرسال أي شيء لم يستهلكه العنصر نفسه إلى العناصر الرئيسية مرة أخرى لاستهلاكه.

مرحلة ما بعد الانتقال للأسفل - إرسال
للأعلى

تعمل مرحلة ما بعد التمرير للأعلى أو للأسفل بطريقة مشابهة لمرحلة ما قبل التمرير للأعلى أو للأسفل، حيث يمكن لأي من العناصر الرئيسية اختيار عرض الإعلان أو عدم عرضه.

مرحلة ما بعد الانتقال للأسفل - الفقاعات
تتلاشى

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

المشاركة في دورة التمرير المتداخل

وتعني المشاركة في الدورة اعتراض استهلاك الفروق والإبلاغ عنه على طول التسلسل الهرمي. توفّر Compose مجموعة من الأدوات للتأثير في طريقة عمل نظام التمرير المتداخل وكيفية التفاعل معه مباشرةً، مثلاً عندما تحتاج إلى إجراء عملية ما على دلتا التمرير قبل أن يبدأ مكوّن قابل للتمرير في التمرير.

إذا كانت دورة التمرير المتداخلة عبارة عن نظام يعمل على سلسلة من العُقد، فإنّ المعدِّل nestedScroll هو طريقة لاعتراض هذه التغييرات وإدراجها فيها، والتأثير في البيانات (فروق التمرير) التي يتم نشرها في السلسلة. يمكن وضع هذا المعدِّل في أي مكان في التسلسل الهرمي، ويتواصل مع مثيلات معدِّل التمرير المتداخل في أعلى الشجرة حتى يتمكّن من مشاركة المعلومات من خلال هذه القناة. إنّ العناصر الأساسية لهذا المعدِّل هي NestedScrollConnection وNestedScrollDispatcher.

توفّر NestedScrollConnection طريقة للرد على مراحل دورة التمرير المتداخل والتأثير في نظام التمرير المتداخل. يتألف من أربع طرق لمعالجة البيانات، يمثّل كل منها إحدى مراحل الاستهلاك: ما قبل/بعد التمرير وما قبل/بعد التحريك السريع:

val nestedScrollConnection = object : NestedScrollConnection {
    override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
        println("Received onPreScroll callback.")
        return Offset.Zero
    }

    override fun onPostScroll(
        consumed: Offset,
        available: Offset,
        source: NestedScrollSource
    ): Offset {
        println("Received onPostScroll callback.")
        return Offset.Zero
    }
}

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

val disabledNestedScrollConnection = remember {
    object : NestedScrollConnection {
        override fun onPostScroll(
            consumed: Offset,
            available: Offset,
            source: NestedScrollSource
        ): Offset {
            return if (source == NestedScrollSource.SideEffect) {
                available
            } else {
                Offset.Zero
            }
        }
    }
}

تقدّم جميع عمليات معاودة الاتصال معلومات عن NestedScrollSource النوع.

تعمل الدالة NestedScrollDispatcher على تهيئة دورة التمرير المتداخل. يؤدي استخدام أداة إرسال واستدعاء طرقها إلى بدء الدورة. تحتوي الحاويات القابلة للتمرير على أداة إرسال مدمجة ترسل التغييرات التي تم رصدها أثناء الإيماءات إلى النظام. لهذا السبب، تتضمّن معظم حالات استخدام تخصيص التمرير المتداخل استخدام NestedScrollConnection بدلاً من أداة إرسال، وذلك للتفاعل مع الفروق الحالية بدلاً من إرسال فروق جديدة. يمكنك الاطّلاع على NestedScrollDispatcherSample لمزيد من حالات الاستخدام.

تغيير حجم صورة عند التمرير

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

تغيير حجم صورة استنادًا إلى موضع التمرير

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

@Composable
fun ImageResizeOnScrollExample(
    modifier: Modifier = Modifier,
    maxImageSize: Dp = 300.dp,
    minImageSize: Dp = 100.dp
) {
    var currentImageSize by remember { mutableStateOf(maxImageSize) }
    var imageScale by remember { mutableFloatStateOf(1f) }

    val nestedScrollConnection = remember {
        object : NestedScrollConnection {
            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
                // Calculate the change in image size based on scroll delta
                val delta = available.y
                val newImageSize = currentImageSize + delta.dp
                val previousImageSize = currentImageSize

                // Constrain the image size within the allowed bounds
                currentImageSize = newImageSize.coerceIn(minImageSize, maxImageSize)
                val consumed = currentImageSize - previousImageSize

                // Calculate the scale for the image
                imageScale = currentImageSize / maxImageSize

                // Return the consumed scroll amount
                return Offset(0f, consumed.value)
            }
        }
    }

    Box(Modifier.nestedScroll(nestedScrollConnection)) {
        LazyColumn(
            Modifier
                .fillMaxWidth()
                .padding(15.dp)
                .offset {
                    IntOffset(0, currentImageSize.roundToPx())
                }
        ) {
            // Placeholder list items
            items(100, key = { it }) {
                Text(
                    text = "Item: $it",
                    style = MaterialTheme.typography.bodyLarge
                )
            }
        }

        Image(
            painter = ColorPainter(Color.Red),
            contentDescription = "Red color image",
            Modifier
                .size(maxImageSize)
                .align(Alignment.TopCenter)
                .graphicsLayer {
                    scaleX = imageScale
                    scaleY = imageScale
                    // Center the image vertically as it scales
                    translationY = -(maxImageSize.toPx() - currentImageSize.toPx()) / 2f
                }
        )
    }
}

النقاط الرئيسية حول الرمز

  • يستخدِم هذا الرمز NestedScrollConnection لاعتراض أحداث التمرير.
  • تحسب الدالة onPreScroll التغيير في حجم الصورة استنادًا إلى الفرق في مقدار التمرير.
  • يخزّن متغير الحالة currentImageSize الحجم الحالي للصورة، ويتم تحديد قيمته بين minImageSize وmaxImageSize. imageScale استنادًا إلى currentImageSize.
  • تتم تسوية LazyColumn استنادًا إلى currentImageSize.
  • يستخدم Image المعدِّل graphicsLayer لتطبيق المقياس المحسوب.
  • يضمن الرمز translationY داخل graphicsLayer بقاء الصورة في المنتصف عموديًا أثناء تغيير حجمها.

النتيجة

يؤدي المقتطف السابق إلى ظهور تأثير تغيير حجم الصورة عند التمرير:

الشكل 1. تأثير تغيير حجم الصورة عند التمرير

التوافق مع التمرير المتداخل

عند محاولة تضمين عناصر View قابلة للتمرير في عناصر قابلة للتجميع قابلة للتمرير، أو العكس، قد تواجه مشاكل. تحدث أبرزها عند التمرير سريعًا للأسفل أو للأعلى في العنصر الفرعي والوصول إلى حدود بدايته أو نهايته، وعندما تتوقّع أن يتولّى العنصر الرئيسي عملية التمرير السريع. ومع ذلك، قد لا يحدث هذا السلوك المتوقّع أو قد لا يعمل على النحو المتوقّع.

هذه المشكلة ناتجة عن التوقعات المضمّنة في العناصر القابلة للإنشاء والقابلة للتمرير. تتضمّن العناصر القابلة للإنشاء والقابلة للتمرير قاعدة "nested-scroll-by-default"، ما يعني أنّه يجب أن تشارك أي حاوية قابلة للتمرير في سلسلة التمرير المتداخل، سواء كعنصر رئيسي من خلال NestedScrollConnection أو كعنصر ثانوي من خلال NestedScrollDispatcher. بعد ذلك، سيؤدي العنصر الفرعي إلى تفعيل التمرير المتداخل للعنصر الرئيسي عندما يكون العنصر الفرعي عند الحد. على سبيل المثال، تسمح هذه القاعدة لـ Compose Pager وCompose LazyRow بالعمل معًا بشكل جيد. ومع ذلك، عند إجراء التمرير المتوافق مع أنظمة التشغيل الأخرى باستخدام ViewPager2 أو RecyclerView، لا يمكن إجراء التمرير المستمر من العنصر الثانوي إلى العنصر الرئيسي لأنّ هذين العنصرَين لا ينفّذان NestedScrollingParent3.

لتفعيل واجهة برمجة تطبيقات التوافق مع التمرير المتداخل بين عناصر View القابلة للتمرير وعناصر قابلة للإنشاء قابلة للتمرير، والمتداخلة في كلا الاتجاهين، يمكنك استخدام واجهة برمجة تطبيقات التوافق مع التمرير المتداخل للحد من هذه المشاكل في السيناريوهات التالية.

عنصر أبوي متعاون View يحتوي على عنصر فرعي ComposeView

الوالد المتعاون View هو الوالد الذي يطبّق NestedScrollingParent3 وبالتالي يمكنه تلقّي دلتا التمرير من عنصر فرعي متداخل متعاون قابل للإنشاء. سيعمل ComposeView كعنصر فرعي في هذه الحالة وسيحتاج إلى تنفيذ NestedScrollingChild3 (بشكل غير مباشر). أحد الأمثلة على الوالد المتعاون هو androidx.coordinatorlayout.widget.CoordinatorLayout.

إذا كنت بحاجة إلى إمكانية التشغيل التفاعلي للتمرير المتداخل بين الحاويات الرئيسية View القابلة للتمرير والعناصر الثانوية المركّبة القابلة للتمرير والمتداخلة، يمكنك استخدام rememberNestedScrollInteropConnection().

تسمح السمة rememberNestedScrollInteropConnection() وتتذكّر NestedScrollConnection التي تتيح إمكانية التشغيل التفاعلي للتمرير المتداخل بين عنصر View رئيسي ينفّذ NestedScrollingParent3 وعنصر ثانوي في Compose. يجب استخدام هذا المعدِّل مع معدِّل nestedScroll. بما أنّ ميزة "التمرير المتداخل" مفعَّلة تلقائيًا في Compose، يمكنك استخدام هذا الاتصال لتفعيل ميزة "التمرير المتداخل" في View وإضافة منطق الربط اللازم بين Views والعناصر القابلة للإنشاء.

من حالات الاستخدام الشائعة استخدام CoordinatorLayout وCollapsingToolbarLayout وعنصر قابل للإنشاء ثانوي، كما هو موضّح في المثال التالي:

<androidx.coordinatorlayout.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.appbar.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:fitsSystemWindows="true">

        <com.google.android.material.appbar.CollapsingToolbarLayout
            android:id="@+id/collapsing_toolbar_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:fitsSystemWindows="true"
            app:layout_scrollFlags="scroll|exitUntilCollapsed">

            <!--...-->

        </com.google.android.material.appbar.CollapsingToolbarLayout>

    </com.google.android.material.appbar.AppBarLayout>

    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

في النشاط أو الجزء، عليك إعداد العنصر القابل للإنشاء التابع وNestedScrollConnection المطلوب:

open class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        findViewById<ComposeView>(R.id.compose_view).apply {
            setContent {
                val nestedScrollInterop = rememberNestedScrollInteropConnection()
                // Add the nested scroll connection to your top level @Composable element
                // using the nestedScroll modifier.
                LazyColumn(modifier = Modifier.nestedScroll(nestedScrollInterop)) {
                    items(20) { item ->
                        Box(
                            modifier = Modifier
                                .padding(16.dp)
                                .height(56.dp)
                                .fillMaxWidth()
                                .background(Color.Gray),
                            contentAlignment = Alignment.Center
                        ) {
                            Text(item.toString())
                        }
                    }
                }
            }
        }
    }
}

عنصر قابل للإنشاء رئيسي يحتوي على عنصر ثانوي AndroidView

يغطّي هذا السيناريو تنفيذ واجهة برمجة التطبيقات الخاصة بالتوافق مع التمرير المتداخل على جانب Compose، أي عندما يكون لديك عنصر قابل للإنشاء رئيسي يحتوي على عنصر ثانوي AndroidView. تنفِّذ السمة AndroidView NestedScrollDispatcher، لأنّها تعمل كعنصر فرعي في عنصر رئيسي قابل للتمرير في Compose، بالإضافة إلى NestedScrollingParent3 ، لأنّها تعمل كعنصر رئيسي في عنصر فرعي قابل للتمرير View. سيتمكّن العنصر الرئيسي الذي يتضمّن العنصر الفرعي من تلقّي دلتا التمرير المدمجة من العنصر الفرعي القابل للتمرير View.

يوضّح المثال التالي كيفية تحقيق إمكانية التشغيل التفاعلي للتمرير المتداخل في هذا السيناريو، بالإضافة إلى شريط أدوات قابل للتصغير في Compose:

@Composable
private fun NestedScrollInteropComposeParentWithAndroidChildExample() {
    val toolbarHeightPx = with(LocalDensity.current) { ToolbarHeight.roundToPx().toFloat() }
    val toolbarOffsetHeightPx = remember { mutableStateOf(0f) }

    // Sets up the nested scroll connection between the Box composable parent
    // and the child AndroidView containing the RecyclerView
    val nestedScrollConnection = remember {
        object : NestedScrollConnection {
            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
                // Updates the toolbar offset based on the scroll to enable
                // collapsible behaviour
                val delta = available.y
                val newOffset = toolbarOffsetHeightPx.value + delta
                toolbarOffsetHeightPx.value = newOffset.coerceIn(-toolbarHeightPx, 0f)
                return Offset.Zero
            }
        }
    }

    Box(
        Modifier
            .fillMaxSize()
            .nestedScroll(nestedScrollConnection)
    ) {
        TopAppBar(
            modifier = Modifier
                .height(ToolbarHeight)
                .offset { IntOffset(x = 0, y = toolbarOffsetHeightPx.value.roundToInt()) }
        )

        AndroidView(
            { context ->
                LayoutInflater.from(context)
                    .inflate(R.layout.view_in_compose_nested_scroll_interop, null).apply {
                        with(findViewById<RecyclerView>(R.id.main_list)) {
                            layoutManager = LinearLayoutManager(context, VERTICAL, false)
                            adapter = NestedScrollInteropAdapter()
                        }
                    }.also {
                        // Nested scrolling interop is enabled when
                        // nested scroll is enabled for the root View
                        ViewCompat.setNestedScrollingEnabled(it, true)
                    }
            },
            // ...
        )
    }
}

private class NestedScrollInteropAdapter :
    Adapter<NestedScrollInteropAdapter.NestedScrollInteropViewHolder>() {
    val items = (1..10).map { it.toString() }

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int
    ): NestedScrollInteropViewHolder {
        return NestedScrollInteropViewHolder(
            LayoutInflater.from(parent.context)
                .inflate(R.layout.list_item, parent, false)
        )
    }

    override fun onBindViewHolder(holder: NestedScrollInteropViewHolder, position: Int) {
        // ...
    }

    class NestedScrollInteropViewHolder(view: View) : ViewHolder(view) {
        fun bind(item: String) {
            // ...
        }
    }
    // ...
}

يوضّح هذا المثال كيف يمكنك استخدام واجهة برمجة التطبيقات مع المعدِّل scrollable:

@Composable
fun ViewInComposeNestedScrollInteropExample() {
    Box(
        Modifier
            .fillMaxSize()
            .scrollable(rememberScrollableState {
                // View component deltas should be reflected in Compose
                // components that participate in nested scrolling
                it
            }, Orientation.Vertical)
    ) {
        AndroidView(
            { context ->
                LayoutInflater.from(context)
                    .inflate(android.R.layout.list_item, null)
                    .apply {
                        // Nested scrolling interop is enabled when
                        // nested scroll is enabled for the root View
                        ViewCompat.setNestedScrollingEnabled(this, true)
                    }
            }
        )
    }
}

وأخيرًا، يوضّح هذا المثال كيفية استخدام واجهة برمجة التطبيقات المتوافقة مع التمرير المتداخل مع BottomSheetDialogFragment لتحقيق سلوك ناجح للسحب والإغلاق:

class BottomSheetFragment : BottomSheetDialogFragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        val rootView: View = inflater.inflate(R.layout.fragment_bottom_sheet, container, false)

        rootView.findViewById<ComposeView>(R.id.compose_view).apply {
            setContent {
                val nestedScrollInterop = rememberNestedScrollInteropConnection()
                LazyColumn(
                    Modifier
                        .nestedScroll(nestedScrollInterop)
                        .fillMaxSize()
                ) {
                    item {
                        Text(text = "Bottom sheet title")
                    }
                    items(10) {
                        Text(
                            text = "List item number $it",
                            modifier = Modifier.fillMaxWidth()
                        )
                    }
                }
            }
            return rootView
        }
    }
}

يُرجى العِلم أنّ rememberNestedScrollInteropConnection() سيؤدي إلى تثبيت NestedScrollConnection في العنصر الذي يتم إرفاقه به. NestedScrollConnection هو المسؤول عن نقل التغييرات من مستوى Compose إلى مستوى View. يتيح ذلك مشاركة العنصر في التنقّل المتداخل، ولكنّه لا يتيح التنقّل في العناصر تلقائيًا. بالنسبة إلى العناصر القابلة للإنشاء غير القابلة للتمرير تلقائيًا، مثل Box أو Column، لن يتم نقل التغييرات في التمرير على هذه العناصر إلى نظام التمرير المتداخل، ولن تصل التغييرات إلى NestedScrollConnection التي توفّرها rememberNestedScrollInteropConnection()، وبالتالي لن تصل هذه التغييرات إلى العنصر الرئيسي View. لحلّ هذه المشكلة، تأكَّد من ضبط أدوات التعديل القابلة للتمرير على هذه الأنواع من العناصر القابلة للإنشاء المتداخلة. يمكنك الرجوع إلى القسم السابق حول التمرير المتداخل للحصول على معلومات أكثر تفصيلاً.

أحد الوالدَين غير المتعاونَين View الذي يحتوي على طفل ComposeView

طريقة العرض غير المتوافقة هي تلك التي لا تنفّذ واجهات NestedScrolling الضرورية على جهة View. يُرجى العِلم أنّ هذا يعني أنّ التوافق مع التمرير المتداخل مع هذه Views لا يعمل بشكل جاهز. Views غير المتعاونة هي RecyclerView وViewPager2.

مراجع إضافية