Todas as telas do app precisam ser responsivas e se adaptar ao espaço disponível.
É possível criar uma interface responsiva com
ConstraintLayout
que permite que uma abordagem 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 ao lado de uma lista de detalhes do item selecionado.
O componente
SlidingPaneLayout
oferece suporte à 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 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" }
Kotlin
dependencies { implementation("androidx.slidingpanelayout:slidingpanelayout:1.2.0") }
Configuração do layout XML
O SlidingPaneLayout
oferece um layout de dois painéis horizontais para uso no nível superior
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.
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 as visualizações não se sobreporem, o SlidingPaneLayout
vai 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
vai 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 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>
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 anterior, 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 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()
na FragmentTransaction
. Isso evita a criação de um backstack no painel de
detalhes.
Implementar o componente de navegação
Os exemplos nesta página usam SlidingPaneLayout
diretamente e exigem que você
gerencie as transações de fragmento manualmente. No entanto, o
componente de navegação oferece uma implementação pré-criada de
um layout de dois painéis usando
AbstractListDetailFragment
,
uma classe de API que usa um SlidingPaneLayout
internamente para gerenciar a lista
e os 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>
Implemente
onCreateListPaneView()
e
onListPaneViewCreated()
para fornecer uma visualização personalizada do painel de lista. No painel de detalhes,
AbstractListDetailFragment
usa um
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 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 ajuda a garantir que os links diretos externos naveguem primeiro
para o destino SlidingPaneLayout
e, em seguida, acessem o destino correto
do painel de detalhes.
Consulte o exemplo TwoPaneFragment para conferir uma implementação completa de um layout de dois painéis usando o componente de navegação.
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, 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 o callback 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
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 a seguinte documentação: