שינויים בגלילה שמוצבים בתוך שינויים אחרים

גלילה בתצוגות עץ היא מערכת שבה כמה רכיבי גלילה שמוכלים זה בתוך זה פועלים יחד על ידי תגובה לתנועת גלילה אחת והעברת השינויים בגלילה.

מערכת הגלילה בתצוגות עץ מאפשרת תיאום בין רכיבים שניתן לגלול בהם ומקושרים באופן היררכי (לרוב על ידי שיתוף של אותו הורה). המערכת הזו מקשרת בין קונטיינרים של גלילה ומאפשרת אינטראקציה עם השינויים בגלילה שמופצים ומשותפים ביניהם.

ב-Compose יש כמה דרכים לטפל בגלילה בתצוגות עץ בין רכיבים קומפוזביליים. דוגמה אופיינית לגלילה בתצוגות עץ היא רשימה בתוך רשימה אחרת, ומקרה מורכב יותר הוא סרגל כלים שניתן לכיווץ.

גלילה אוטומטית של רכיבים מקוננים

גלילה פשוטה בתוך רכיב לא דורשת שום פעולה מצידכם. תנועות שמתחילות פעולת גלילה מועברות אוטומטית מצאצאים להורים, כך שכאשר הצאצא לא יכול לגלול יותר, התנועה מטופלת על ידי רכיב ההורה שלו.

גלילה אוטומטית בתוך גלילה נתמכת ומסופקת מראש על ידי חלק מהרכיבים והמשנים של Compose:‏ verticalScroll,‏ horizontalScroll,‏ scrollable,‏ ממשקי API של Lazy ו-TextField. כלומר, כשהמשתמש גולל רכיב צאצא פנימי של רכיבים בתצוגת עץ, משתני ה-modifier הקודמים מעבירים את השינויים בגלילה לרכיבי ההורה שתומכים בגלילה בתצוגת עץ.

בדוגמה הבאה מוצגים רכיבים שהוחל עליהם משתנה מסוג 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)
                    )
                }
            }
        }
    }
}

שני רכיבי ממשק משתמש עם גלילה אנכית שמוטמעים אחד בתוך השני, שמגיבים למחוות בתוך הרכיב הפנימי ומחוצה לו
איור 1. שני רכיבי ממשק משתמש עם גלילה אנכית שמוטמעים אחד בתוך השני, ומגיבים למחוות בתוך הרכיב הפנימי ומחוצה לו.

שימוש במקש הצירוף nestedScroll

אם אתם צריכים ליצור גלילה מתואמת מתקדמת בין כמה רכיבים, מגדיר השינוי nestedScroll נותן לכם יותר גמישות על ידי הגדרת היררכיית גלילה בתצוגת עץ. כמו שצוין בקטע הקודם, לחלק מהרכיבים יש תמיכה מובנית בגלילה בתצוגת עץ. עם זאת, ברכיבים שניתנים ליצירה ולא ניתנים לגלילה באופן אוטומטי, כמו Box או Column, השינויים בגלילה ברכיבים כאלה לא יתפשטו במערכת הגלילה בתצוגת עץ, והשינויים לא יגיעו ל-NestedScrollConnection או לרכיב ההורה. כדי לפתור את הבעיה, אפשר להשתמש ב-nestedScroll כדי להעניק תמיכה כזו לרכיבים אחרים, כולל רכיבים בהתאמה אישית.

מחזור גלילה מקונן

מחזור גלילה בתצוגת עץ הוא זרימת השינויים בגלילה שנשלחים למעלה ולמטה בעץ ההיררכיה דרך כל הרכיבים (או הצמתים) שהם חלק ממערכת הגלילה בתצוגת עץ, למשל באמצעות רכיבים ומשתני שינוי שניתן לגלול בהם, או nestedScroll.

שלבים במחזור גלילה בתצוגה משורשרת

כשמזוהה אירוע הפעלה (לדוגמה, תנועת אצבע) על ידי רכיב שאפשר לגלול בו, לפני שפעולת הגלילה עצמה מופעלת, הדלתאות שנוצרו נשלחות למערכת הגלילה המקוננת ועוברות שלושה שלבים: לפני הגלילה, צריכת הצומת ואחרי הגלילה.

שלבים במחזור הגלילה
המוטמעת
איור 2. שלבים במחזור הגלילה בתצוגה המקוננת.

בשלב הראשון, לפני הגלילה, הרכיב שקיבל את השינויים באירוע הטריגר ישלח את האירועים האלה למעלה, דרך עץ ההיררכיה, לרכיב ההורה העליון ביותר. אירועי הדלתא יתפשטו כלפי מטה, כלומר הדלתות יתפשטו מההורה ברמה הכי גבוהה כלפי מטה אל הצאצא שהתחיל את מחזור הגלילה המקוננת.

שלב לפני הגלילה – שליחה
איור 3. שלב לפני הגלילה: שליחה למעלה.

