פיתוח פריסת חלונית תומכת

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

לפרטים נוספים, עיינו בהנחיות של Material 3 בנושא חלונית תומכת.

הטמעה של חלונית תמיכה עם פיגום (scaffold)

‫NavigableSupportingPaneScaffold הוא רכיב קומפוזבילי שמפשט את ההטמעה של פריסת חלונית תמיכה ב-Jetpack Compose. הוא כולל את SupportingPaneScaffold ומוסיף ניווט מובנה וטיפול בחיזוי החזרה.

תבנית של חלונית תמיכה תומכת בעד שלוש חלוניות:

  • החלונית הראשית: מציגה את התוכן הראשי.
  • חלונית עזר: מספקת הקשר נוסף או כלים שקשורים לחלונית הראשית.
  • חלונית נוספת (אופציונלית): משמשת לתוכן נוסף לפי הצורך.

הפיגום מותאם לגודל החלון:

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

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

הוספת יחסי תלות

‫NavigableSupportingPaneScaffold הוא חלק מספריית הפריסה הדינמית של Material 3.

מוסיפים את שלושת יחסי התלות הקשורים הבאים לקובץ build.gradle של האפליקציה או המודול:

Kotlin

implementation("androidx.compose.material3.adaptive:adaptive")
implementation("androidx.compose.material3.adaptive:adaptive-layout")
implementation("androidx.compose.material3.adaptive:adaptive-navigation")

מגניב

implementation 'androidx.compose.material3.adaptive:adaptive'
implementation 'androidx.compose.material3.adaptive:adaptive-layout'
implementation 'androidx.compose.material3.adaptive:adaptive-navigation'
  • ‫adaptive: אבני בניין ברמה נמוכה כמו HingeInfo ו-Posture

  • ‫adaptive-layout: פריסות מותאמות כמו ListDetailPaneScaffold ו-SupportingPaneScaffold

  • ניווט-אדפטיבי: פריטים הניתנים לקומפוזיציה לניווט בתוך ובין חלוניות, וכן פריסות אדפטיביות התומכות בניווט כברירת מחדל כגון NavigableListDetailPaneScaffold ו-NavigableSupportingPaneScaffold

מוודאים שהפרויקט כולל את compose-material3-adaptive מגרסה 1.1.0-beta1 ואילך.

איך מפעילים את חיזוי תנועת החזרה

כדי להפעיל אנימציות של חיזוי החזרה ב-Android מגרסה 15 ומטה, צריך להביע הסכמה לתמיכה בחיזוי תנועת החזרה. כדי להביע הסכמה, מוסיפים את הערך android:enableOnBackInvokedCallback="true" לתג <application> או לתגי <activity> נפרדים בקובץ AndroidManifest.xml.

אחרי שהאפליקציה מטרגטת ל-Android 16 (רמת API ‏36) ומעלה, התכונה "חיזוי החזרה" מופעלת כברירת מחדל.

יצירת נווט

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

val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator()
val scope = rememberCoroutineScope()

העברת ה-navigator ל-scaffold

ה-scaffold דורש ThreePaneScaffoldNavigator, שהוא ממשק שמייצג את המצב של ה-scaffold, את ThreePaneScaffoldValue ואת PaneScaffoldDirective.

NavigableSupportingPaneScaffold(
    navigator = scaffoldNavigator,
    mainPane = { /*...*/ },
    supportingPane = { /*...*/ },
)

החלונית הראשית והחלונית המשנית הן רכיבים קומפוזביליים שמכילים את התוכן שלכם. כדי להחיל את האנימציות שמוגדרות כברירת מחדל בחלונית במהלך הניווט, משתמשים ב-AnimatedPane. משתמשים בערך של ה-scaffold כדי לבדוק אם חלונית התמיכה מוסתרת. אם כן, מציגים כפתור שמפעיל את navigateTo(SupportingPaneScaffoldRole.Supporting) כדי להציג את חלונית התמיכה.

במסכים גדולים, משתמשים ב-method‏ ThreePaneScaffoldNavigator.navigateBack() כדי לסגור את החלונית המשנית, ומעבירים את הקבוע BackNavigationBehavior.PopUntilScaffoldValueChange. קריאה ל-method הזה מאלצת הרכבה מחדש של NavigableSupportingPaneScaffold. במהלך ה-recomposition, בודקים את המאפיין ThreePaneScaffoldNavigator.currentDestination כדי לקבוע אם להציג את חלונית העזר.

הנה הטמעה מלאה של הפיגום:

val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator()
val scope = rememberCoroutineScope()
val backNavigationBehavior = BackNavigationBehavior.PopUntilScaffoldValueChange

NavigableSupportingPaneScaffold(
    navigator = scaffoldNavigator,
    mainPane = {
        AnimatedPane(
            modifier = Modifier
                .safeContentPadding()
                .background(Color.Red)
        ) {
            if (scaffoldNavigator.scaffoldValue[SupportingPaneScaffoldRole.Supporting] == PaneAdaptedValue.Hidden) {
                Button(
                    modifier = Modifier
                        .wrapContentSize(),
                    onClick = {
                        scope.launch {
                            scaffoldNavigator.navigateTo(SupportingPaneScaffoldRole.Supporting)
                        }
                    }
                ) {
                    Text("Show supporting pane")
                }
            } else {
                Text("Supporting pane is shown")
            }
        }
    },
    supportingPane = {
        AnimatedPane(modifier = Modifier.safeContentPadding()) {
            Column {
                // Allow users to dismiss the supporting pane. Use back navigation to
                // hide an expanded supporting pane.
                if (scaffoldNavigator.scaffoldValue[SupportingPaneScaffoldRole.Supporting] == PaneAdaptedValue.Expanded) {
                    // Material design principles promote the usage of a right-aligned
                    // close (X) button.
                    IconButton(
                        modifier =  Modifier.align(Alignment.End).padding(16.dp),
                        onClick = {
                            scope.launch {
                                scaffoldNavigator.navigateBack(backNavigationBehavior)
                            }
                        }
                    ) {
                        Icon(Icons.Default.Close, contentDescription = "Close")
                    }
                }
                Text("Supporting pane")
            }

        }
    }
)

