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

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

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

בדוגמאות שבדף הזה נעשה שימוש ישיר ב-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 וclose() בטלפונים כדי לעבור בין חלונית הרשימה לחלונית הפרטים.open() השיטות האלה לא משפיעות אם שני החלונות גלויים ולא חופפים.

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

Kotlin

binding.slidingPaneLayout.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED

Java

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

מידע נוסף

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

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