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

عوامل تعديل الانتقال

توفّر المُعدِّلات 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 ستتمّ دعوتها في كلّ خطوة لفّ (من خلال إدخال إيماءة أو لفّ سلس أو رمي) مع إضافة القيمة delta بالبكسل. يجب أن تعرِض هذه الدالة مقدار المسافة التي تم الانتقال إليها باستخدام شريط التمرير، لضمان انتشار الحدث بشكلٍ سليم في الحالات التي تتوفّر فيها عناصر متداخلة تحتوي على المُعدِّل scrollable.

يرصد المقتطف التالي الإيماءات ويعرض قيمة رقمية لoffset، ولكنّه لا يغيّر موضع أي عناصر:

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

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

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

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

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

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

الانتقال التلقائي للأعلى أو للأسفل في القوائم المُدمجة

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

تتوفّر ميزة الانتقال المتداخل التلقائي بشكل تلقائي في بعض مكونات 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 أو المُعدِّلات القابلة للتقديم/الترجيع) فرصة لإجراء إجراء ما بشأن القيمة الإضافية قبل أن يتمكّن العنصر نفسه من استخدامها.

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

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

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

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

أخيرًا، في مرحلة ما بعد الانتقال إلى الأسفل، سيتم إرسال أيّ محتوى لم تستهلكه العقدة نفسها إلى أسلافها مرة أخرى للاستهلاك.

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

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

مرحلة ما بعد الانتقال للأسفل: تصاعد المحتوى
للأسفل

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

المشاركة في دورة الانتقال المُدمَج

تعني المشاركة في الدورة اعتراض استهلاك القيم الجديدة واستخدامها وإعداد تقارير عنها على مستوى التسلسل الهرمي. يوفّر 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 التغيُّر في حجم الصورة استنادًا إلى قيمة delta لقيمة التمرير.
  • تخزِّن متغيّر الحالة 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 حاويات والد قابلة للانتقال وعناصر تركيب فرعية قابلة للانتقال متداخلة، يمكنك استخدام rememberNestedScrollInteropConnection().

يسمح rememberNestedScrollInteropConnection() ويتذكر NestedScrollConnection الذي يتيح إمكانية التشغيل التفاعلي للانتقال المتداخل بين View الرئيسي الذي ينفّذ NestedScrollingParent3 وعنصر الإنشاء الفرعي. ويجب استخدامه مع أحد المُعدِّلات التالية: nestedScroll. بما أنّ ميزة "الانتقال المتداخل" مفعَّلة تلقائيًا على جانب "الإنشاء"، يمكنك استخدام هذا الربط لتفعيل كلّ من ميزة "الانتقال المتداخل" على جانب 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

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

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

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

حساب أحد الوالدَين غير المتعاونَين View الذي يتضمّن حساب طفل ComposeView

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