Todas las pantallas de tu app deben ser responsivas y adaptarse al espacio disponible.
Puedes crear una IU responsiva con
ConstraintLayout
que permite un panel único
la escala a muchos tamaños, pero los dispositivos más grandes pueden beneficiarse de dividir
el diseño en varios paneles. Por ejemplo, es posible que desees que una pantalla muestre un
Lista de elementos junto a una lista de detalles del elemento seleccionado.
El
SlidingPaneLayout
admite mostrar dos paneles, uno al lado del otro, en dispositivos más grandes y
plegables y se adaptan automáticamente para mostrar solo un panel a la vez en
dispositivos más pequeños, como teléfonos.
Si necesitas orientación específica para un dispositivo, consulta la descripción general de la compatibilidad de pantallas.
Configuración
Para usar SlidingPaneLayout
, incluye la siguiente dependencia en el archivo build.gradle
de tu app:
Groovy
dependencies { implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0" }
Kotlin
dependencies { implementation("androidx.slidingpanelayout:slidingpanelayout:1.2.0") }
Configuración del diseño XML
SlidingPaneLayout
proporciona un diseño horizontal de dos paneles para usar en la parte superior.
nivel de una IU. Este diseño usa el primer panel como una lista de contenido o un navegador, subordinado a una vista principal de detalles para mostrar contenido en el otro panel.
![Una imagen que muestra un ejemplo de SlidingPaneLayout](https://developer.android.google.cn/static/images/ui/slidingpanelayout-example.png?authuser=9&hl=es)
SlidingPaneLayout
SlidingPaneLayout
usa el ancho de los dos paneles para determinar si se muestran los paneles uno al lado del otro. Por ejemplo, si el panel de lista se mide para tener una
tamaño mínimo de 200 dp y el panel de detalles necesita 400 dp. Luego,
SlidingPaneLayout
muestra automáticamente los dos paneles uno al lado del otro, siempre y cuando
tenga al menos 600 dp de ancho disponible.
Las vistas secundarias se superponen si el ancho combinado supera el ancho disponible en el SlidingPaneLayout
. En este caso, las vistas secundarias se expanden para llenar el ancho disponible en el SlidingPaneLayout
. El usuario puede arrastrar la vista superior desde el borde de la pantalla y deslizarla hacia afuera.
Si las vistas no se superponen, SlidingPaneLayout
admite el uso del diseño.
El parámetro layout_weight
en vistas secundarias para definir cómo dividir el espacio restante
una vez que se completa la medición. Este parámetro solo es relevante para el ancho.
En un dispositivo plegable que tenga espacio en la pantalla para mostrar ambas vistas una al lado de la otra
SlidingPaneLayout
ajusta automáticamente el tamaño de los dos paneles para
se posicionan a ambos lados de un pliegue o una bisagra superpuestos. En este
caso, los anchos establecidos se consideran el ancho mínimo que debe existir en cada
del lado del plegado. Si no hay espacio suficiente para mantenerla
tamaño mínimo, SlidingPaneLayout
vuelve a superponer las vistas.
El siguiente es un ejemplo de cómo usar un SlidingPaneLayout
que tiene un RecyclerView
como panel izquierdo y un FragmentContainerView
como vista principal de detalles para mostrar contenido del panel izquierdo:
<!-- 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>
En este ejemplo, el atributo android:name
en FragmentContainerView
agrega
el fragmento inicial al panel de detalles, lo que garantiza que los usuarios en pantallas grandes
Los dispositivos no ven un panel derecho vacío cuando la app se inicia por primera vez.
Cómo intercambiar el panel de detalles de manera programática
En el ejemplo de XML anterior, cuando se presiona un elemento en RecyclerView
activa un cambio en el panel de detalles. Cuando se usan fragmentos, se requiere un FragmentTransaction
que reemplace el panel derecho y llame a open()
en el SlidingPaneLayout
para cambiar al fragmento que se volvió visible:
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(); }
Este código específicamente no llama a
addToBackStack()
en el FragmentTransaction
. Esto evita que se compile una pila de actividades en el panel de detalles.
Implementación de componentes de Navigation
En los ejemplos de esta página, se usa directamente SlidingPaneLayout
y se requiere que
administrar transacciones de fragmentos manualmente. Sin embargo, el
El componente Navigation proporciona una implementación compilada previamente de
un diseño de doble panel mediante
AbstractListDetailFragment
:
Una clase de API que usa un SlidingPaneLayout
de forma interna para administrar tu lista
y los paneles de detalles.
Esto te permite simplificar la configuración de tu diseño XML. En lugar de declarar de manera explícita un SlidingPaneLayout
y ambos paneles, el diseño solo necesita un FragmentContainerView
para contener tu implementación 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>
Implementación
onCreateListPaneView()
y
onListPaneViewCreated()
para proporcionar una vista personalizada
para tu panel de lista. Para el panel de detalles,
AbstractListDetailFragment
usa un
NavHostFragment
Esto significa que puedes definir un gráfico de navegación que solo contenga los destinos que se mostrarán en el panel de detalles. Luego, puedes usar NavController
para cambiar el panel de detalles entre los destinos en el gráfico de navegación autónomo:
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(); }
Los destinos del gráfico de navegación del panel de detalles no deben estar presentes en
cualquier gráfico de navegación externo de toda la app. Sin embargo, los vínculos directos
gráfico de navegación del panel debe estar adjunto al destino que aloja al
SlidingPaneLayout
Esto ayuda a garantizar que los vínculos directos externos naveguen primero
al destino SlidingPaneLayout
y, luego, navega al detalle correcto
destino del panel.
Consulta la Ejemplo de TwoPaneFragment para obtener una implementación completa de un diseño de doble panel con el componente Navigation.
Cómo integrar con el botón Atrás del sistema
En dispositivos más pequeños, en los que los paneles de lista y detalles se superponen, asegúrate de que el sistema
cuando el botón Atrás lleve al usuario del panel de detalles al panel de lista. Hacer esto
al proporcionar una forma de pago personalizada
Navigation y conectar un
OnBackPressedCallback
para
el estado actual de 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); } }
Puedes agregar la devolución de llamada al
OnBackPressedDispatcher
mediante
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 bloqueado
SlidingPaneLayout
siempre te permite llamar de forma manual a open()
y
close()
hacer la transición entre los paneles de lista y de detalles en los teléfonos. Estos métodos no tienen
si ambos paneles están visibles y no se superponen.
Cuando se superponen los paneles de lista y de detalles, los usuarios pueden deslizar el dedo de forma predeterminada hacia cualquiera de ellos para alternar entre ambas direcciones, incluso cuando no usan la navegación por gestos. Para controlar la dirección del deslizamiento, configura el modo bloqueado del SlidingPaneLayout
:
Kotlin
binding.slidingPaneLayout.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED
Java
binding.getSlidingPaneLayout().setLockMode(SlidingPaneLayout.LOCK_MODE_LOCKED);
Más información
Para obtener más información sobre cómo crear diseños para diferentes factores de forma, consulta la la siguiente documentación:
- Descripción general de la compatibilidad de pantallas
- Cómo diseñar para diferentes factores de forma