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

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

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

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

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

הגדרה

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

Groovy

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 מציגה באופן אוטומטי את שתי החלוניות זו לצד זו, כל עוד יש רוחב זמין של 600 dp לפחות.

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

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

Kotlin

binding.slidingPaneLayout.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED

Java

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

מידע נוסף

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

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