앱의 모든 화면은 사용 가능한 공간에 반응하고 적응해야 합니다.
단일 창 접근 방식을 다양한 크기로 확장할 수 있는 ConstraintLayout
를 사용하여 반응형 UI를 빌드할 수 있지만 큰 기기에서는 레이아웃을 여러 창으로 분할하는 것이 좋습니다. 예를 들어 화면에서 선택한 항목의 세부정보 목록 옆에 항목 목록을 표시할 수 있습니다.
SlidingPaneLayout
구성요소는 대형 기기와 폴더블에서는 창 두 개를 나란히 표시하고 휴대전화와 같은 소형 기기에서는 창을 한 번에 한 개만 표시하도록 자동으로 조정할 수 있도록 지원합니다.
기기별 안내는 화면 호환성 개요를 참고하세요.
설정
SlidingPaneLayout
를 사용하려면 앱의 build.gradle
파일에 다음 종속 항목을 포함합니다.
Groovy
dependencies { implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0" }
Kotlin
dependencies { implementation("androidx.slidingpanelayout:slidingpanelayout:1.2.0") }
XML 레이아웃 구성
SlidingPaneLayout
는 UI의 최상위 수준에서 사용할 수 있는 두 개의 창이 있는 가로 레이아웃을 제공합니다. 이 레이아웃은 첫 번째 창을 콘텐츠 목록 또는 브라우저로 사용합니다. 이 창은 다른 창에 콘텐츠를 표시하는 기본 세부정보 뷰에 종속됩니다.
![SlidingPaneLayout의 예를 보여주는 이미지](https://developer.android.google.cn/static/images/ui/slidingpanelayout-example.png?authuser=3&hl=ko)
SlidingPaneLayout
로 만든 레이아웃의 예
SlidingPaneLayout
은 두 창의 너비를 감안해 창을 나란히 표시할지 결정합니다. 예를 들어 목록 창이 최소 크기가 200dp로 측정되고 세부정보 창에 400dp가 필요한 경우 SlidingPaneLayout
는 사용 가능한 너비가 600dp 이상이면 두 창을 자동으로 나란히 표시합니다.
합산 너비가 SlidingPaneLayout
에 사용 가능한 너비를 초과하는 경우 하위 뷰가 겹칩니다. 이 경우 하위 뷰는 SlidingPaneLayout
에 사용 가능한 너비를 채우도록 확장됩니다. 사용자는 최상단 뷰를 화면 가장자리에서부터 다시 드래그하여 화면 밖으로 슬라이드할 수 있습니다.
뷰가 겹치지 않는 경우 SlidingPaneLayout
는 하위 뷰에 레이아웃 매개변수 layout_weight
를 사용하여 측정이 완료된 후 남은 공간을 나누는 방법을 정의할 수 있도록 지원합니다. 이 매개변수는 너비에만 적용됩니다.
화면에 두 뷰를 나란히 표시할 공간이 있는 폴더블 기기에서 SlidingPaneLayout
는 겹치는 접힘 또는 힌지의 한쪽에 배치되도록 두 창의 크기를 자동으로 조정합니다. 이 경우 설정된 너비는 접기 기능의 양쪽에 있어야 하는 최소 너비로 간주됩니다. 이 최소 크기를 유지하기에 공간이 충분하지 않으면 SlidingPaneLayout
는 뷰를 겹치도록 다시 전환됩니다.
다음은 SlidingPaneLayout
을 사용하는 예입니다. 이 레이아웃에서는 RecyclerView
가 왼쪽 창이고, FragmentContainerView
가 왼쪽 창의 콘텐츠를 표시하는 기본 세부정보 뷰입니다.
<!-- 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>
이 예에서 FragmentContainerView
의 android:name
속성은 초기 프래그먼트를 세부정보 창에 추가하여 앱이 처음 실행될 때 대형 화면 기기의 사용자에게 빈 오른쪽 창이 표시되지 않도록 합니다.
프로그래매틱 방식으로 세부정보 창 바꾸기
이전 XML 예에서 RecyclerView
의 요소를 탭하면 세부정보 창이 변경됩니다. 프래그먼트를 사용하는 경우에는 오른쪽 창을 바꿀 FragmentTransaction
이 필요합니다. 이 요소는 SlidingPaneLayout
에서 open()
를 호출하여 새로 표시되는 프래그먼트로 교체됩니다.
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(); }
이 코드는 특히 FragmentTransaction
에서 addToBackStack()
를 호출하지 않습니다. 이는 세부정보 창에 백 스택을 빌드하는 것을 방지합니다.
탐색 구성요소 구현
이 페이지의 예에서는 SlidingPaneLayout
를 직접 사용하며 프래그먼트 트랜잭션을 수동으로 관리해야 합니다. 그러나 탐색 구성요소는 AbstractListDetailFragment
를 통해 창 두 개 레이아웃의 사전 빌드된 구현을 제공합니다. 이 API 클래스는 내부적으로 SlidingPaneLayout
를 사용하여 목록 창과 세부정보 창을 관리합니다.
이를 통해 XML 레이아웃 구성을 단순화할 수 있습니다. SlidingPaneLayout
및 창 두 개를 명시적으로 선언하는 대신, 레이아웃에는 AbstractListDetailFragment
구현만 보유하는 FragmentContainerView
만 있으면 됩니다.
<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>
onCreateListPaneView()
및 onListPaneViewCreated()
를 구현하여 목록 창에 맞춤 뷰를 제공합니다. 세부정보 창의 경우 AbstractListDetailFragment
는 NavHostFragment
를 사용합니다.
즉, 세부정보 창에 표시할 대상만 포함된 탐색 그래프를 정의할 수 있습니다. 그런 다음 NavController
를 사용하여 독립된 탐색 그래프의 대상 간에 세부정보 창을 전환할 수 있습니다.
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(); }
세부정보 창의 탐색 그래프에 있는 대상은 외부 앱 전체 탐색 그래프에 있으면 안 됩니다. 그러나 세부정보 창의 탐색 그래프 내에 있는 모든 딥 링크는 SlidingPaneLayout
를 호스팅하는 대상에 연결되어야 합니다. 이렇게 하면 외부 딥 링크가 먼저 SlidingPaneLayout
대상으로 이동한 다음 올바른 세부정보 창 대상으로 이동할 수 있습니다.
탐색 구성요소를 사용하여 창이 두 개인 레이아웃을 완전히 구현하려면 TwoPaneFragment 예를 참고하세요.
시스템 뒤로 버튼과 통합
목록 창과 세부정보 창이 겹치는 소형 기기에서는 시스템 뒤로 버튼을 누르면 세부정보 창에서 목록 창으로 다시 이동해야 합니다. 이렇게 하려면 맞춤 뒤로 탐색을 제공하고 OnBackPressedCallback
를 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); } }
addCallback()
를 사용하여 OnBackPressedDispatcher
에 콜백을 추가할 수 있습니다.
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. } }
잠금 모드
SlidingPaneLayout
를 사용하면 항상 open()
및 close()
를 수동으로 호출하여 휴대전화에서 목록 창과 세부정보 창 간에 전환할 수 있습니다. 두 창이 모두 표시되고 겹치지 않으면 이러한 메서드는 아무런 영향을 주지 않습니다.
목록 창과 세부정보 창이 겹치는 경우 사용자는 기본적으로 양방향으로 스와이프할 수 있고 동작 탐색을 사용하지 않더라도 두 창 간에 자유롭게 전환할 수 있습니다. 스와이프 방향은 SlidingPaneLayout
의 잠금 모드를 설정하여 제어할 수 있습니다.
Kotlin
binding.slidingPaneLayout.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED
Java
binding.getSlidingPaneLayout().setLockMode(SlidingPaneLayout.LOCK_MODE_LOCKED);
자세히 알아보기
다양한 폼 팩터의 레이아웃 디자인에 관한 자세한 내용은 다음 문서를 참고하세요.
추가 리소스
- 적응형 레이아웃 Codelab
- GitHub의 SlidingPaneLayout 예