גלילה

משתני גלילה

המשתנים 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. דוגמה אופיינית לגלילה בתצוגת עץ היא רשימה בתוך רשימה אחרת, ודוגמה מורכבת יותר היא סרגל כלים שניתן לכווץ.

גלילה אוטומטית בתצוגת עץ

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

חלק מהרכיבים והמפעילים של Compose תומכים בגלילה אוטומטית בתצוגת עץ, ומספקים אותה כברירת מחדל: verticalScroll,‏ horizontalScroll,‏ scrollable,‏ ממשקי API של 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.

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

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

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

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

שלב לפני גלילה – שליחת בקשה למעלה

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

שלב לפני הגלילה – התכונה עוברת למטה

בשלב הצריכה בצומת, הצומת עצמו ישתמש ב-delta שלא היה בשימוש אצל ההורים שלו. זהו המצב שבו תנועת הגלילה מתבצעת בפועל וניתן לראות אותה.

שלב הצריכה בצומת

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

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

שלב אחרי הגלילה – שליחת בקשות

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

השלב שלאחר הגלילה – מעבר למטה

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

להשתתף במחזור הגלילה המורכב

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

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

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

כל קריאות החזרה (callbacks) מספקות מידע על הסוג NestedScrollSource.

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

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

כדי להפעיל את ממשק ה-API לפעולות קריאה וכתיבה (interop) של גלילה בתצוגת עץ בין רכיבי View שניתן לגלול בהם לבין רכיבי composable שניתן לגלול בהם, שממוקמים בתצוגת עץ בשני הכיוונים, אפשר להשתמש בממשק ה-API לפעולות קריאה וכתיבה (interop) של גלילה בתצוגת עץ כדי לצמצם את הבעיות האלה בתרחישים הבאים.

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

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

אם אתם צריכים יכולת פעולה הדדית של גלילה בתצוגת עץ בין קונטיינרים הורה מסוג View שניתן לגלילה לבין רכיבי Compose צאצאים שניתן לגלילה בתצוגת עץ, תוכלו להשתמש ב-rememberNestedScrollInteropConnection().

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

תרחיש לדוגמה שכיח הוא שימוש ב-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>

ב-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 – כשיש רכיב הורה שמכיל רכיב צאצא 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) {
            // ...
        }
    }
    // ...
}

בדוגמה הזו אפשר לראות איך משתמשים ב-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(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)
                    }
            }
        )
    }
}

ולבסוף, בדוגמה הזו מוצגת שימוש ב-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 אחראי להעברת ההבדלים (deltas) מרמת ה-Compose לרמה View. כך הרכיב יוכל להשתתף בגלילה בתצוגת עץ, אבל הגלילה של הרכיבים לא תופעל באופן אוטומטי. ברכיבים מורכבים שלא ניתן לגלול בהם באופן אוטומטי, כמו Box או Column, ההפרשים בגלילה ברכיבים כאלה לא יופצו במערכת הגלילה ההיררכית, וההפרשים לא יגיעו ל-NestedScrollConnection ש-rememberNestedScrollInteropConnection() מספק, ולכן ההפרשים האלה לא יגיעו לרכיב ההורה View. כדי לפתור את הבעיה, חשוב להגדיר גם משתני גלילה לסוגי הרכיבים המשולבים בתצוגת עץ האלה. מידע מפורט יותר זמין בקטע הקודם בנושא גלילה בתצוגת עץ.

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

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