적응형 레이아웃

1. 시작하기 전에

Android 기기는 다양한 모양과 크기, 폼 팩터로 출시됩니다. 앱을 설계할 때는 소형 화면 기기에서 대형 화면 기기에 이르기까지 다양한 유형의 기기에서 실행되도록 해야 합니다. 프로덕션 환경에서 사용 가능한 앱을 작성하는 개발자는 Android Wear, Android Auto, Android TV를 지원할 수 있지만 이러한 주제는 이 과정에서 다루지 않습니다. 앱에서 다양한 화면을 지원하면 최대한 많은 사용자가 다양한 기기에서 앱을 사용하도록 할 수 있습니다.

앱은 레이아웃이 유연해야 합니다. 특정 가로세로 비율과 화면 크기를 가정하는 고정 크기로 레이아웃을 정의하는 대신, 다양한 화면 크기와 방향에 적절하게 맞출 수 있는 레이아웃이어야 합니다. 앱 실행 도중 화면 크기와 가로세로 비율이 변경될 수 있는 폴더블 기기에서 앱이 실행되는 경우에도 동일한 원칙이 적용됩니다. 이 Codelab 끝에서 폴더블 기기에 관해 간단히 소개합니다.

aecb59fc49fb4abf.png

기본 요건

  • Android 스튜디오에 코드를 다운로드하고 실행하는 방법
  • Android 아키텍처 구성요소 ViewModelLiveData 숙지
  • 탐색 구성요소에 관한 기본 지식

학습할 내용

  • 앱에 SlidingPaneLayout을 추가하는 방법

빌드할 항목

  • Sports 앱을 큰 화면에 맞도록 업데이트합니다.

필요한 항목

  • Android 스튜디오가 설치된 컴퓨터
  • Sports 앱의 시작 코드

이 Codelab의 시작 코드 다운로드

이 Codelab은 시작 코드를 제공합니다. 이 Codelab에서 학습한 기능을 사용하여 시작 코드를 확장할 수 있습니다. 시작 코드에는 이전 Codelab을 통해 익숙한 코드와 이후 Codelab에서 학습할 익숙하지 않은 코드가 포함되어 있을 수 있습니다.

GitHub에서 이 Codelab의 코드를 가져와 Android 스튜디오에서 열려면 다음을 실행합니다.

  1. Android 스튜디오를 시작합니다.
  2. Welcome to Android Studio 창에서 Get from VCS를 클릭합니다.

61c42d01719e5b6d.png

  1. Get from Version Control 대화상자에서 Version controlGit이 선택되어 있는지 확인합니다.

9284cfbe17219bbb.png

  1. 제공된 코드 URL을 URL 상자에 붙여넣습니다.
  2. 필요한 경우 Directory를 제안된 기본값과 다른 것으로 변경합니다.

5ddca7dd0d914255.png

  1. Clone을 클릭합니다. Android 스튜디오에서 코드를 가져오기 시작합니다.
  2. Android 스튜디오가 열릴 때까지 기다립니다.
  3. Codelab 시작 코드나 앱, 솔루션 코드에 적합한 모듈을 선택합니다.

2919fe3e0c79d762.png

  1. Run 버튼 8de56cba7583251f.png을 클릭하여 코드를 빌드하고 실행합니다.

2. 코드 따라 하기 동영상 시청(선택사항)

교육 과정 강사가 Codelab을 완료하는 모습을 보려면 아래 동영상을 재생하세요.

동영상을 전체 화면으로 펼쳐(동영상 하단의 오른쪽 모서리에 있는 아이콘 이 기호는 전체 화면 모드를 나타내기 위해 정사각형의 네 개 모서리가 강조 표시되어 있음을 보여줍니다. 사용) Android 스튜디오와 코드를 더 선명하게 보는 것이 좋습니다.

이 단계는 선택사항입니다. 이 동영상을 건너뛰고 Codelab 안내를 바로 시작할 수도 있습니다.

3. 시작 앱 개요

Sports 앱은 두 화면으로 구성됩니다. 첫 번째 화면에는 스포츠 목록이 표시됩니다. 사용자가 특정 스포츠 항목을 선택하면 두 번째 화면이 표시됩니다. 두 번째 화면은 선택한 스포츠 뉴스를 표시하는 세부정보 화면입니다. 세부정보 화면에는 구현을 간소화하기 위한 자리표시자 텍스트가 표시됩니다.

시작 코드 둘러보기

다운로드한 시작 코드에는 목록 화면과 세부정보 화면 레이아웃이 미리 디자인되어 있습니다. 이 과정에서는 앱이 대형 화면에 맞게 조정되도록 만드는 데에만 중점을 둡니다. 대형 화면을 이용하기 위해 SlidingPaneLayout을 사용하게 됩니다. 시작하는 데 도움이 되는 다음과 같은 몇 가지 파일을 둘러보겠습니다.