חילוץ רכיבי Composable של חלוניות

מחלצים את החלוניות הנפרדות של SupportingPaneScaffold לפונקציות קומפוזביליות משלהן כדי שיהיה אפשר לעשות בהן שימוש חוזר ולבדוק אותן. כדי להשתמש באנימציות שמוגדרות כברירת מחדל, משתמשים ב-ThreePaneScaffoldScope כדי לגשת אל AnimatedPane:

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ThreePaneScaffoldPaneScope.MainPane(
    shouldShowSupportingPaneButton: Boolean,
    onNavigateToSupportingPane: () -> Unit,
    modifier: Modifier = Modifier,
) {
    AnimatedPane(
        modifier = modifier.safeContentPadding()
    ) {
        // Main pane content
        if (shouldShowSupportingPaneButton) {
            Button(onClick = onNavigateToSupportingPane) {
                Text("Show supporting pane")
            }
        } else {
            Text("Supporting pane is shown")
        }
    }
}

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ThreePaneScaffoldPaneScope.SupportingPane(
    scaffoldNavigator: ThreePaneScaffoldNavigator<Any>,
    modifier: Modifier = Modifier,
    backNavigationBehavior: BackNavigationBehavior = BackNavigationBehavior.PopUntilScaffoldValueChange,
) {
    val scope = rememberCoroutineScope()
    AnimatedPane(modifier = Modifier.safeContentPadding()) {
        Column {
            // Allow users to dismiss the supporting pane. Use back navigation to
            // hide an expanded supporting pane.
            if (scaffoldNavigator.scaffoldValue[SupportingPaneScaffoldRole.Supporting] == PaneAdaptedValue.Expanded) {
                // Material design principles promote the usage of a right-aligned
                // close (X) button.
                IconButton(
                    modifier =  modifier.align(Alignment.End).padding(16.dp),
                    onClick = {
                        scope.launch {
                            scaffoldNavigator.navigateBack(backNavigationBehavior)
                        }
                    }
                ) {
                    Icon(Icons.Default.Close, contentDescription = "Close")
                }
            }
            Text("Supporting pane")
        }

    }
}

חילוץ החלוניות לרכיבים קומפוזביליים מפשט את השימוש ב-SupportingPaneScaffold (השוו את הקוד הבא להטמעה המלאה של הפיגום בקטע הקודם):

val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator()
val scope = rememberCoroutineScope()

NavigableSupportingPaneScaffold(
    navigator = scaffoldNavigator,
    mainPane = {
        MainPane(
            shouldShowSupportingPaneButton = scaffoldNavigator.scaffoldValue.secondary == PaneAdaptedValue.Hidden,
            onNavigateToSupportingPane = {
                scope.launch {
                    scaffoldNavigator.navigateTo(ThreePaneScaffoldRole.Secondary)
                }
            }
        )
    },
    supportingPane = { SupportingPane(scaffoldNavigator = scaffoldNavigator) },
)

אם אתם צריכים יותר שליטה על היבטים ספציפיים של ה-scaffold, כדאי להשתמש ב-SupportingPaneScaffold במקום ב-NavigableSupportingPaneScaffold. הפונקציה הזו מקבלת PaneScaffoldDirective ו-ThreePaneScaffoldValue או ThreePaneScaffoldState בנפרד. הגמישות הזו מאפשרת לכם להטמיע לוגיקה מותאמת אישית לריווח בין החלוניות ולקבוע כמה חלוניות יוצגו בו-זמנית. אפשר גם להפעיל תמיכה בחיזוי החזרה על ידי הוספת ThreePaneScaffoldPredictiveBackHandler.

הוספה של ThreePaneScaffoldPredictiveBackHandler

מצרפים את ה-handler של חיזוי החזרה שמקבל מופע של scaffold navigator ומציינים את backBehavior. ההגדרה הזו קובעת איך יעדים נשלפים מהמחסנית במהלך ניווט לאחור. ואז מעבירים את scaffoldDirective ואת scaffoldState אל SupportingPaneScaffold. משתמשים בעומס היתר שמקבל ThreePaneScaffoldState, ומעבירים את scaffoldNavigator.scaffoldState.

מגדירים את החלוניות הראשית והמשנית בתוך SupportingPaneScaffold. משתמשים ב-AnimatedPane לאנימציות של חלוניות ברירת המחדל.

אחרי שתבצעו את השלבים האלה, הקוד אמור להיראות בערך כך:

val scaffoldNavigator = rememberSupportingPaneScaffoldNavigator()
val scope = rememberCoroutineScope()

ThreePaneScaffoldPredictiveBackHandler(
    navigator = scaffoldNavigator,
    backBehavior = BackNavigationBehavior.PopUntilScaffoldValueChange
)

SupportingPaneScaffold(
    directive = scaffoldNavigator.scaffoldDirective,
    scaffoldState = scaffoldNavigator.scaffoldState,
    mainPane = {
        MainPane(
            shouldShowSupportingPaneButton = scaffoldNavigator.scaffoldValue.secondary == PaneAdaptedValue.Hidden,
            onNavigateToSupportingPane = {
                scope.launch {
                    scaffoldNavigator.navigateTo(ThreePaneScaffoldRole.Secondary)
                }
            }
        )
    },
    supportingPane = { SupportingPane(scaffoldNavigator = scaffoldNavigator) },
)