Criar um layout de dois painéis

Todas as telas do app precisam ser responsivas e se adaptar ao espaço disponível. Criar uma IU responsiva com o ConstraintLayout permite que uma interface de painel único seja escalonada para vários tamanhos, mas dispositivos maiores podem se beneficiar da divisão do layout em vários painéis. Por exemplo, você pode querer que uma tela mostre uma lista de itens lado a lado com os detalhes do item selecionado no momento.

O componente SlidingPaneLayout é compatível com a exibição de dois painéis lado a lado em dispositivos maiores e dobráveis, adaptando-a automaticamente para mostrar apenas um painel por vez. em dispositivos menores, como smartphones.

Para ver orientações específicas do 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-beta01"
}

Kotlin

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

Configuração do layout XML

O SlidingPaneLayout oferece um layout de dois painéis horizontais para uso no nível superior de uma IU. 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.

O SlidingPaneLayout usa a largura dos dois painéis para determinar se eles serão exibidos lado a lado. Por exemplo, se o painel da lista for medido para ter um tamanho mínimo de 200 dp e o de detalhes precisar de 400 dp, o SlidingPaneLayout mostrará automaticamente os dois painéis lado a lado, contanto que haja, 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 não houver a sobreposição de visualizações, o SlidingPaneLayout aceitará o uso do parâmetro de layout 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 que tem espaço na tela para mostrar as duas visualizações lado a lado, o SlidingPaneLayout ajusta automaticamente o tamanho dos dois painéis para que fiquem posicionados em um dos lados da dobra ou de uma dobra ou articulação sobreposta. Nesse caso, as larguras definidas são consideradas como a largura mínima que precisa existir em cada lado do recurso de dobra. Se não houver espaço suficiente para manter esse tamanho mínimo, o SlidingPaneLayout voltará 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
        desired width (expressed using android:layout_width) would
        not 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
        the entire window is wide enough to fit both 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>

Nesse exemplo, o atributo android:name na FragmentContainerView adiciona o fragmento inicial ao painel de detalhes, garantindo que os usuários em dispositivos de tela grande não vejam um painel direito vazio quando o app é iniciado pela primeira vez.

Trocar programaticamente o painel de detalhes

No exemplo de XML acima, tocar em um elemento na RecyclerView aciona uma mudança 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 we're 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 we're 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 não chama addToBackStack() na FragmentTransaction. Isso evita a criação de um backstack no painel de detalhes.

O exemplo acima usou diretamente uma FragmentContainerView e transações de fragmento. No entanto, você pode implementar um painel de detalhes usando o componente de navegação. Se você usar NavHostFragment como painel de detalhes, poderá usar NavController para alternar entre os destinos. em um gráfico de navegação que contenha apenas os destinos a serem mostrados no painel de detalhes:

Kotlin

// A method on the Fragment that owns the SlidingPaneLayout,
// called by the adapter when an item is selected.
fun openDetails(itemId: Int) {
    // Assume the NavHostFragment is added with the +id/detail_container.
    val navHostFragment = childFragmentManager.findFragmentById(
       R.id.detail_container) as NavHostFragment
    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 we're 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

// A method on the Fragment that owns the SlidingPaneLayout,
// called by the adapter when an item is selected.
void openDetails(int itemId) {
    // Assume the NavHostFragment is added with the +id/detail_container.
    NavHostFragment navHostFragment = (NavHostFragment) getChildFragmentManager()
        .findFragmentById(R.id.detail_container);
    NavController navController = navHostFragment.getNavController();
    NavOptions.Builder builder = new NavOptions.Builder()
            // Pop all destinations off the back stack.
            .setPopUpTo(navController.getGraph().getStartDestination(), true);
    // If we're 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 devem estar presentes em nenhum gráfico de navegação externo em todo o app. No entanto, todos os links diretos no gráfico de navegação do painel de detalhes precisam ser anexados ao destino que hospeda o SlidingPaneLayout. Isso garante que os links diretos externos naveguem primeiro para o destino SlidingPaneLayout e, em seguida, acessem o destino correto do painel de detalhes.

Integrar com o botão "Voltar" do sistema

Em dispositivos menores em que os painéis de lista e de detalhes se sobrepõem, garanta que o botão "Voltar" do sistema leve o usuário do painel de detalhes de volta ao painel de listas. Para isso, ofereça navegação de retorno personalizada e conecte um OnBackPressedCallback ao estado atual do SlidingPaneLayout:

Kotlin

class TwoPaneOnBackPressedCallback(
    private val slidingPaneLayout: SlidingPaneLayout
) : OnBackPressedCallback(
    // Set the default 'enabled' state to true only if it is slidable (i.e., the panes
    // are overlapping) and open (i.e., 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 pressed.
        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 (i.e., the panes
        // are overlapping) and open (i.e., 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 pressed.
        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);
    }
}

Em seguida, você pode adicionar o callback a 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))

        // Setup the RecyclerView adapter, etc.
    }
}

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

        // Setup the RecyclerView adapter, etc.
    }
}

Modo de bloqueio

O SlidingPaneLayout sempre permite que você chame open() e close() manualmente para fazer a transição entre os painéis de lista e de detalhes em smartphones. Esses métodos não terão 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 os seguintes guias: