יצירת פריסה של שתי חלוניות

אפשר לנסות את הדרך של כתיבת הודעה
‫Jetpack Compose היא ערכת הכלים המומלצת לבניית ממשק משתמש ב-Android. איך עובדים עם פריסות בכתיבת אימייל

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

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

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

הגדרה

כדי להשתמש ב-SlidingPaneLayout, צריך לכלול את התלות הבאה בקובץ build.gradle של האפליקציה:

מגניב

dependencies {
    implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0"
}

Kotlin

dependencies {
    implementation("androidx.slidingpanelayout:slidingpanelayout:1.2.0")
}

הגדרת פריסה של XML

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

תמונה שמציגה דוגמה ל-SlidingPaneLayout
איור 1. דוגמה לפריסה שנוצרה באמצעות SlidingPaneLayout.

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

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

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

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

דוגמה לשימוש ב-SlidingPaneLayout עם RecyclerView כחלונית הימנית ועם FragmentContainerView כתצוגת הפרטים הראשית להצגת תוכן מהחלונית הימנית:

<!-- two_pane.xml -->
<androidx.slidingpanelayout.widget.SlidingPaneLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   android:id="@+id/sliding_pane_layout"
   android:layout_width="match_parent"
   android:layout_height="match_parent">

   <!-- The first child view becomes the left pane. When the combined needed
        width, expressed using android:layout_width, doesn't fit on-screen at
        once, the right pane is permitted to overlap the left. -->

   <androidx.recyclerview.widget.RecyclerView
             android:id="@+id/list_pane"
             android:layout_width="280dp"
             android:layout_height="match_parent"
             android:layout_gravity="start"/>

   <!-- The second child becomes the right (content) pane. In this example,
        android:layout_weight is used to expand this detail pane to consume
        leftover available space when the entire window is wide enough to fit
        the left and right pane.-->
   <androidx.fragment.app.FragmentContainerView
       android:id="@+id/detail_container"
       android:layout_width="300dp"
       android:layout_weight="1"
       android:layout_height="match_parent"
       android:background="#ff333333"
       android:name="com.example.SelectAnItemFragment" />
</androidx.slidingpanelayout.widget.SlidingPaneLayout>

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

החלפה של חלונית הפרטים באופן פרוגרמטי

בדוגמת ה-XML הקודמת, הקשה על רכיב ב-RecyclerView מפעילה שינוי בחלונית הפרטים. כשמשתמשים בפרגמנטים, צריך להשתמש ב-FragmentTransaction כדי להחליף את החלונית הימנית, ולקרוא ל-open() ב-SlidingPaneLayout כדי לעבור לפרגמנט החדש שגלוי:

Kotlin

// A method on the Fragment that owns the SlidingPaneLayout,called by the
// adapter when an item is selected.
fun openDetails(itemId: Int) {
    childFragmentManager.commit {
        setReorderingAllowed(true)
        replace<ItemFragment>(R.id.detail_container,
            bundleOf("itemId" to itemId))
        // If it's already open and the detail pane is visible, crossfade
        // between the fragments.
        if (binding.slidingPaneLayout.isOpen) {
            setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
        }
    }
    binding.slidingPaneLayout.open()
}

Java

// A method on the Fragment that owns the SlidingPaneLayout, called by the
// adapter when an item is selected.
void openDetails(int itemId) {
    Bundle arguments = new Bundle();
    arguments.putInt("itemId", itemId);
    FragmentTransaction ft = getChildFragmentManager().beginTransaction()
            .setReorderingAllowed(true)
            .replace(R.id.detail_container, ItemFragment.class, arguments);
    // If it's already open and the detail pane is visible, crossfade
    // between the fragments.
    if (binding.getSlidingPaneLayout().isOpen()) {
        ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
    }
    ft.commit();
    binding.getSlidingPaneLayout().open();
}

הקוד הזה לא קורא ספציפית ל-addToBackStack() ב-FragmentTransaction. כך לא נוצר מקבץ פעילויות קודמות (back stack) בחלונית הפרטים.

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

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

<FrameLayout 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">
    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/two_pane_container"
        <!-- The name of your AbstractListDetailFragment implementation.-->
        android:name="com.example.testapp.TwoPaneFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        <!-- The navigation graph for your detail pane.-->
        app:navGraph="@navigation/two_pane_navigation" />
</FrameLayout>

מטמיעים את onCreateListPaneView() ואת onListPaneViewCreated() כדי לספק תצוגה מותאמת אישית לחלונית הרשימה. בחלונית הפרטים,‏ AbstractListDetailFragment משתמש בNavHostFragment. כלומר, אפשר להגדיר גרף ניווט שמכיל רק את היעדים שיוצגו בחלונית הפרטים. אחר כך תוכלו להשתמש בNavController כדי להחליף את חלונית הפרטים בין היעדים בתרשים הניווט העצמאי:

Kotlin

fun openDetails(itemId: Int) {
    val navController = navHostFragment.navController
    navController.navigate(
        // Assume the itemId is the android:id of a destination in the graph.
        itemId,
        null,
        NavOptions.Builder()
            // Pop all destinations off the back stack.
            .setPopUpTo(navController.graph.startDestination, true)
            .apply {
                // If it's already open and the detail pane is visible,
                // crossfade between the destinations.
                if (binding.slidingPaneLayout.isOpen) {
                    setEnterAnim(R.animator.nav_default_enter_anim)
                    setExitAnim(R.animator.nav_default_exit_anim)
                }
            }
            .build()
    )
    binding.slidingPaneLayout.open()
}

Java

void openDetails(int itemId) {
    NavController navController = navHostFragment.getNavController();
    NavOptions.Builder builder = new NavOptions.Builder()
            // Pop all destinations off the back stack.
            .setPopUpTo(navController.getGraph().getStartDestination(), true);
    // If it's already open and the detail pane is visible, crossfade between
    // the destinations.
    if (binding.getSlidingPaneLayout().isOpen()) {
        builder.setEnterAnim(R.animator.nav_default_enter_anim)
                .setExitAnim(R.animator.nav_default_exit_anim);
    }
    navController.navigate(
        // Assume the itemId is the android:id of a destination in the graph.
        itemId,
        null,
        builder.build()
    );
    binding.getSlidingPaneLayout().open();
}

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

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

שילוב עם כפתור 'הקודם' של המערכת

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

Kotlin

class TwoPaneOnBackPressedCallback(
    private val slidingPaneLayout: SlidingPaneLayout
) : OnBackPressedCallback(
    // Set the default 'enabled' state to true only if it is slidable, such as
    // when the panes overlap, and open, such as when the detail pane is
    // visible.
    slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen
), SlidingPaneLayout.PanelSlideListener {

    init {
        slidingPaneLayout.addPanelSlideListener(this)
    }

    override fun handleOnBackPressed() {
        // Return to the list pane when the system back button is tapped.
        slidingPaneLayout.closePane()
    }

    override fun onPanelSlide(panel: View, slideOffset: Float) { }

    override fun onPanelOpened(panel: View) {
        // Intercept the system back button when the detail pane becomes
        // visible.
        isEnabled = true
    }

    override fun onPanelClosed(panel: View) {
        // Disable intercepting the system back button when the user returns to
        // the list pane.
        isEnabled = false
    }
}

Java

class TwoPaneOnBackPressedCallback extends OnBackPressedCallback
        implements SlidingPaneLayout.PanelSlideListener {

    private final SlidingPaneLayout mSlidingPaneLayout;

    TwoPaneOnBackPressedCallback(@NonNull SlidingPaneLayout slidingPaneLayout) {
        // Set the default 'enabled' state to true only if it is slideable, such
        // as when the panes overlap, and open, such as when the detail pane is
        // visible.
        super(slidingPaneLayout.isSlideable() && slidingPaneLayout.isOpen());
        mSlidingPaneLayout = slidingPaneLayout;
        slidingPaneLayout.addPanelSlideListener(this);
    }

    @Override
    public void handleOnBackPressed() {
        // Return to the list pane when the system back button is tapped.
        mSlidingPaneLayout.closePane();
    }

    @Override
    public void onPanelSlide(@NonNull View panel, float slideOffset) { }

    @Override
    public void onPanelOpened(@NonNull View panel) {
        // Intercept the system back button when the detail pane becomes
        // visible.
        setEnabled(true);
    }

    @Override
    public void onPanelClosed(@NonNull View panel) {
        // Disable intercepting the system back button when the user returns to
        // the list pane.
        setEnabled(false);
    }
}

אפשר להוסיף את הפונקציה לביצוע שיחה חוזרת אל OnBackPressedDispatcher באמצעות addCallback():

Kotlin

class TwoPaneFragment : Fragment(R.layout.two_pane) {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val binding = TwoPaneBinding.bind(view)

        // Connect the SlidingPaneLayout to the system back button.
        requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner,
            TwoPaneOnBackPressedCallback(binding.slidingPaneLayout))

        // Set up the RecyclerView adapter.
    }
}

Java

class TwoPaneFragment extends Fragment {

    public TwoPaneFragment() {
        super(R.layout.two_pane);
    }

    @Override
    public void onViewCreated(@NonNull View view,
             @Nullable Bundle savedInstanceState) {
        TwoPaneBinding binding = TwoPaneBinding.bind(view);

        // Connect the SlidingPaneLayout to the system back button.
        requireActivity().getOnBackPressedDispatcher().addCallback(
            getViewLifecycleOwner(),
            new TwoPaneOnBackPressedCallback(binding.getSlidingPaneLayout()));

        // Set up the RecyclerView adapter.
    }
}

מצב נעילה

SlidingPaneLayout תמיד מאפשרת להתקשר באופן ידני open() וclose() לעבור בין חלונית הרשימה לחלונית הפרטים בטלפונים. לשיטות האלה אין השפעה אם שני החלוניות גלויות ולא חופפות.

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

Kotlin

binding.slidingPaneLayout.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED

Java

binding.getSlidingPaneLayout().setLockMode(SlidingPaneLayout.LOCK_MODE_LOCKED);

מידע נוסף

מידע נוסף על עיצוב פריסות למגוון גורמי צורה זמין במאמרי העזרה הבאים:

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