Создание двухпанельного макета

Попробуйте способ создания
Jetpack Compose — рекомендуемый набор инструментов пользовательского интерфейса для Android. Узнайте, как работать с макетами в Compose.

Каждый экран в вашем приложении должен быть отзывчивым и адаптироваться к доступному пространству. Вы можете создать адаптивный пользовательский интерфейс с помощью ConstraintLayout , который позволяет масштабировать однопанельный подход до многих размеров, но на более крупных устройствах может быть полезно разделить макет на несколько панелей. Например, вам может потребоваться экран, чтобы показать список элементов рядом со списком деталей выбранного элемента.

Компонент SlidingPaneLayout поддерживает отображение двух панелей рядом на больших и складных устройствах, а также автоматически адаптируется для отображения только одной панели за раз на небольших устройствах, таких как телефоны.

Для руководства для конкретного устройства см. Обзор совместимости экрана .

Настраивать

Чтобы использовать SlidingPaneLayout , включите следующую зависимость в файл вашего приложения build.gradle :

классный

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

Котлин

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

Конфигурация макета XML

SlidingPaneLayout обеспечивает горизонтальную двустороннюю компоновку для использования на верхнем уровне пользовательского интерфейса. В этом макете используется первая панель в качестве списка контента или браузер, подчиненного первичному представлению детализации для отображения контента на другой панели.

Изображение, показывающее пример SlidingPanelayout
Рисунок 1. Пример макета, созданный с помощью SlidingPaneLayout .

SlidingPaneLayout использует ширину двух панелей, чтобы определить, показывать ли панели рядом. Например, если минимальный размер панели списка составляет 200 dp, а для панели сведений требуется 400 dp, то 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 , чтобы поменять недавно видимый фрагмент:

Котлин

// 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()
}

Ява

// 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 , чтобы обмениваться панелью детализации между направлениями на автономном навигационном графике:

Котлин

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()
}

Ява

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();
}

Навигационные навигационные графики детальной панели не должны присутствовать ни на одном внешнем навигационном графике. Тем не менее, любые глубокие ссылки в навигационном графике панели Detail должны быть прикреплены к месту назначения, который размещает SlidingPaneLayout . Это помогает гарантировать, что внешние глубокие ссылки сначала перемещаются в пункт назначения SlidingPaneLayout , а затем перейдите к правильному пункту назначения панели.

См. Пример двойного фрагмента для полной реализации двухслойного макета с использованием навигационного компонента.

Интегрировать с кнопкой System Back

На небольших устройствах, где панели списка и сведений перекрываются, убедитесь, что системная кнопка «Назад» переводит пользователя с панели сведений обратно на панель списка. Сделайте это , предоставив пользовательскую навигацию и подключив к нынешнему состоянию SlidingPaneLayout : OnBackPressedCallback :

Котлин

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
    }
}

Ява

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() :

Котлин

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.
    }
}

Ява

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 :

Котлин

binding.slidingPaneLayout.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED

Ява

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

Узнать больше

Чтобы узнать больше о проектировании макетов для различных форм -факторов, см. Следующую документацию:

Дополнительные ресурсы