Criar um layout de dois painéis

Testar o Compose
O Jetpack Compose é o kit de ferramentas de interface recomendado para Android. Aprenda a trabalhar com layouts no Compose.

Cada tela do app precisa ser responsiva e se adaptar ao espaço disponível. Você pode criar uma interface responsiva ConstraintLayout, que permite que um painel único podem ser dimensionados para muitos tamanhos, mas dispositivos maiores podem se beneficiar da divisão o layout em vários painéis. Por exemplo, talvez você queira que uma tela mostre lista de itens ao lado de uma lista de detalhes do item selecionado.

A SlidingPaneLayout oferece suporte à exibição de dois painéis lado a lado em dispositivos maiores e dobráveis, adaptando-se automaticamente para mostrar apenas um painel por vez e dispositivos menores, como celulares.

Para orientações específicas sobre cada dispositivo, consulte a visão geral de compatibilidade de tela.

Configurar

Para usar o SlidingPaneLayout, inclua a seguinte dependência no arquivo build.gradle do app:

Groovy

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

Kotlin

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

Configuração do layout XML

SlidingPaneLayout fornece um layout horizontal de dois painéis para uso na parte de cima. de uma interface. Esse layout usa o primeiro painel como uma lista de conteúdo ou um navegador, subordinado a uma visualização de detalhes principal para exibir conteúdo no outro painel.

Uma imagem mostrando um exemplo de SlidingPaneLayout
Figura 1. Um exemplo de layout criado com SlidingPaneLayout:

O SlidingPaneLayout usa a largura dos dois painéis para determinar se eles serão exibidos lado a lado. Por exemplo, se o painel de lista for medido para ter um tamanho mínimo de 200 dp e o painel de detalhes precisar de 400 dp, o A SlidingPaneLayout mostra automaticamente os dois painéis lado a lado, desde que tenha pelo menos 600 dp de largura disponível.

As visualizações filhas vão se sobrepor se a largura combinada exceder a disponível no SlidingPaneLayout. Nesse caso, as visualizações filhas se expandem para preencher a largura disponível no SlidingPaneLayout. O usuário pode deslizar a visualização superior para fora da tela arrastando-a de volta da borda.

Se as visualizações não se sobrepuserem, SlidingPaneLayout vai oferecer suporte ao uso do layout. parâmetro layout_weight em visualizações filhas para definir como dividir o espaço restante após a conclusão da medição. Esse parâmetro só é relevante para a largura.

Em um dispositivo dobrável com espaço na tela para mostrar as duas visualizações lado a lado lado, SlidingPaneLayout ajusta automaticamente o tamanho dos dois painéis para que eles são posicionados ao lado de uma dobra ou articulação sobreposta. Neste caso, as larguras definidas são consideradas a largura mínima que deve existir em cada lado do recurso de dobra. Se não houver espaço suficiente para manter tamanho mínimo, SlidingPaneLayout volta a sobrepor as visualizações.

Veja um exemplo de como usar um SlidingPaneLayout que tem uma RecyclerView como painel esquerdo e uma FragmentContainerView como a visualização de detalhes principal para exibir o conteúdo do painel esquerdo:

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

Neste exemplo, o atributo android:name em FragmentContainerView adiciona do fragmento inicial para o painel de detalhes, garantindo que os usuários em telas grandes os dispositivos não veem um painel direito vazio quando o app é iniciado pela primeira vez.

Trocar programaticamente o painel de detalhes

No exemplo XML anterior, tocar em um elemento na RecyclerView aciona uma alteração no painel de detalhes. Ao usar fragmentos, isso requer uma FragmentTransaction que substitua o painel direito, chamando open() no SlidingPaneLayout para trocar para o fragmento que ficou visível recentemente:

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

Esse código, especificamente, não chama addToBackStack() no FragmentTransaction. Isso evita a criação de um backstack no painel de detalhes.

Os exemplos nesta página usam SlidingPaneLayout diretamente e exigem que você gerenciar transações de fragmentos manualmente. No entanto, O componente de navegação fornece uma implementação pré-criada de um layout de dois painéis AbstractListDetailFragment, uma classe de API que usa um SlidingPaneLayout internamente para gerenciar a lista; e painéis de detalhes.

Isso permite simplificar a configuração do layout XML. Em vez de declarar explicitamente um SlidingPaneLayout e os dois painéis, o layout só precisa de uma FragmentContainerView para manter a implementação de 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>

Implementação onCreateListPaneView() e onListPaneViewCreated() para fornecer uma visualização personalizada para o painel de lista. Para o painel de detalhes, AbstractListDetailFragment usa uma NavHostFragment. Isso significa que você pode definir um gráfico de navegação que contenha apenas os destinos que vão ser mostrados no painel de detalhes. Em seguida, use NavController para alternar os destinos do painel de detalhes no gráfico autônomo de navegação:

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

Os destinos no gráfico de navegação do painel de detalhes não podem estar presentes no qualquer gráfico de navegação externo do app. No entanto, todos os links diretos nos detalhes gráfico de navegação do painel deve ser anexado ao destino que hospeda o SlidingPaneLayout: Isso ajuda a garantir que os links diretos externos naveguem primeiro ao destino SlidingPaneLayout e navegar até o detalhe correto destino do painel.

Consulte a Exemplo de TwoPaneFragment para uma implementação completa de um layout de dois painéis usando o componente Navigation.

Integrar com o botão "Voltar" do sistema

Em dispositivos menores em que os painéis de lista e de detalhes se sobrepõem, verifique se o sistema Voltar leva o usuário do painel de detalhes de volta para o painel de lista. Faça isto ao fornecer um retorno personalizado de navegação e conectar um OnBackPressedCallback para o estado atual da 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);
    }
}

É possível adicionar a chamada de retorno ao OnBackPressedDispatcher usando 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.
    }
}

Modo de bloqueio

SlidingPaneLayout sempre permite que você chame open() manualmente e close() para fazer a transição entre os painéis de lista e de detalhes em smartphones. Esses métodos não têm efeito se os dois painéis estiverem visíveis e não se sobrepuserem.

Quando houver a sobreposição dos painéis de lista e de detalhes, os usuários poderão deslizar nas duas direções por padrão, alternando livremente entre os painéis, mesmo quando não estiverem usando a navegação por gestos. Você pode controlar a direção do deslize definindo o modo de bloqueio do SlidingPaneLayout:

Kotlin

binding.slidingPaneLayout.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED

Java

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

Saiba mais

Para saber mais sobre o design de layouts para diferentes formatos, consulte a documentação a seguir:

Outros recursos