fragment_sports_list.xml

  • Design 뷰에서 res/layout/fragment_sports_list.xml을 엽니다.
  • 여기에는 앱의 첫 번째 화면인 스포츠 목록의 레이아웃이 포함되어 있습니다.
  • 이 레이아웃은 스포츠 뉴스 목록을 표시하는 Recyclerview로 구성됩니다.

f50d3e7b41fcb338.png

d9af155f87ddbcdf.png

sports_list_item.xml

  • Design 뷰에서 res/layout/sports_list_item.xml을 엽니다.
  • 여기에는 Recyclerview에 있는 각 항목의 레이아웃이 포함되어 있습니다.
  • 이 레이아웃은 스포츠의 썸네일 이미지와 뉴스 제목 그리고 간단한 스포츠 뉴스의 자리표시자 텍스트로 구성됩니다.

b19fd0e779c1d7c3.png

fragment_sports_news.xml

  • Design 뷰에서 res/layout/fragment_sports_news.xml을 엽니다.
  • 여기에는 앱의 두 번째 화면 레이아웃이 포함되어 있습니다. 이 화면은 사용자가 Recyclerview에서 스포츠를 선택하면 표시됩니다.
  • 이 레이아웃은 스포츠 이미지 배너와 스포츠 뉴스의 자리표시자 텍스트로 구성됩니다.

c2073b1752342d97.png

main_activity.xml 및 content_main.xml

이 두 요소는 단일 프래그먼트가 있는 기본 활동 레이아웃을 정의합니다.

탐색 그래프에는 두 개의 대상이 포함되어 있습니다. 하나는 스포츠 목록을 위한 것이고 다른 하나는 스포츠 뉴스를 위한 것입니다.

res/values 폴더

이 폴더의 리소스 파일에 관해서는 잘 알고 있을 것입니다.

  • colors.xml에는 앱에서 사용되는 테마 색상이 있습니다.
  • strings.xml에는 앱에 필요한 모든 문자열이 있습니다.
  • themes.xml에는 앱의 UI 맞춤설정이 있습니다.

MainActivity.kt

여기에는 활동의 콘텐츠 뷰를 main_activity.xml로 설정하기 위한 기본 템플릿 생성 코드가 있습니다. onSupportNavigateUp() 메서드는 앱 바에서 위로 탐색하는 기본 동작을 처리하도록 재정의됩니다.

model/Sport.kt

스포츠 목록 Recyclerview의 각 행에 표시할 데이터를 보유한 데이터 클래스입니다.

data/SportsData.kt

이 파일에는 getSportsData()라는 함수가 포함되어 있습니다. 하드코딩된 스포츠 데이터로 미리 채워진 ArrayList를 반환하는 함수입니다.

SportsViewModel.kt

앱의 공유된 ViewModel입니다. ViewModel은 스포츠 목록을 포함한 첫 번째 화면 SportsListFragment와 자세한 스포츠 뉴스를 포함한 두 번째 화면 NewsDetailsFragment에 의해 공유됩니다.

  • _currentSport 속성은 MutableLiveData, 유형으로, 사용자가 선택한 현재 스포츠를 저장합니다. currentSport 속성은 _currentSport에 관한 지원 속성으로, 다른 클래스의 읽기 전용 공개 버전으로 노출됩니다.
  • _sportsData 속성에는 스포츠 데이터의 목록이 포함되어 있습니다. 이전 속성과 마찬가지로 sportsData는 이 속성의 읽기 전용 공개 버전입니다.
  • 이니셜라이저 init{} 블록은 _currentSport_sportsData를 초기화합니다. _sportsDatadata/SportsData.kt의 전체 스포츠 목록으로 초기화됩니다. _currentSport는 목록의 첫 번째 항목으로 초기화됩니다.
  • updateCurrentSport() 함수는 Sports 인스턴스를 가져와 전달된 값으로 _currentSport를 업데이트합니다.

SportsAdapter.kt

RecyclerView의 어댑터입니다. 생성자에서 클릭 리스너가 전달됩니다. 이 파일의 코드는 대부분 이전 Codelab에서 익숙한 상용구 코드입니다.

SportsListFragment.kt

스포츠 목록이 표시되는 첫 번째 화면 프래그먼트입니다.

  • onCreateView() 함수는 결합 객체를 사용하여 fragment_sports_list 레이아웃 XML을 확장합니다.
  • onViewCreated() 함수는 RecyclerView 어댑터를 설정합니다. 그리고 사용자가 선택한 스포츠를 공유된 ViewModelSportsViewModel의 현재 스포츠로 업데이트합니다. 또한 스포츠 뉴스가 포함된 세부정보 화면으로 이동하고 submitList(List)를 사용하여 표시할 스포츠 목록을 어댑터에 제출합니다.

NewsDetailsFragment.kt

앱의 두 번째 화면으로, 스포츠 뉴스의 자리표시자 텍스트가 표시되는 곳입니다.

  • onCreateView() 함수는 결합 객체를 사용하여 fragment_sports_news 레이아웃 XML을 확장합니다.
  • onViewCreated() 함수는 데이터가 변경되면 UI를 자동으로 업데이트하도록 SportsViewModel의 속성인 currentSport에 관찰자를 연결합니다. 관찰자 내에서 스포츠 제목, 이미지, 뉴스가 업데이트됩니다.

앱 빌드 및 실행

  1. 에뮬레이터 또는 기기에서 앱을 빌드하고 실행합니다. 스포츠 목록에서 항목을 선택하면 앱이 뉴스 자리표시자 텍스트가 있는 두 번째 화면으로 이동합니다.

4. 목록-세부정보 패턴

현재 시작 앱은 태블릿 같은 큰 기기의 화면 공간을 충분히 활용하지 못합니다. 이 문제를 해결하기 위해 이 Codelab에서 배울 목록-세부정보 패턴을 사용하여 앱 UI를 표시할 것입니다.

태블릿에서 앱 실행

이 작업에서는 태블릿 프로필을 사용하는 에뮬레이터를 만듭니다. 에뮬레이터가 생성되면 Sports 앱 시작 코드를 실행하고 UI를 살펴봅니다.

  1. Android 스튜디오에서 Tools > AVD Manager로 이동합니다.
  2. Android Virtual Device Manager 창이 표시됩니다. 하단에 표시되는 + Create New Virtual Device...를 클릭합니다.
  3. Virtual Device Configuration 창이 표시됩니다. 여기에서 에뮬레이터 하드웨어와 OS를 구성하게 됩니다. 왼쪽 창에서 Tablet을 클릭합니다. 가운데 창에서 Pixel C 또는 기타 유사한 하드웨어 프로필을 선택합니다.

8303f9b3e70321eb.png

  1. Next를 클릭합니다.
  2. 최신 시스템 이미지를 선택합니다. 이 Codelab 작성 시점의 최신 버전은 R(API 수준 30)입니다.
  3. Next를 클릭합니다.
  4. 이제 가상 기기의 이름을 바꿀 수 있습니다(선택사항).
  5. Finish를 클릭합니다.
  6. Android Virtual Device Manager 창으로 다시 이동합니다. 새로 만든 가상 기기 옆에 있는 시작 아이콘 38752506de85d293.png을 클릭합니다.
  7. 태블릿 프로필을 사용하는 에뮬레이터가 실행됩니다. 이 작업에는 다소 시간이 걸릴 수 있으니 기다려 주시기 바랍니다.
  8. Android Virtual Device Manager 창을 닫습니다.
  9. 새로 만든 에뮬레이터에서 Sports 앱을 실행합니다.

200e209de7a2f0ad.png

앱이 큰 기기의 전체 화면을 제대로 활용하지 못함을 알 수 있습니다. 큰 화면에서는 목록-세부정보가 목록보다 더 효율적입니다. 항목-세부정보 패턴(마스터-세부정보 패턴이라고도 함)은 레이아웃의 한쪽에 항목 목록을 표시합니다. 항목을 탭하면 항목 옆에 세부정보가 표시됩니다. 일반적으로 이러한 보기는 태블릿 같은 큰 화면에만 표시됩니다. 큰 화면의 공간이 더 넓어 콘텐츠를 더 많이 표시할 수 있기 때문입니다.

다음 이미지는 목록-세부정보 패턴의 예입니다.

71698910dd129a91.png

위의 목록-세부정보 패턴에서 왼쪽에는 항목 목록이, 오른쪽에는 선택한 항목의 세부정보가 표시됩니다.

마찬가지로 sports 앱에서 위 패턴을 사용할 경우 뉴스 프래그먼트가 세부정보 화면이 됩니다.

51c9542717d2f875.png

이 Codelab에서는 SlidingPaneLayout을 사용하여 목록-세부정보 UI를 구현하는 방법을 알아봅니다.

5. SlidingPaneLayout 패턴

목록-세부정보 UI는 화면 크기에 따라 다르게 작동해야 할 수 있습니다. 대형 디스플레이에는 목록 창과 세부정보 창을 나란히 배치할 수 있는 충분한 공간이 있습니다. 목록 항목을 클릭하면 세부정보 창에 세부정보가 표시됩니다. 하지만 작은 화면에서는 이들이 복잡하게 보입니다. 한 번에 창 두 개를 표시하는 대신 한 번에 창 하나씩 표시하는 것이 더 좋습니다. 처음에는 화면이 목록 창으로 채워집니다. 항목을 탭하면 목록 창이 그 항목의 세부정보 창으로 바뀌고, 화면도 세부정보 창으로 채워집니다.

현재 화면 크기에 따라 적절한 사용자 환경을 선택하기 위해 SlidingPaneLayout을 사용하여 로직을 관리하는 방법을 알아봅니다.

b0a205de3494e95d.gif

작은 화면에서 세부정보 창이 어떻게 목록 창 위로 슬라이드되는지 알 수 있습니다.

다음은 SlidingPaneLayout이 작은 화면에 어떻게 표시되는지 보여주는 이미지입니다. 목록에서 항목이 선택되면 세부정보 창이 목록 창과 어떻게 겹치는지 관찰합니다. 실제로 두 창이 항상 표시됩니다.

e26f94d9579b6121.png

471b0b38d4dfa95a.png

따라서 SlidingPaneLayout을 사용하면 대형 기기에서는 창 두 개를 나란히 표시하고 휴대전화와 같은 소형 기기에서는 창을 한 번에 한 개만 표시하도록 자동 조정할 수 있습니다.

6. 라이브러리 종속 항목 추가

  1. build.gradle (Module: Sports.app)을 엽니다.
  2. 앱에서 SlidingPaneLayout을 사용하려면 dependencies 섹션에서 다음 종속 항목을 포함합니다.
dependencies {
...
    implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0-beta01"
}

7. 스포츠 목록 프래그먼트 xml 구성

이 작업에서는 fragment_sports_list의 루트 레이아웃을 SlidingPaneLayout으로 변환합니다. 이미 알아본 것처럼, SlidingPaneLayout은 UI의 최상위 수준에서 사용할 수 있는 두 개의 가로 창 레이아웃을 제공합니다. 이 레이아웃은 첫 번째 창을 콘텐츠 목록 또는 브라우저로 사용합니다. 이 창은 다른 창에 콘텐츠를 표시하는 기본 세부정보 뷰에 종속됩니다.

Sports 앱에서 첫 번째 창은 스포츠 목록을 표시하는 RecyclerView이고 두 번째 창은 스포츠 뉴스를 표시합니다.

SlidingPaneLayout 추가

  1. fragment_sports_list.xml을 엽니다. 루트 레이아웃이 FrameLayout임을 알 수 있습니다.
  2. FrameLayoutandroidx.slidingpanelayout.widget.SlidingPaneLayout.으로 변경합니다.
<androidx.slidingpanelayout.widget.SlidingPaneLayout
   xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".SportsListFragment">

   <androidx.recyclerview.widget.RecyclerView...>
</androidx.slidingpanelayout.widget.SlidingPaneLayout>
  1. android:id 속성을 SlidingPaneLayout에 추가하고 그 항목에 @+id/sliding_pane_layout의 값을 지정합니다.
<androidx.slidingpanelayout.widget.SlidingPaneLayout
   ...
   android:id="@+id/sliding_pane_layout"
   ...>

SlidingPaneLayout에 두 번째 창 추가

이 작업에서는 SlidingPaneLayout에 두 번째 하위 요소를 추가합니다. 이 요소는 오른쪽 콘텐츠 창으로 표시됩니다.

  1. fragment_sports_list.xmlRecyclerView 아래에 두 번째 하위 요소인 androidx.fragment.app.FragmentContainerView를 추가합니다.
  2. 필수 속성 layout_heightlayout_widthFragmentContainerView에 추가합니다. 두 속성에 match_parent의 값을 지정합니다. 이 값은 나중에 업데이트합니다.
<androidx.fragment.app.FragmentContainerView
   android:layout_height="match_parent"
   android:layout_width="match_parent"/>
  1. android:id 속성을 FragmentContainerView에 추가하고 그 항목에 @+id/detail_container의 값을 지정합니다.
android:id="@+id/detail_container"
  1. android:name 속성을 사용하여 FragmentContainerViewNewsDetailsFragment를 추가합니다.
android:name="com.example.android.sports.NewsDetailsFragment"

layout_width 속성 업데이트

SlidingPaneLayout은 두 창의 너비를 감안해 창을 나란히 표시할지 결정합니다. 예를 들어 목록 창이 최소 크기가 300dp로 측정되고 세부정보 창에 400dp가 필요한 경우 SlidingPaneLayout은 최소 700dp의 너비를 사용할 수 있다면 두 창을 자동으로 나란히 표시합니다.

합산 너비가 SlidingPaneLayout에 사용 가능한 너비를 초과하는 경우 하위 뷰가 겹칩니다. 이 경우 하위 뷰는 SlidingPaneLayout에 사용 가능한 너비를 채우도록 확장됩니다.

하위 뷰의 너비를 확인하려면 기기 화면 너비에 관한 몇 가지 기본 정보를 알아야 합니다. 다음 표에는 크기 조절이 가능한 애플리케이션 레이아웃을 설계하고 개발하며 테스트할 수 있는 체계적인 중단점 목록이 나와 있습니다. 특히 고유의 사례에 맞춰 앱을 최적화하도록 레이아웃의 단순성과 유연성의 균형을 이루기 위해 이러한 클래스를 선택했습니다.

너비

중단점

기기 표현

좁은 너비

600dp 미만

세로 모드 휴대전화의 99.96%

중간 너비

600dp 이상

세로 모드 태블릿 중 93.73% 세로 모드의 펼친 대형 내부 디스플레이

확장 후 너비

840dp 이상

가로 모드 태블릿 중 97.22% 가로 모드의 펼친 대형 내부 디스플레이

a247a843310d061a.png

너비가 600dp 미만인 기기에서 Sports 앱을 사용할 경우 휴대전화에는 단일 창, 즉 스포츠 목록 창만 표시하는 것이 좋습니다. 태블릿에 두 창을 모두 표시하려면 합산 너비가 840dp보다 커야 합니다. 첫 번째 하위 요소 recycler 뷰에는 너비 550dp를, 두 번째 하위 요소 FragmentContainerView에는 300dp를 사용할 수 있습니다.

  1. fragment_sports_list.xml에서 RecyclerView의 레이아웃 너비를 550dp로 변경하고 FragmentContainerView의 레이아웃 너비를 300dp로 변경합니다.
<androidx.recyclerview.widget.RecyclerView
   ...
   android:layout_width="550dp"
   .../>

<androidx.fragment.app.FragmentContainerView
   ...
   android:layout_width="300dp"
   .../>
  1. 태블릿 프로필을 사용하는 에뮬레이터와 휴대전화 프로필을 사용하는 에뮬레이터에서 앱을 각각 실행합니다.

ad148a96d7487e66.png

태블릿에서는 두 창이 표시됩니다. 이후 단계에서 태블릿의 두 번째 창의 너비를 수정할 것입니다.

  1. 휴대전화 프로필을 사용하는 에뮬레이터에서 앱을 실행합니다.

a6be6d199d2975ac.png

layout_weight 추가

이 작업에서는 두 번째 창이 남은 전체 공간을 차지하도록 태블릿의 UI를 수정합니다.

SlidingPaneLayout을 사용하면 뷰가 겹치지 않는 경우에는 하위 뷰에 레이아웃 매개변수 layout_weight를 사용하여 측정 완료 후 남은 공간을 어떻게 분할할지 정의할 수 있습니다. 이 매개변수는 너비에만 적용됩니다.

  1. fragment_sports_list.xml에서 layout_weightFragmentContainerView에 추가하고 값을 1로 지정합니다. 이제 목록 창이 측정된 후 남은 공간을 채우도록 두 번째 창이 확장됩니다.
android:layout_weight="1"
  1. 앱을 실행합니다.

ce3a93fe501ee5dc.png

축하합니다. SlidingPaneLayout을 추가했습니다. 하지만 아직 완료되지는 않았습니다. 뒤로 탐색을 구현하고, 목록에서 항목이 선택되면 두 번째 창을 업데이트해야 합니다. 이러한 작업은 이후 작업에서 구현하게 됩니다.

8. 세부정보 창 변경

에뮬레이터에서 태블릿 프로필을 사용하여 앱을 실행합니다. 스포츠 목록에서 목록 항목을 선택합니다. 앱이 세부정보 창으로 이동하는 것을 알 수 있습니다.

8fedee8d4837909.png

이 작업에서는 이 문제를 해결할 것입니다. 현재 이중 창 콘텐츠가 선택된 스포츠로 업데이트된 다음, 앱이 NewsDetailsFragment로 이동합니다.

  1. SportsListFragment 파일의 onViewCreated() 함수에서 세부정보 화면으로 이동하는 다음 줄을 찾습니다.
// Navigate to the details screen
val action = SportsListFragmentDirections.actionSportsListFragmentToNewsFragment()
this.findNavController().navigate(action)
  1. 위의 줄을 아래 코드로 바꿉니다.
binding.slidingPaneLayout.openPane()

SlidingPaneLayout에서 openPane()을 호출하여 두 번째 창을 첫 번째 창으로 전환합니다. 태블릿 같이 두 창이 모두 표시되는 경우에는 이렇게 해도 가시적인 영향이 없습니다.

  1. 태블릿 및 휴대전화 에뮬레이터에서 앱을 실행합니다. 이중 창 콘텐츠가 올바르게 업데이트되는 것을 확인할 수 있습니다.

b0d3c8c263be15f8.png

이제 다음 작업에서는 앱에 맞춤 뒤로 탐색 기능을 추가합니다.

