משתני גלילה
המשתנים 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
מוודא שהתמונה תישאר במרכז אנכית כשהיא תתאים.
התוצאה
קטע הקוד הקודם יוצר אפקט של תמונה שמתרחבת בזמן גלילה:
יכולת פעולה הדדית של גלילה בתצוגת עץ
כשמנסים להטמיע רכיבי 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
.
מומלץ עבורך
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- הסבר על תנועות
- העברת
CoordinatorLayout
ל-Compose - שימוש בתצוגות ב'כתיבה'