앱의 모든 화면은 사용 가능한 공간에 반응하고 적응해야 합니다.
다음을 사용하여 반응형 UI를 빌드할 수 있습니다.
단일 창을 허용하는 ConstraintLayout
접근 방식은 다양한 크기로 확장되지만, 더 큰 기기에서는 분할을 통해 이점을 얻을 수 있습니다
레이아웃을 여러 창으로 할 수 있습니다. 예를 들어 화면에
항목 목록을 클릭합니다.
이
SlidingPaneLayout
드림
구성요소를 사용하면 대형 기기에서 두 개의 창을 나란히 표시하고
폴더블 기기에서 한 번에 창 하나만 표시하도록 자동 조정
더 작은 장치입니다.
기기별 안내는 다음을 참고하세요. 화면 호환성 개요를 참고하세요.
설정
SlidingPaneLayout
를 사용하려면 앱의 build.gradle
파일에 다음 종속 항목을 포함합니다.
Groovy
dependencies { implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0" }
Kotlin
dependencies { implementation("androidx.slidingpanelayout:slidingpanelayout:1.2.0") }
XML 레이아웃 구성
SlidingPaneLayout
는 상단에서 사용할 수 있는 두 개의 창 레이아웃을 제공합니다.
지정할 수 있습니다. 이 레이아웃은 첫 번째 창을 콘텐츠 목록 또는 브라우저로 사용합니다. 이 창은 다른 창에 콘텐츠를 표시하는 기본 세부정보 뷰에 종속됩니다.
![SlidingPaneLayout의 예를 보여주는 이미지](https://developer.android.com/static/images/ui/slidingpanelayout-example.png?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() }
자바
// 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(); }
이 코드는 특히
addToBackStack()
드림
(FragmentTransaction
) 이는 세부정보 창에 백 스택을 빌드하는 것을 방지합니다.
탐색 구성요소 구현
이 페이지의 예에서는 SlidingPaneLayout
를 직접 사용하며
프래그먼트 트랜잭션을 수동으로 관리해야 합니다. 그러나
탐색 구성요소는
두 개의 창으로 구성된 레이아웃을 통해
AbstractListDetailFragment
님,
내부적으로 SlidingPaneLayout
를 사용하여 목록을 관리하는 API 클래스
세부정보 창을 제공합니다
이를 통해 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() }
자바
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
대상으로 이동한 다음 올바른 세부정보로 이동합니다.
페인(pane) 대상입니다.
자세한 내용은 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 } }
자바
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); } }
콜백을
OnBackPressedDispatcher
드림
사용
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. } }
자바
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
자바
binding.getSlidingPaneLayout().setLockMode(SlidingPaneLayout.LOCK_MODE_LOCKED);
자세히 알아보기
다양한 폼 팩터를 위한 레이아웃 디자인에 관해 자세히 알아보려면 다음을 참고하세요. 다음 문서를 참조하세요.
추가 리소스
- 적응형 레이아웃 Codelab
- SlidingPaneLayout 예 를 참조하세요.