9. 맞춤 뒤로 탐색 추가

목록 창과 세부정보 창이 겹치는 소형 기기에서는 시스템 뒤로 버튼을 누르면 세부정보 창에서 목록 창으로 다시 이동해야 합니다. 이렇게 하려면 맞춤 뒤로 탐색을 제공하고 OnBackPressedCallbackSlidingPaneLayout의 현재 상태에 연결하면 됩니다.

뒤로 탐색

뒤로 탐색 기능은 사용자가 이전에 방문한 화면 기록을 통해 뒤로 이동하는 기능입니다. 모든 Android 기기는 이 같은 유형의 탐색을 위해 뒤로 버튼을 제공합니다. 사용자의 Android 기기에 따라 이 버튼은 물리적 버튼 또는 소프트웨어 버튼이 될 수 있습니다.

맞춤 뒤로 탐색

Android는 사용자가 애플리케이션을 탐색할 때 대상의 백 스택을 유지합니다. 이를 통해 일반적으로 Android를 사용하면 뒤로 버튼을 누를 때 이전 대상으로 적절하게 이동할 수 있습니다. 하지만 최상의 사용자 환경을 제공하기 위해 앱에서 뒤로 이동하는 동작을 자체적으로 구현해야 하는 경우도 있습니다.

예를 들어, Chrome 브라우저 같은 WebView를 사용할 때는 기본 뒤로 버튼 동작을 재정의하여 사용자에게 앱의 이전 화면 대신 웹 방문 기록을 통해 뒤로 이동하도록 하는 것이 좋습니다.

마찬가지로 SlidingPaneLayout에 맞춤 뒤로 탐색을 제공하고 세부정보 창에서 목록 창으로 다시 앱을 이동해야 합니다.

맞춤 뒤로 탐색 구현

Sports 앱에서 맞춤 뒤로 탐색을 구현하려면 다음 작업이 필요합니다.

  • 뒤로 키 누름을 처리하는 맞춤 콜백을 정의합니다. 이 콜백이 OnBackPressedCallback보다 우선 적용됩니다.
  • 콜백 인스턴스를 등록하고 추가합니다.

먼저 맞춤 콜백을 정의합니다.

  1. SportsListFragment 파일의 SportsListFragment 클래스 정의 아래에 새 클래스를 추가합니다. 그 이름을 SportsListOnBackPressedCallback으로 지정합니다.
  2. SlidingPaneLayoutprivate 인스턴스를 생성자 매개변수로 전달합니다.
class SportsListOnBackPressedCallback(
   private val slidingPaneLayout: SlidingPaneLayout
)
  1. OnBackPressedCallback에서 클래스를 확장합니다. OnBackPressedCallback 클래스는 onBackPressed 콜백을 처리합니다. 곧 생성자 매개변수 오류를 수정합니다.
class SportsListOnBackPressedCallback(
   private val slidingPaneLayout: SlidingPaneLayout
): OnBackPressedCallback()

OnBackPressedCallback의 생성자는 초기 사용 설정 상태를 나타내는 부울 값을 사용합니다. 콜백이 사용 설정된 때만(즉, isEnabled()가 true를 반환) 디스패처가 콜백의 handleOnBackPressed()를 호출하여 뒤로 버튼 이벤트를 처리합니다.

  1. slidingPaneLayout.isSlideable* && slidingPaneLayout.isOpen*을 생성자 매개변수로 OnBackPressedCallback에 전달합니다. (소형 화면에서) 두 번째 창이 슬라이드 가능하고 단일 창이 표시되는 경우에만 부울 isSlideable이 true입니다. 두 번째 창 즉, 콘텐츠 창이 완전히 열리는 경우 isOpen의 값은 true입니다.
class SportsListOnBackPressedCallback(
   private val slidingPaneLayout: SlidingPaneLayout
): OnBackPressedCallback(slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen)

이 코드는 소형 화면 기기에서 콘텐츠 창이 열려 있을 때만 콜백이 사용 설정되도록 해 줍니다.

  1. 구현되지 않은 메서드에 관한 오류를 해결하려면 빨간색 전구 5fdf362480bfe665.png를 클릭하고 Implement members를 선택합니다.
  2. Implement members 팝업에서 ok를 클릭하여 handleOnBackPressed 메서드를 재정의합니다.

클래스는 다음과 같이 표시됩니다.

class SportsListOnBackPressedCallback(
   private val slidingPaneLayout: SlidingPaneLayout
): OnBackPressedCallback(slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen) {
   /**
    * Callback for handling the [OnBackPressedDispatcher.onBackPressed] event.
    */
   override fun handleOnBackPressed() {
       TODO("Not yet implemented")
   }
}
  1. 콘텐츠 창을 닫고 목록 창으로 돌아가도록 handleOnBackPressed() 함수 내에서 TODO 문을 삭제하고 다음 코드를 추가합니다.
