Try the Compose way
Jetpack Compose is the recommended UI toolkit for Android. Learn how to work with layouts in Compose.

Every screen in your app should be responsive and should adapt to the available space. Building a responsive UI with ConstraintLayout can allow a single pane approach to scale to many sizes, but larger devices might benefit from splitting the layout into multiple panes. For example, you might want a screen to show a list of items side by side with the details of the currently selected item.
The
SlidingPaneLayout
component supports showing two panes side by side on larger devices and
foldables, while automatically adapting to show only one pane at a time on
smaller devices such as phones.
For device-specific guidance, see the screen compatibility overview.
To use SlidingPaneLayout
, include the following dependency in your app's
build.gradle
file:
dependencies { implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0" }
dependencies { implementation("androidx.slidingpanelayout:slidingpanelayout:1.2.0") }
SlidingPaneLayout
provides a horizontal, two pane layout for use at the top
level of a UI. This layout uses the first pane as a content list or a browser,
subordinate to a primary detail view for displaying content in the other pane.
SlidingPaneLayout
uses the width of the two panes to determine whether to show
the panes side by side. For example, if the list pane is measured to have a
minimum size of 200dp and the detail pane needs 400dp, then the
SlidingPaneLayout
automatically shows the two panes side by side as long as
it has at least 600dp of width available.
Child views overlap if their combined width exceeds the available width in the
SlidingPaneLayout
. In this case, the child views expand to fill the available
width in the SlidingPaneLayout
. The user can slide the topmost view out of the
way by dragging it back from the edge of the screen.
If the views do not overlap, SlidingPaneLayout
supports the use of the layout
parameter layout_weight
on child views to define how to divide leftover space
after measurement is complete. This parameter is only relevant for width.
On a foldable device that has space on the screen to show both views side by
side, SlidingPaneLayout
automatically adjusts the size of the two panes so
that they are positioned on either side of an overlapping fold or hinge. In this
case, the widths set are considered the minimum width that must exist on each
side of the folding feature. If there is not enough space to maintain that
minimum size, SlidingPaneLayout
switches back to overlapping the views.
Here is an example of using a SlidingPaneLayout
that has a
RecyclerView
as its
left pane and a
FragmentContainerView
as its primary detail view to display content from the left pane:
<!-- 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>
In this example, the android:name
attribute on FragmentContainerView
adds
the initial fragment to the detail pane, ensuring that users on large-screen
devices do not see an empty right pane when the app first launches.
In the XML example above, tapping on an element in the RecyclerView
triggers
a change in the detail pane. When using fragments, this requires a
FragmentTransaction
that replaces the right pane, calling
open()
on the SlidingPaneLayout
to swap to the newly visible fragment:
// 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() }
// 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(); }
This code specifically does not call
addToBackStack()
on the FragmentTransaction
. This avoids building a back stack in the detail
pane.
The examples presented so far in this guide used SlidingPaneLayout
directly
and required you to manage fragment transactions manually. However, the
Navigation component provides a prebuilt implementation of
a two pane layout through
AbstractListDetailFragment
,
an API class that uses a SlidingPaneLayout
under the hood to manage your list
and detail panes.
This allows you to simplify your XML layout configuration. Instead of explicitly
declaring a SlidingPaneLayout
and both of your panes, your layout only needs a
FragmentContainerView
to hold your AbstractListDetailFragment
implementation:
<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>
In your implementation, you need to implement
onCreateListPaneView()
and
onListPaneViewCreated()
to provide a custom view for your list pane; but AbstractListDetailFragment
uses a
NavHostFragment
as
the detail pane. This means you can define a navigation
graph that only contains the
destinations to be shown in the detail pane. Then, you can use
NavController
to swap your
detail pane between the destinations in the self-contained navigation graph:
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 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() }
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 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(); }
The destinations in the detail pane's navigation graph should not be present
in any outer, app-wide navigation graph. However, any deep links within the
detail pane's navigation graph should be attached to the destination that hosts
the SlidingPaneLayout
. This ensures that external deep links first navigate to
the SlidingPaneLayout
destination and then navigate to the correct detail pane
destination.
See the example for a full implementation of a two pane layout using the Navigation component.
On smaller devices where the list and detail panes overlap, you should ensure
that the system back button takes the user from the detail pane back to the list
pane. Do this by providing custom back
navigation and connecting an
OnBackPressedCallback
to
the current state of the SlidingPaneLayout
:
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 } }
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); } }
Then, you can add the callback to the
OnBackPressedDispatcher
using
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)) // Setup the RecyclerView adapter, etc. } }
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. } }
SlidingPaneLayout
always allows you to manually call open()
and
close()
to transition between the list and detail panes on phones. These methods have no
effect if both panes are visible and do not overlap.
When the list and detail panes overlap, users can swipe in both directions by
default, freely switching between the two panes even when not using gesture
navigation. You can control the swipe direction
by setting the lock mode of the SlidingPaneLayout
:
binding.slidingPaneLayout.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED
binding.getSlidingPaneLayout().setLockMode(SlidingPaneLayout.LOCK_MODE_LOCKED);
To learn more about designing layouts for different form factors, see the following guides:
Content and code samples on this page are subject to the licenses described in the Content License. Java and OpenJDK are trademarks or registered trademarks of Oracle and/or its affiliates.
Last updated 2023-05-11 UTC.