כך, לרכיבי ההורה של הגלילה המקוננת (רכיבים קומפוזביליים שמשתמשים ב-nestedScroll או בשינויי גלילה) יש הזדמנות לעשות משהו עם הדלתא לפני שהצומת עצמו יכול לצרוך אותה.

שלב טרום-גלילה – בועה
למטה
איור 4. שלב לפני הגלילה – העברת האירוע למטה (bubbling down).

בשלב צריכת הצומת, הצומת עצמו ישתמש בכל דלתא שלא נוצלה על ידי ההורים שלו. זה קורה כשהתנועה של הגלילה מתבצעת בפועל וגללו את המסך.

שלב צריכת הצומת
איור 5. שלב הצריכה של הצומת.

בשלב הזה, הילד יכול לבחור אם להשתמש בכל זמן הגלילה שנותר או רק בחלק ממנו. כל מה שנשאר יישלח חזרה למעלה כדי לעבור את שלב הגלילה.

לבסוף, בשלב שאחרי הגלילה, כל מה שהצומת עצמו לא צרך יישלח שוב למעלה אל האבות הקדמונים שלו לצריכה.

שלב אחרי הגלילה – שליחה למעלה
איור 6. שלב אחרי הגלילה – שליחה למעלה.

השלב שאחרי הגלילה פועל באופן דומה לשלב שלפני הגלילה, שבו כל אחד מההורים יכול לבחור אם לצרוך את האירוע או לא.

השלב שאחרי הגלילה – העברת אירועים (bubbling)
איור 7. שלב אחרי הגלילה – בועות למטה.

בדומה לגלילה, כשמחוות הגרירה מסתיימת, הכוונה של המשתמש יכולה להתפרש כמהירות שמשמשת להטלה (גלילה באמצעות אנימציה) של מאגר התוכן שאפשר לגלול בו. ההטלה היא גם חלק ממחזור הגלילה בתצוגת עץ, והמהירויות שנוצרות על ידי אירוע הגרירה עוברות שלבים דומים: לפני ההטלה, צריכת צומת ואחרי ההטלה. חשוב לזכור שאנימציית ההנפה משויכת רק לתנועת מגע, ולא תופעל על ידי אירועים אחרים, כמו נגישות או גלילה באמצעות חומרה.

השתתפות במחזור הגלילה המקוננת

השתתפות במחזור פירושה יירוט, צריכה ודיווח על צריכת השינויים לאורך ההיררכיה. ב-Compose יש קבוצת כלים שמשפיעים על אופן הפעולה של מערכת הגלילה בתצוגת עץ ועל האינטראקציה הישירה איתה. לדוגמה, כשצריך לעשות משהו עם השינויים בגלילה לפני שרכיב שניתן לגלילה בכלל מתחיל לגלול.

אם מחזור הגלילה המקונן הוא מערכת שפועלת על שרשרת של צמתים, ה-modifier‏ nestedScroll מאפשר ליירט את השינויים האלה ולהוסיף להם שינויים, וגם להשפיע על הנתונים (הפרשי הגלילה) שמועברים בשרשרת. אפשר למקם את המשתנה הזה בכל מקום בהיררכיה, והוא מתקשר עם מופעים של משתני גלילה בתצוגת עץ כדי לשתף מידע דרך הערוץ הזה. אבני הבניין של המאפיין הזה הן NestedScrollConnection ו-NestedScrollDispatcher.

‫NestedScrollConnection מספק דרך להגיב לשלבים של מחזור הגלילה המקונן ולהשפיע על מערכת הגלילה המקוננת. הוא מורכב מארבע שיטות קריאה חוזרת (callback), שכל אחת מהן מייצגת אחד משלבי הצריכה: לפני/אחרי גלילה ולפני/אחרי החלקה מהירה:

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
    }
}

כל קריאה חוזרת (callback) מספקת גם מידע על הדלתא שמופצת: דלתא של 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 מפעיל את מחזור הגלילה בתצוגה המקוננת. שימוש ב-dispatcher וקריאה לשיטות שלו מפעילים את המחזור. לקונטיינרים שאפשר לגלול יש משגר מובנה ששולח למערכת את השינויים שנקלטו במהלך תנועות. לכן, ברוב תרחישי השימוש בהתאמה אישית של גלילה מקוננת, נעשה שימוש ב-NestedScrollConnection במקום ב-dispatcher, כדי להגיב לדלתאות שכבר קיימות ולא לשלוח דלתאות חדשות. לדוגמאות נוספות לשימוש, אפשר לעיין במאמר בנושא 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 מבטיח שהתמונה תישאר ממוקמת אנכית במרכז כשהיא משתנה לגודל הרצוי.

תוצאה

הקטע הקודם יוצר אפקט של שינוי קנה מידה של התמונה בגלילה:

איור 8. אפקט של שינוי גודל התמונה בזמן גלילה.

יכולת פעולה הדדית של גלילה בתצוגות מקוננות

כשמנסים להטמיע רכיבי View שניתן לגלול בתוך רכיבים קומפוזביליים שניתן לגלול, או להיפך, יכול להיות שיהיו בעיות. הבעיות הכי בולטות קורות כשגוללים את הצאצא ומגיעים לגבולות ההתחלה או הסיום שלו, ומצפים שההורה ימשיך את הגלילה. עם זאת, יכול להיות שההתנהגות הצפויה הזו לא תתרחש או לא תפעל כצפוי.

הבעיה הזו נובעת מהציפיות שנוצרו ברכיבי קומפוזבל שניתן לגלול בהם. לרכיבי Composables שניתנים לגלילה יש כלל של 'גלילה מוטמעת כברירת מחדל', כלומר כל קונטיינר שניתן לגלילה חייב להשתתף בשרשרת הגלילה המוטמעת, גם כרכיב אב באמצעות NestedScrollConnection וגם כרכיב צאצא באמצעות NestedScrollDispatcher. כשהרכיב הצאצא מגיע לגבול, הוא מפעיל גלילה בתצוגה הפנימית של רכיב ההורה. לדוגמה, הכלל הזה מאפשר ל-Compose Pager ול-Compose LazyRow לעבוד יחד בצורה טובה. עם זאת, כשמבצעים גלילה עם יכולת פעולה הדדית באמצעות ViewPager2 או RecyclerView, מכיוון שהם לא מטמיעים את NestedScrollingParent3, אי אפשר לבצע גלילה רציפה מפריט הצאצא לפריט ההורה.

כדי להפעיל את nested scrolling interop API בין רכיבי View שניתן לגלול בהם לבין רכיבים שניתן לגלול בהם, שמוטמעים אחד בתוך השני בשני הכיוונים, אתם יכולים להשתמש ב-nested scrolling interop API כדי לפתור את הבעיות האלה בתרחישים הבאים.

הורה משתף פעולה View שמכיל צאצא ComposeView

רכיב הורה תואם View הוא רכיב שכבר מטמיע את NestedScrollingParent3, ולכן יכול לקבל שינויי גלילה מרכיב צאצא תואם שמוטמע בתוכו. במקרה כזה, ComposeView יפעל כצאצא ויצטרך להטמיע (באופן עקיף) את NestedScrollingChild3. דוגמה להורה משתף פעולה היא androidx.coordinatorlayout.widget.CoordinatorLayout.

אם אתם צריכים יכולת פעולה הדדית של גלילה מקוננת בין View parent containers שניתנים לגלילה לבין רכיבי composable צאצא שניתנים לגלילה ומקוננים, אתם יכולים להשתמש ב-rememberNestedScrollInteropConnection().

‫rememberNestedScrollInteropConnection() מאפשר וזוכר את NestedScrollConnection שמאפשר יכולת פעולה הדדית של גלילה בתצוגת עץ בין הורה View שמטמיע את NestedScrollingParent3 לבין צאצא של Compose. צריך להשתמש בזה בשילוב עם משתנה מסוג nestedScroll. מכיוון שהגלילה בתצוגות עץ מופעלת כברירת מחדל בצד של Compose, אתם יכולים להשתמש בחיבור הזה כדי להפעיל את הגלילה בתצוגות עץ בצד של View ולהוסיף את הלוגיקה הדרושה לחיבור בין Views לבין רכיבים קומפוזביליים.

תרחיש שימוש נפוץ הוא שימוש ב-CoordinatorLayout, ב-CollapsingToolbarLayout וב-composable צאצא, כמו בדוגמה הזו:

<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>

ב-Activity או ב-Fragment, צריך להגדיר את ה-composable הצאצא ואת ה-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

התרחיש הזה מתייחס להטמעה של API של פעולת הדדית לגלילה מקוננת בצד של Compose – כשיש רכיב composable ראשי שמכיל רכיב צאצא 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(
            title = {},
            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) {
            // ...
        }
    }
    // ...
}

בדוגמה הזו מוסבר איך משתמשים ב-API עם משתנה מסוג 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(R.layout.list_item, null)
                    .apply {
                        // Nested scrolling interop is enabled when
                        // nested scroll is enabled for the root View
                        ViewCompat.setNestedScrollingEnabled(this, true)
                    }
            }
        )
    }
}

ולסיום, בדוגמה הזו אפשר לראות איך משתמשים ב-API של יכולת הפעולה ההדדית של גלילה בתצוגות עץ עם 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. כדי לפתור את הבעיה, צריך להגדיר גם משנים שאפשר לגלול בהם לסוגים האלה של רכיבים הניתנים להרכבה (composables) מוטמעים. מידע נוסף זמין בקטע הקודם בנושא גלילה בתצוגות עץ.

הורה לא משתף פעולה View שמכיל ילד ComposeView

תצוגה לא תואמת היא תצוגה שלא מטמיעה את הממשקים הנדרשים של NestedScrolling בצד של View. הערה: המשמעות היא שאי אפשר להשתמש בViews כדי להפעיל גלילה מקוננת. Views שלא משתפות פעולה הן RecyclerView ו-ViewPager2.

מקורות מידע נוספים