slidingPaneLayout.closePane()

SlidingPaneLayout의 이벤트 모니터링

뒤로 누르기 이벤트를 처리하는 것 외에도 슬라이딩 창과 관련된 이벤트를 수신하고 모니터링해야 합니다. 콘텐츠 창이 슬라이드되면 그에 따라 콜백이 사용 설정되거나 중지되어야 합니다. 이를 위해 PanelSlideListener를 사용합니다.

인터페이스 SlidingPaneLayout.PanelSlideListener에는 세 가지 추상 메서드 즉, onPanelSlide(), onPanelOpened(), onPanelClosed()가 포함되어 있습니다. 이러한 메서드는 세부정보 창을 슬라이드하거나 열거나 닫을 때 호출됩니다.

  1. SlidingPaneLayout.PanelSlideListener에서 SportsListOnBackPressedCallback 클래스를 확장합니다.
  2. 오류를 해결하려면 세 가지 메서드를 구현합니다. 빨간색 전구를 클릭하고 Android 스튜디오에서 Implement members를 선택합니다.

ad52135eecbee09f.png

  1. SportsListOnBackPressedCallback 클래스는 다음과 유사합니다.
class SportsListOnBackPressedCallback(
   private val slidingPaneLayout: SlidingPaneLayout
): OnBackPressedCallback(slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen),
  SlidingPaneLayout.PanelSlideListener{

   override fun handleOnBackPressed() {
       slidingPaneLayout.closePane()
   }

   override fun onPanelSlide(panel: View, slideOffset: Float) {
       TODO("Not yet implemented")
   }

   override fun onPanelOpened(panel: View) {
       TODO("Not yet implemented")
   }

   override fun onPanelClosed(panel: View) {
       TODO("Not yet implemented")
   }
}
  1. TODO 문을 삭제합니다.
  2. 세부정보 창이 열려 있을 때(표시될 때) OnBackPressedCallback 콜백을 사용 설정합니다. 이렇게 하려면 setEnabled() 함수를 호출하고 true를 전달하면 됩니다. onPanelOpened() 내에 다음 코드를 작성합니다.
setEnabled(true)
  1. 위 코드는 속성 액세스 구문을 사용하여 단순화할 수 있습니다.
override fun onPanelOpened(panel: View) {
   isEnabled = true
}
  1. 마찬가지로 세부정보 창이 닫혀 있을 때 isEnabledfalse로 설정합니다.
override fun onPanelClosed(panel: View) {
   isEnabled = false
}
  1. 콜백을 완료하는 마지막 단계는 세부정보 창 슬라이드 이벤트 관련 알림을 받는 SportsListOnBackPressedCallback 리스너 클래스를 리스너 목록에 추가하는 것입니다. SportsListOnBackPressedCallback 클래스에 init 블록을 추가합니다. init 블록 내에서 this를 전달하는 slidingPaneLayout.addPanelSlideListener()를 호출합니다.
init {
   slidingPaneLayout.addPanelSlideListener(this)
}

완성된 SportsListOnBackPressedCallback 클래스는 다음과 유사합니다.

class SportsListOnBackPressedCallback(
   private val slidingPaneLayout: SlidingPaneLayout
): OnBackPressedCallback(slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen),
  SlidingPaneLayout.PanelSlideListener{

   init {
       slidingPaneLayout.addPanelSlideListener(this)
   }

   override fun handleOnBackPressed() {
       slidingPaneLayout.closePane()
   }

   override fun onPanelSlide(panel: View, slideOffset: Float) {
   }

   override fun onPanelOpened(panel: View) {
       isEnabled = true
   }

   override fun onPanelClosed(panel: View) {
       isEnabled = false
   }
}

콜백 등록

콜백의 작동 모습을 보기 위해 디스패처 OnBackPressedDispatcher를 사용하여 콜백을 등록합니다.

FragmentActivity의 기본 클래스에서는 OnBackPressedDispatcher를 사용하여 뒤로 버튼의 동작을 제어할 수 있습니다. OnBackPressedDispatcher는 뒤로 버튼 이벤트가 하나 이상의 OnBackPressedCallback 객체로 전달되는 방법을 제어합니다.

addCallback() 메서드를 사용하여 콜백을 추가합니다. 이 메서드는 LifecycleOwner를 받습니다. 따라서 LifecycleOwnerLifecycle.State.STARTED일 때만 OnBackPressedCallback이 추가됩니다. 또한 활동 또는 프래그먼트는 연결된 LifecycleOwner가 제거될 때 등록된 콜백을 삭제합니다. 이는 메모리 누수를 방지하며, 전체 기간이 짧은 프래그먼트 또는 기타 수명 주기 소유자에 사용하기 적합합니다.

