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

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

يوفّر المعدّلان 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 { mutableStateOf(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 قابلة للتمرير في عناصر قابلة للإنشاء قابلة للتمرير، أو العكس، قد تواجه مشاكل. تحدث أبرزها عندما تنتقل إلى بداية أو نهاية حدود العنصر الفرعي وتتوقّع أن يتولّى العنصر الرئيسي عملية التمرير. ومع ذلك، قد لا يحدث هذا السلوك المتوقّع أو قد لا يعمل على النحو المتوقّع.

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

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

محتوى View يضم طفلاً ComposeView

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

إذا كنت بحاجة إلى إمكانية التشغيل التفاعلي للتمرير المتداخل بين الحاويات القابلة للتمرير View الرئيسية وعناصر Composables الثانوية القابلة للتمرير والمتداخلة، يمكنك استخدام 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. سيتمكّن العنصر الرئيسي الذي يتضمّن Compose من تلقّي فروق التمرير المدمجة من عنصر فرعي قابل للتمرير ومدمج 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.

مراجع إضافية