창이 두 개인 레이아웃 만들기

Compose 방식 사용해 보기
Jetpack Compose는 Android에 권장되는 UI 도구 키트입니다. Compose에서 레이아웃을 사용하는 방법을 알아보세요.
<ph type="x-smartling-placeholder"></ph> 적응형 레이아웃 → 를 통해 개인정보처리방침을 정의할 수 있습니다.

앱의 모든 화면은 사용 가능한 공간에 반응하고 적응해야 합니다. 다음을 사용하여 반응형 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의 예를 보여주는 이미지
그림 1. 다음을 사용하여 만든 레이아웃의 예: 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>

이 예에서 FragmentContainerViewandroid: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() 목록 창에 맞춤 뷰를 제공합니다. 세부정보 창에서는 AbstractListDetailFragmentNavHostFragment 즉, 세부정보 창에 표시할 대상만 포함된 탐색 그래프를 정의할 수 있습니다. 그런 다음 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);

자세히 알아보기

다양한 폼 팩터를 위한 레이아웃 디자인에 관해 자세히 알아보려면 다음을 참고하세요. 다음 문서를 참조하세요.

추가 리소스