또한 addCallback() 메서드는 인스턴스에서 콜백 클래스를 두 번째 매개변수로 받습니다. 다음 단계에 따라 콜백을 등록합니다.

  1. SportsListFragment 파일의 onViewCreated() 함수 내에서 바인딩 변수 선언 바로 아래에 SlidingPaneLayout을 위한 인스턴스를 만들고 이 인스턴스에 binding.slidingPaneLayout의 값을 할당합니다.
val slidingPaneLayout = binding.slidingPaneLayout
  1. SportsListFragment 파일의 onViewCreated() 함수 내에서 slidingPaneLayout 선언 바로 아래에 다음 코드를 추가합니다.
// Connect the SlidingPaneLayout to the system back button.
requireActivity().onBackPressedDispatcher.addCallback(
   viewLifecycleOwner,
   SportsListOnBackPressedCallback(slidingPaneLayout)
)

위 코드는 addCallback()을 사용하여 viewLifecycleOwnerSportsListOnBackPressedCallback 인스턴스를 전달합니다. 이 콜백은 프래그먼트의 수명 주기 동안에만 활성화됩니다.

  1. 이제 휴대전화 프로필을 사용하는 에뮬레이터에서 앱을 실행하고 맞춤 뒤로 버튼 기능의 작동 모습을 확인할 차례입니다.

33967fa8fde5b902.gif

10. 잠금 모드

휴대전화 같은 소형 화면에서 목록 창과 세부정보 창이 겹치는 경우 사용자는 기본적으로 양방향으로 스와이프할 수 있고 동작 탐색을 사용하지 않더라도 두 창 간에 자유롭게 전환할 수 있습니다. SlidingPaneLayout의 잠금 모드를 설정하여 세부정보 창을 잠그거나 잠금 해제할 수 있습니다.

  1. 휴대전화 프로필을 사용하는 에뮬레이터에서 세부정보 창이 화면에서 사라질 때까지 스와이프해 봅니다.
  2. 세부정보 창이 표시되도록 안쪽으로 스와이프할 수도 있습니다. 직접 해 보세요.
  3. 이 기능은 Sports 앱에서는 바람직하지 않습니다. 사용자가 동작을 사용하여 안쪽과 바깥쪽으로 스와이프하지 못하도록 SlidingPaneLayout을 잠그는 것이 좋습니다. 이를 구현하려면 onViewCreated() 메서드의 slidingPaneLayout 정의 아래에서 lockModeLOCK_MODE_LOCKED로 설정합니다.
slidingPaneLayout.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED

다른 잠금 모드에 관해 자세히 알아보려면 문서를 참고하세요.

  1. 앱을 한 번 더 실행합니다. 이제 세부정보 창이 잠겨 있음을 알 수 있습니다.

축하합니다. 앱에 SlidingPaneLayout을 추가했습니다.

11. 솔루션 코드

이 Codelab의 솔루션 코드는 아래에 나온 프로젝트와 모듈에 있습니다.

  1. 프로젝트에 제공된 GitHub 저장소 페이지로 이동합니다.
  2. 브랜치 이름이 Codelab에 지정된 브랜치 이름과 일치하는지 확인합니다. 예를 들어 다음 스크린샷에서 브랜치 이름은 main입니다.

1e4c0d2c081a8fd2.png

  1. 프로젝트의 GitHub 페이지에서 Code 버튼을 클릭하여 팝업을 엽니다.

1debcf330fd04c7b.png

  1. 팝업에서 Download ZIP 버튼을 클릭하여 컴퓨터에 프로젝트를 저장합니다. 다운로드가 완료될 때까지 기다립니다.
  2. 컴퓨터에서 파일을 찾습니다(예: Downloads 폴더).
  3. ZIP 파일을 더블클릭하여 압축을 해제합니다. 프로젝트 파일이 포함된 새 폴더가 만들어집니다.

Android 스튜디오에서 프로젝트 열기

  1. Android 스튜디오를 시작합니다.
  2. Welcome to Android Studio 창에서 Open을 클릭합니다.

d8e9dbdeafe9038a.png

참고: Android 스튜디오가 이미 열려 있는 경우 File > Open 메뉴 옵션을 대신 선택합니다.

8d1fda7396afe8e5.png

  1. 파일 브라우저에서 압축 해제된 프로젝트 폴더가 있는 위치로 이동합니다(예: Downloads 폴더).
  2. 프로젝트 폴더를 더블클릭합니다.
  3. Android 스튜디오가 프로젝트를 열 때까지 기다립니다.
  4. Run 버튼 8de56cba7583251f.png을 클릭하여 앱을 빌드하고 실행합니다. 예상대로 작동하는지 확인합니다.

12. 자세히 알아보기