활동 삽입

활동 삽입은 애플리케이션의 작업 창을 두 개의 활동 또는 동일한 활동의 두 개의 인스턴스로 분할하여 대형 화면 기기에서 앱을 최적화합니다.

그림 1. 활동을 나란히 보여주는 설정 앱

앱이 여러 활동으로 구성된 경우 활동 삽입을 사용하면 태블릿, 폴더블, ChromeOS 기기에서 향상된 사용자 환경을 제공할 수 있습니다.

활동 삽입에는 코드 리팩터링이 필요하지 않습니다. XML 구성 파일을 만들거나 Jetpack WindowManager API를 호출하여 앱이 활동을 표시하는 방법, 즉 나란히 표시할지 스택으로 표시할지를 결정하면 됩니다.

작은 화면 지원은 자동으로 유지됩니다. 화면이 작은 기기에 앱이 있는 경우 활동이 서로 스택됩니다. 대형 화면에서는 활동이 나란히 표시됩니다. 생성한 구성에 따라 시스템에서 표현 방식을 결정하며 분기화 로직이 필요하지 않습니다.

활동 삽입은 기기 방향 변경을 지원하고 폴더블 기기에서 원활하게 작동하여 기기를 접거나 펼칠 때 활동을 스택하고 스택 해제합니다.

활동 삽입은 Android 12L(API 수준 32) 및 이후 버전을 실행하는 대부분의 대형 화면 기기에서 지원됩니다.

작업 창 분할

활동 삽입은 앱 작업 창을 기본 컨테이너와 보조 컨테이너로 분할합니다. 컨테이너는 기본 활동에서 실행된 활동, 또는 이미 컨테이너에 있는 다른 활동에서 실행된 활동을 보유합니다.

활동이 실행되면 보조 컨테이너에 스택되고, 작은 화면에서 보조 컨테이너는 기본 컨테이너 위에 스택됩니다. 따라서 액티비티 스택 및 뒤로 탐색이 앱에 이미 내장된 활동의 순서와 일치합니다.

활동 삽입을 사용하면 다양한 방식으로 활동을 표시할 수 있습니다. 앱은 두 활동을 나란히 동시 실행하여 작업 창을 분할할 수 있습니다.

그림 2. 나란히 표시되는 두 활동

또는 전체 작업 창을 차지하고 있는 활동은 새 활동을 나란히 실행하여 분할을 만들 수 있습니다.

그림 3. 활동 A가 측면에 활동 B를 시작함
를 통해 개인정보처리방침을 정의할 수 있습니다.
를 통해 개인정보처리방침을 정의할 수 있습니다.

이미 분할에 표시되며 작업 창을 공유하는 활동은 다음과 같은 방법으로 다른 활동을 실행할 수 있습니다.

  • 측면으로, 다른 활동 위:

    그림 4. 활동 A가 측면의 활동 B 위로 활동 C를 시작함
  • 측면으로, 분할의 좌우를 전환하여 이전 기본 활동을 숨김:

    그림 5. 활동 B가 측면에 활동 C를 시작하고 분할의 좌우를 전환함
  • 기존 위치의 위에(동일한 활동 스택) 활동 실행:

    그림 6. 활동 B가 추가 인텐트 플래그 없이 활동 C를 시작함
  • 동일한 작업에서 활동 전체 크기 창 실행:

    그림 7. 활동 A 또는 활동 B가 작업 창을 채우는 활동 C를 시작함

뒤로 탐색

다양한 유형의 애플리케이션은 분할 작업 창 상태에서 활동 간의 종속성이나 사용자가 뒤로 이벤트를 트리거하는 방식(아래의 예)에 따라 서로 다른 뒤로 탐색 규칙을 가질 수 있습니다.

  • 동시: 활동이 관련되어 있어서 두 활동을 따로 표시해서는 안 되는 경우 둘 다 종료하도록 뒤로 탐색을 구성할 수 있습니다.
  • 단독: 활동이 완전히 독립적인 경우 한 활동의 뒤로 탐색이 작업 창에 있는 다른 활동의 상태에 영향을 주지 않습니다.

버튼 탐색을 사용할 때 마지막으로 포커스가 맞춰진 활동으로 뒤로 이벤트가 전송됩니다. 동작 기반 탐색을 사용하면 동작이 발생한 활동으로 뒤로 이벤트가 전송됩니다.

다중 창 레이아웃

Jetpack WindowManager를 사용하면 Android 12L(API 수준 32) 이상을 사용하는 대형 화면 기기와 이전 플랫폼 버전을 사용하는 일부 기기에서 활동 삽입 다중 창 레이아웃을 빌드할 수 있습니다. 프래그먼트나 뷰 기반 레이아웃(예: SlidingPaneLayout)이 아닌 여러 활동을 기반으로 하는 기존 앱은 소스 코드를 리팩터링하지 않고도 개선된 대형 화면 사용자 환경을 제공할 수 있습니다.

한 가지 일반적인 예는 목록-세부정보 분할입니다. 높은 품질의 표현을 위해 시스템에서 목록 활동을 시작한 후에 애플리케이션이 즉시 세부정보 활동을 시작합니다. 전환 시스템은 두 활동이 모두 그려질 때까지 대기했다가 두 활동을 함께 표시합니다. 두 활동은 사용자의 관점에서 하나로 실행됩니다.

그림 8. 다중 창 레이아웃에서 동시에 시작된 두 활동

속성 분할

분할 컨테이너 간 작업 창의 비율과 컨테이너가 다른 컨테이너에 대해 배치되는 방식을 지정할 수 있습니다.

XML 구성 파일에 정의된 규칙의 경우 다음 속성을 설정해야 합니다.

  • splitRatio: 컨테이너 비율을 설정합니다. 값은 개구간 (0.0, 1.0) 내의 부동 소수점 숫자입니다.
  • splitLayoutDirection: 분할 컨테이너가 다른 컨테이너에 대해 배치되는 방식을 지정합니다. 이 속성에는 다음과 같은 값이 있습니다.
    • ltr: 왼쪽에서 오른쪽
    • rtl: 오른쪽에서 왼쪽
    • locale: 언어 설정에 따라 ltr 또는 rtl로 결정됨

아래 예에서 XML 구성을 참고하세요.

WindowManager API를 사용하여 만든 규칙의 경우 SplitAttributes.Builder를 사용하여 SplitAttributes 객체를 만들고 다음 빌더 메서드를 호출합니다.

아래 예에서 WindowManager API를 참고하세요.

그림 9. 두 개의 활동 분할이 왼쪽에서 오른쪽으로 배치되었지만 분할 비율이 서로 다릅니다.

자리표시자

자리표시자 활동은 활동 분할의 한 영역을 차지하는 빈 보조 활동입니다. 궁극적으로 콘텐츠가 포함된 다른 활동으로 대체됩니다. 예를 들어 자리표시자 활동은 목록의 항목이 선택될 때까지 목록-세부정보 레이아웃에서 활동 분할의 보조 측면을 차지할 수 있습니다. 목록의 항목이 선택되면 이 목록 항목에 관한 세부정보가 포함된 활동이 자리표시자를 대체합니다.

기본적으로 시스템은 활동 분할을 위한 공간이 충분한 경우에만 자리표시자를 표시합니다. 너무 작아서 분할을 표시할 수 없는 너비나 높이로 디스플레이 크기가 변경되면 자리표시자는 자동으로 종료됩니다. 공간이 허락하는 경우 시스템은 다시 초기화된 상태로 자리표시자를 다시 실행합니다.

그림 10. 폴더블 기기 접기 및 펼치기. 디스플레이 크기가 변경됨에 따라 자리표시자 활동이 종료되고 재생성됨

하지만 SplitPlaceholder.Builder에 있는 SplitPlaceholderRule 또는 setSticky() 메서드의 stickyPlaceholder 속성은 기본 동작을 재정의할 수 있습니다. 속성이나 메서드가 true 값을 지정하면 디스플레이가 두 개의 창 디스플레이에서 단일 창 디스플레이로 크기가 조절될 때 시스템에서 이 자리표시자를 작업 창의 최상위 활동으로 표시합니다 (아래 예의 분할 구성 참고).

그림 11. 폴더블 기기 접기 및 펼치기. 자리표시자 활동이 고정됩니다.

창 크기 변경

기기 설정이 변경되면 작업 창 너비가 다중 창 레이아웃에 맞게 충분히 크지 않을 때(예: 대형 화면의 폴더블 기기가 태블릿 크기에서 휴대전화 크기로 접히는 경우 또는 앱 창의 크기가 멀티 윈도우 모드에서 조절되는 경우) 작업 창의 보조 창에 있는 자리표시자 이외의 활동이 기본 창의 활동 위에 스택됩니다.

자리표시자 활동은 분할에 맞게 디스플레이 너비가 충분한 경우에만 표시됩니다. 작은 화면에서는 자리표시자가 자동으로 닫힙니다. 디스플레이 영역이 다시 충분히 커지면 자리표시자가 재생성됩니다. 위의 자리표시자를 참고하세요.

WindowManager가 기본 창의 활동 위에 보조 창의 활동을 z 순서로 지정하므로 액티비티 스택이 가능합니다.

보조 창의 여러 활동

활동 B가 추가 인텐트 플래그 없이 제자리에서 활동 C를 시작합니다.

B 위에 스택된 C를 포함하는 활동 A, B, C가 있는 활동 분할

동일한 작업에서 다음과 같이 활동이 z 순서로 지정됩니다.

B 위에 스택된 활동 C를 포함하는 보조 활동 스택.
          보조 스택이 활동 A를 포함하는 기본 활동 스택 위에 스택됨

따라서 작은 작업 창에서는 애플리케이션이 스택 맨 위에 있는 C를 포함한 단일 활동으로 축소됩니다.

활동 C만 보여주는 작은 창

작은 창에서 뒤로 이동하면 활동 위에 스택된 활동을 탐색하게 됩니다.

작업 창 구성이 여러 창을 수용할 수 있는 큰 크기로 복원되면 활동이 다시 나란히 표시됩니다.

스택된 분할

활동 B가 측면에 활동 C를 시작하고 분할의 좌우를 전환합니다.

활동 A와 B, 그리고 활동 B와 C를 보여주는 작업 창

동일한 작업에서 다음과 같이 활동이 z 순서로 지정됩니다.

단일 스택에 있는 활동 A, B, C. 활동은 위에서 아래로 C, B, A의 순서로 스택됨

작은 작업 창에서는 애플리케이션이 맨 위에 있는 C를 포함한 단일 활동으로 축소됩니다.

활동 C만 보여주는 작은 창

고정 세로 방향

android:screenOrientation 매니페스트 설정을 사용하면 앱의 활동을 세로 또는 가로 방향으로 제한할 수 있습니다. 태블릿 및 폴더블과 같은 대형 화면 기기에서 사용자 경험을 개선하기 위해 기기 제조업체(OEM)는 화면 방향 요청을 무시하고 가로 모드 디스플레이에서 세로 방향으로 앱을 레터박스 처리하거나 세로 모드 디스플레이에서 가로 방향으로 앱을 레터박스 처리할 수 있습니다.

그림 12. 레터박스 처리된 활동: 가로 모드 기기의 고정 세로 모드(왼쪽), 세로 모드 기기의 고정 가로 모드(오른쪽)

마찬가지로 활동 삽입이 사용 설정된 경우 OEM은 대형 화면(너비 600dp 이상)의 가로 모드 방향에서 고정 세로 방향 활동을 레터박스 처리하도록 기기를 맞춤설정할 수 있습니다. 고정 세로 모드 활동이 두 번째 활동을 실행하면 기기는 두 개의 창이 있는 디스플레이에 두 활동을 나란히 표시할 수 있습니다.

그림 13. 고정 세로 모드 활동 A가 측면에 활동 B를 시작함

항상 android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED 속성을 앱 매니페스트 파일에 추가하여 앱에서 활동 삽입을 지원한다는 것을 기기에 알립니다(아래의 분할 구성 참고). 그러면 OEM 맞춤 기기에서 고정 세로 방향 활동의 레터박스 처리 여부를 결정할 수 있습니다.

분할 구성

분할 규칙은 활동 분할을 구성합니다. 분할 규칙은 XML 구성 파일에서 또는 Jetpack WindowManager API를 호출하여 정의합니다.

어느 경우든 앱은 WindowManager 라이브러리에 액세스해야 하고 앱에서 활동 삽입을 구현했음을 시스템에 알려야 합니다.

다음 단계를 따르세요.

  1. 최신 WindowManager 라이브러리 종속 항목을 앱의 모듈 수준 build.gradle 파일에 추가합니다. 예를 들면 다음과 같습니다.

    implementation 'androidx.window:window:1.1.0-beta02'

    WindowManager 라이브러리는 활동 삽입에 필요한 모든 구성요소를 제공합니다.

  2. 앱이 활동 삽입을 구현했음을 시스템에 알립니다.

    앱 매니페스트 파일의 <application> 요소에 android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED 속성을 추가하고 값을 true로 설정합니다. 예를 들면 다음과 같습니다.

    <manifest xmlns:android="http://schemas.android.com/apk/res/android">
        <application>
            <property
                android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED"
                android:value="true" />
        </application>
    </manifest>
    

    WindowManager 버전 1.1.0-alpha06 이상에서는 활동 삽입 분할이 사용 중지됩니다. 단, 속성이 매니페스트에 추가되고 true로 설정되는 경우는 예외입니다.

    또한 기기 제조업체는 이 설정을 사용하여 활동 삽입을 지원하는 앱의 맞춤 기능을 사용 설정합니다. 예를 들어 기기는 가로 모드 디스플레이에서 세로 모드 전용 활동을 레터박스 처리하여, 두 번째 활동이 시작될 때 창 두 개 레이아웃으로 전환하도록 활동의 방향을 설정할 수 있습니다(세로 모드로 고정된 방향 참고).

XML 구성

활동 삽입의 XML 기반 구현을 만들려면 다음 단계를 완료하세요.

  1. 다음을 실행하는 XML 리소스 파일을 만듭니다.

    • 분할을 공유하는 활동 정의
    • 분할 옵션 구성
    • 콘텐츠를 사용할 수 없는 경우 분할의 보조 컨테이너에 맞는 자리표시자를 만듭니다.
    • 분할에 포함되면 안 되는 활동을 지정합니다.

    예를 들면 다음과 같습니다.

    <!-- main_split_config.xml -->
    
    <resources
        xmlns:window="http://schemas.android.com/apk/res-auto">
    
        <!-- Define a split for the named activities. -->
        <SplitPairRule
            window:splitRatio="0.33"
            window:splitLayoutDirection="locale"
            window:splitMinWidthDp="840"
            window:splitMaxAspectRatioInPortrait="alwaysAllow"
            window:finishPrimaryWithSecondary="never"
            window:finishSecondaryWithPrimary="always"
            window:clearTop="false">
            <SplitPairFilter
                window:primaryActivityName=".ListActivity"
                window:secondaryActivityName=".DetailActivity"/>
        </SplitPairRule>
    
        <!-- Specify a placeholder for the secondary container when content is
             not available. -->
        <SplitPlaceholderRule
            window:placeholderActivityName=".PlaceholderActivity"
            window:splitRatio="0.33"
            window:splitLayoutDirection="locale"
            window:splitMinWidthDp="840"
            window:splitMaxAspectRatioInPortrait="alwaysAllow"
            window:stickyPlaceholder="false">
            <ActivityFilter
                window:activityName=".ListActivity"/>
        </SplitPlaceholderRule>
    
        <!-- Define activities that should never be part of a split. Note: Takes
             precedence over other split rules for the activity named in the
             rule. -->
        <ActivityRule
            window:alwaysExpand="true">
            <ActivityFilter
                window:activityName=".ExpandedActivity"/>
        </ActivityRule>
    
    </resources>
    
  2. 이니셜라이저를 만듭니다.

    WindowManager RuleController 구성요소는 XML 구성 파일을 파싱하고 시스템에 제공할 규칙을 만듭니다. Jetpack Startup 라이브러리 Initializer는 앱 시작 시 RuleController에 제공할 XML 파일을 만들어 모든 활동이 시작될 때 규칙이 적용되도록 합니다.

    이니셜라이저를 만들려면 다음 단계를 따르세요.

    1. 최신 Jetpack Startup 라이브러리 종속 항목을 모듈 수준 build.gradle 파일에 추가합니다. 예를 들면 다음과 같습니다.

      implementation 'androidx.startup:startup-runtime:1.1.1'

    2. Initializer 인터페이스를 구현하는 클래스를 만듭니다.

      이니셜라이저는 XML 구성 파일(main_split_config.xml)의 ID를 RuleController.parseRules() 메서드에 전달하여 RuleController에서 사용할 수 있는 분할 규칙을 만듭니다.

      Kotlin

      class SplitInitializer : Initializer<RuleController> {
      
       override fun create(context: Context): RuleController {
           return RuleController.getInstance(context).apply {
               setRules(RuleController.parseRules(context, R.xml.main_split_config))
           }
       }
      
       override fun dependencies(): List<Class<out Initializer<*>>> {
           return emptyList()
       }
      }
      

      Java

      public class SplitInitializer implements Initializer<RuleController> {
      
        @NonNull
        @Override
        public RuleController create(@NonNull Context context) {
            RuleController ruleController = RuleController.getInstance(context);
            ruleController.setRules(
                RuleController.parseRules(context, R.xml.main_split_config)
            );
            return ruleController;
        }
      
        @NonNull
        @Override
        public List<Class<? extends Initializer<?>>> dependencies() {
            return Collections.emptyList();
        }
      }
      
  3. 규칙 정의에 맞는 콘텐츠 제공자 만들기

    androidx.startup.InitializationProvider를 앱 매니페스트 파일에 <provider>로 추가합니다. RuleController의 이니셜라이저 SplitInitializer 구현에 대한 참조를 포함합니다.

    <!-- AndroidManifest.xml -->
    
    <provider android:name="androidx.startup.InitializationProvider"
        android:authorities="${applicationId}.androidx-startup"
        android:exported="false"
        tools:node="merge">
        <!-- Make SplitInitializer discoverable by InitializationProvider. -->
        <meta-data android:name="${applicationId}.SplitInitializer"
            android:value="androidx.startup" />
    </provider>
    

    InitializationProvider는 앱의 onCreate() 메서드가 호출되기 전에 SplitInitializer를 탐색하고 초기화합니다. 따라서 분할 규칙은 앱의 기본 앱 활동이 시작할 때 적용됩니다.

WindowManager API

몇 개의 API 호출을 사용하여 활동 삽입을 프로그래매틱 방식으로 구현할 수 있습니다. 활동이 시작되기 전에 규칙이 적용되도록 Application의 서브클래스에 있는 onCreate() 메서드에서 API를 호출합니다.

프로그래매틱 방식으로 활동 분할을 만들려면 다음 단계를 따르세요.

  1. 분할 규칙 만들기:

    1. 분할을 공유하는 활동을 식별하는 SplitPairFilter를 만듭니다.

      Kotlin

      val splitPairFilter = SplitPairFilter(
         ComponentName(this, ListActivity::class.java),
         ComponentName(this, DetailActivity::class.java),
         null
      )
      

      Java

      SplitPairFilter splitPairFilter = new SplitPairFilter(
         new ComponentName(this, ListActivity.class),
         new ComponentName(this, DetailActivity.class),
         null
      );
      
    2. 필터 세트에 필터를 추가합니다.

      Kotlin

      val filterSet = setOf(splitPairFilter)
      

      Java

      Set<SplitPairFilter> filterSet = new HashSet<>();
      filterSet.add(splitPairFilter);
      
    3. 분할의 레이아웃 속성을 만듭니다.

      Kotlin

      val splitAttributes: SplitAttributes = SplitAttributes.Builder()
          .setSplitType(SplitAttributes.SplitType.ratio(0.33f))
          .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
          .build()
      

      Java

      final SplitAttributes splitAttributes = new SplitAttributes.Builder()
            .setSplitType(SplitAttributes.SplitType.ratio(0.33f))
            .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
            .build();
      

      SplitAttributes.Builder는 레이아웃 속성을 포함하는 객체를 만듭니다.

      • setSplitType: 사용 가능한 디스플레이 영역이 각 활동 컨테이너에 할당되는 방식을 정의합니다. 비율 분할 유형은 기본 컨테이너에 할당된 가용 디스플레이 영역의 비율을 지정합니다. 보조 컨테이너는 가용 디스플레이 영역의 나머지 부분을 차지합니다.
      • setLayoutDirection: 활동 컨테이너가 다른 컨테이너를 기준으로 어떻게 배치되는지를 지정합니다. 우선 기본 컨테이너를 기준으로 합니다.
    4. SplitPairRule을 빌드합니다.

      Kotlin

      val splitPairRule = SplitPairRule.Builder(filterSet)
          .setDefaultSplitAttributes(splitAttributes)
          .setMinWidthDp(840)
          .setMinSmallestWidthDp(600)
          .setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ratio(1.5f))
          .setFinishPrimaryWithSecondary(SplitRule.FinishBehavior.NEVER)
          .setFinishSecondaryWithPrimary(SplitRule.FinishBehavior.ALWAYS)
          .setClearTop(false)
          .build()
      

      Java

      SplitPairRule splitPairRule = new SplitPairRule.Builder(filterSet)
          .setDefaultSplitAttributes(splitAttributes)
          .setMinWidthDp(840)
          .setMinSmallestWidthDp(600)
          .setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ratio(1.5f))
          .setFinishPrimaryWithSecondary(SplitRule.FinishBehavior.NEVER)
          .setFinishSecondaryWithPrimary(SplitRule.FinishBehavior.ALWAYS)
          .setClearTop(false)
          .build();
      

      SplitPairRule.Builder는 다음과 같은 규칙을 만들고 구성합니다.

      • filterSet: 분할을 공유하는 활동을 파악하여 규칙을 적용할 시기를 결정하는 분할 쌍 필터를 포함합니다.
      • setDefaultSplitAttributes: 규칙에 레이아웃 속성을 적용합니다.
      • setMinWidthDp: 분할을 사용 설정하는 최소 디스플레이 너비를 밀도 독립형 픽셀(dp) 단위로 설정합니다.
      • setMinSmallestWidthDp: 기기 방향과 관계없이 두 디스플레이 크기 중 더 작은 값이 분할을 사용할 수 있는 최솟값(dp)을 설정합니다.
      • setMaxAspectRatioInPortrait: 세로 모드 방향에서 활동 분할이 표시되는 최대 디스플레이 가로세로 비율(높이:너비)을 설정합니다. 세로 모드 디스플레이의 가로세로 비율이 최대 가로세로 비율을 초과하면 디스플레이 너비와 관계없이 분할이 사용 중지됩니다. 참고: 기본값은 1.4이며, 이 경우 활동이 대부분의 태블릿에서 세로 모드 방향의 전체 작업 창을 차지합니다. SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULTsetMaxAspectRatioInLandscape도 참고하세요. 가로 모드의 기본값은 ALWAYS_ALLOW입니다.
      • setFinishPrimaryWithSecondary: 보조 컨테이너의 모든 활동이 종료될 때 기본 컨테이너의 활동에 미치는 영향을 설정합니다. NEVER는 보조 컨테이너의 모든 활동이 종료될 때 시스템에서 기본 활동을 종료하면 안 된다는 것을 나타냅니다(활동 종료 참고).
      • setFinishSecondaryWithPrimary: 기본 컨테이너의 모든 활동이 종료될 때 보조 컨테이너의 활동에 미치는 영향을 설정합니다. ALWAYS는 기본 컨테이너의 모든 활동이 종료될 때 시스템에서 항상 보조 컨테이너의 활동을 종료해야 함을 나타냅니다(활동 종료 참고).
      • setClearTop: 컨테이너에서 새 활동이 실행될 때 보조 컨테이너의 모든 활동을 종료할지를 지정합니다. False는 새 활동이 보조 컨테이너에 이미 있는 활동 위에 스택됨을 지정합니다.
    5. 다음과 같이 WindowManager RuleController의 싱글톤 인스턴스를 가져오고 규칙을 추가합니다.

      Kotlin

      val ruleController = RuleController.getInstance(this)
      ruleController.addRule(splitPairRule)
      

      Java

      RuleController ruleController = RuleController.getInstance(this);
      ruleController.addRule(splitPairRule);
      
  2. 콘텐츠를 사용할 수 없는 경우 보조 컨테이너의 자리표시자를 만듭니다.

    1. 자리표시자가 작업 창 분할을 공유하는 활동을 식별하는 ActivityFilter를 만듭니다.

      Kotlin

      val placeholderActivityFilter = ActivityFilter(
          ComponentName(this, ListActivity::class.java),
          null
      )
      

      Java

      ActivityFilter placeholderActivityFilter = new ActivityFilter(
          new ComponentName(this, ListActivity.class),
          null
      );
      
    2. 필터 세트에 필터를 추가합니다.

      Kotlin

      val placeholderActivityFilterSet = setOf(placeholderActivityFilter)
      

      Java

      Set<ActivityFilter> placeholderActivityFilterSet = new HashSet<>();
      placeholderActivityFilterSet.add(placeholderActivityFilter);
      
    3. SplitPlaceholderRule을 만듭니다.

      Kotlin

      val splitPlaceholderRule = SplitPlaceholderRule.Builder(
            placeholderActivityFilterSet,
            Intent(context, PlaceholderActivity::class.java)
          ).setDefaultSplitAttributes(splitAttributes)
           .setMinWidthDp(840)
           .setMinSmallestWidthDp(600)
           .setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ratio(1.5f))
           .setFinishPrimaryWithPlaceholder(SplitRule.FinishBehavior.ALWAYS)
           .setSticky(false)
           .build()
      

      Java

      SplitPlaceholderRule splitPlaceholderRule = new SplitPlaceholderRule.Builder(
            placeholderActivityFilterSet,
            new Intent(context, PlaceholderActivity.class)
          ).setDefaultSplitAttributes(splitAttributes)
           .setMinWidthDp(840)
           .setMinSmallestWidthDp(600)
           .setMaxAspectRatioInPortrait(EmbeddingAspectRatio.ratio(1.5f))
           .setFinishPrimaryWithPlaceholder(SplitRule.FinishBehavior.ALWAYS)
           .setSticky(false)
           .build();
      

      SplitPlaceholderRule.Builder는 다음과 같은 규칙을 만들고 구성합니다.

      • placeholderActivityFilterSet: 자리표시자 활동과 연결된 활동을 식별하여 규칙을 적용할 시기를 결정하는 활동 필터를 포함합니다.
      • Intent: 자리표시자 활동의 실행을 지정합니다.
      • setDefaultSplitAttributes: 규칙에 레이아웃 속성을 적용합니다.
      • setMinWidthDp: 분할을 허용하는 최소 디스플레이 너비를 밀도 독립형 픽셀(dp) 단위로 설정합니다.
      • setMinSmallestWidthDp: 분할을 허용하기 위해 기기 방향과 관계없이 두 디스플레이 크기 중 더 작은 값이 취해야 할 최솟값(dp)을 설정합니다.
      • setMaxAspectRatioInPortrait: 세로 모드 방향에서 활동 분할이 표시되는 최대 디스플레이 가로세로 비율(높이:너비)을 설정합니다. 참고: 기본값은 1.4이며, 이 경우 활동이 대부분의 태블릿에서 세로 모드 방향의 전체 작업 창을 채웁니다. SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULTsetMaxAspectRatioInLandscape도 참고하세요. 가로 모드의 기본값은 ALWAYS_ALLOW입니다.
      • setFinishPrimaryWithPlaceholder: 자리표시자 활동이 종료될 때 기본 컨테이너의 활동에 미치는 영향을 설정합니다. ALWAYS는 자리표시자가 종료되면 시스템이 항상 기본 컨테이너의 활동을 종료해야 함을 나타냅니다(활동 종료 참고).
      • setSticky: 자리표시자가 충분한 최소 너비로 분할에 처음 표시된 후에 자리표시자 활동이 작은 디스플레이의 액티비티 스택 상단에 표시될지를 결정합니다.
    4. WindowManager RuleController에 규칙을 추가합니다.

      Kotlin

      ruleController.addRule(splitPlaceholderRule)
      

      Java

      ruleController.addRule(splitPlaceholderRule);
      
  3. 분할에 포함되면 안 되는 활동을 지정합니다.

    1. 항상 전체 작업 디스플레이 영역을 차지해야 하는 활동을 식별하는 ActivityFilter를 만듭니다.

      Kotlin

      val expandedActivityFilter = ActivityFilter(
        ComponentName(this, ExpandedActivity::class.java),
        null
      )
      

      Java

      ActivityFilter expandedActivityFilter = new ActivityFilter(
        new ComponentName(this, ExpandedActivity.class),
        null
      );
      
    2. 필터 세트에 필터를 추가합니다.

      Kotlin

      val expandedActivityFilterSet = setOf(expandedActivityFilter)
      

      Java

      Set<ActivityFilter> expandedActivityFilterSet = new HashSet<>();
      expandedActivityFilterSet.add(expandedActivityFilter);
      
    3. ActivityRule을 만듭니다.

      Kotlin

      val activityRule = ActivityRule.Builder(expandedActivityFilterSet)
          .setAlwaysExpand(true)
          .build()
      

      Java

      ActivityRule activityRule = new ActivityRule.Builder(
          expandedActivityFilterSet
      ).setAlwaysExpand(true)
       .build();
      

      ActivityRule.Builder는 다음과 같은 규칙을 만들고 구성합니다.

      • expandedActivityFilterSet: 분할에서 제외할 활동을 식별하여 규칙을 적용할 시기를 결정하는 활동 필터를 포함합니다.
      • setAlwaysExpand: 활동이 전체 작업 창을 채워야 하는지 여부를 지정합니다.
    4. WindowManager RuleController에 규칙을 추가합니다.

      Kotlin

      ruleController.addRule(activityRule)
      

      Java

      ruleController.addRule(activityRule);
      

교차 애플리케이션 삽입

Android 13(API 수준 33) 이상에서는 앱이 다른 앱의 활동을 삽입할 수 있습니다. 교차 애플리케이션 활동 또는 교차 UID 활동 삽입을 사용하면 Android 애플리케이션의 활동을 시각적으로 통합할 수 있습니다. 시스템은 단일 앱 활동 삽입에서처럼 호스트 앱의 활동과 다른 앱의 삽입된 활동을 화면에 나란히 표시하거나 위아래로 표시합니다.

예를 들어 설정 앱에서 WallpaperPicker 앱의 배경화면 선택기 활동을 삽입할 수 있습니다.

그림 14. 배경화면 선택기가 삽입된 활동(오른쪽)으로 설정된 설정 앱(왼쪽 메뉴)

신뢰 모델

다른 앱의 활동을 삽입하는 호스트 프로세스는 크기, 위치, 자르기, 투명도 등 삽입된 활동의 표현을 재정의할 수 있습니다. 악의적인 호스트는 이 기능을 사용하여 사용자를 현혹하고 클릭재킹 또는 기타 UI 수정 공격을 유발할 수 있습니다.

교차 앱 활동 삽입의 오용을 방지하기 위해 Android의 앱에서는 활동 삽입을 허용하도록 선택해야 합니다. 앱은 호스트를 신뢰할 수 있는 호스트 또는 신뢰할 수 없는 호스트로 지정할 수 있습니다.

신뢰할 수 있는 호스트

다른 애플리케이션이 앱의 활동을 삽입하고 이러한 앱 활동 표현을 완전히 제어할 수 있게 하려면 앱 매니페스트 파일에서 <activity> 또는 <application> 요소의 android:knownActivityEmbeddingCerts 속성에 호스트 애플리케이션의 SHA-256 인증서를 지정합니다.

android:knownActivityEmbeddingCerts의 값을 다음과 같은 문자열로 설정하거나

<activity
    android:name=".MyEmbeddableActivity"
    android:knownActivityEmbeddingCerts="@string/known_host_certificate_digest"
    ... />

또는 (여러 인증서를 지정하려면) 문자열로 구성된 배열로 설정합니다.

<activity
    android:name=".MyEmbeddableActivity"
    android:knownActivityEmbeddingCerts="@array/known_host_certificate_digests"
    ... />

다음과 같이 리소스를 참조합니다.

<resources>
    <string-array name="known_host_certificate_digests">
      <item>cert1</item>
      <item>cert2</item>
      ...
    </string-array>
</resources>

앱 소유자는 Gradle signingReport 작업을 실행하여 SHA 인증서 다이제스트를 가져올 수 있습니다. 인증서 다이제스트는 구분용 콜론이 포함되지 않은 SHA-256 지문입니다. 자세한 내용은 서명 보고서 실행클라이언트 인증을 참고하세요.

신뢰할 수 없는 호스트

모든 앱이 앱의 활동을 삽입하고 이러한 앱 활동 표현을 제어할 수 있게 하려면 앱 매니페스트의 <activity> 요소나 <application> 요소에 android:allowUntrustedActivityEmbedding 속성을 지정합니다. 예를 들면 다음과 같습니다.

<activity
    android:name=".MyEmbeddableActivity"
    android:allowUntrustedActivityEmbedding="true"
    ... />

속성의 기본값은 false이며, 이 경우 교차 앱 활동 삽입이 방지됩니다.

맞춤 인증

신뢰할 수 없는 활동 삽입의 위험을 완화하려면 호스트 ID를 확인하는 맞춤 인증 메커니즘을 만듭니다. 호스트 인증서를 알고 있는 경우 androidx.security.app.authenticator 라이브러리를 사용하여 인증합니다. 활동이 삽입된 후 호스트가 인증하는 경우 실제 콘텐츠를 표시할 수 있습니다. 그렇지 않은 경우 사용자에게 작업이 허용되지 않았음을 알리고 콘텐츠를 차단할 수 있습니다.

Jetpack WindowManager 라이브러리의 ActivityEmbeddingController#isActivityEmbedded() 메서드를 사용하여 호스트가 활동을 삽입하는지 여부를 확인합니다. 예를 들면 다음과 같습니다.

Kotlin

fun isActivityEmbedded(activity: Activity): Boolean {
    return ActivityEmbeddingController.getInstance(this).isActivityEmbedded(activity)
}

Java

boolean isActivityEmbedded(Activity activity) {
    return ActivityEmbeddingController.getInstance(this).isActivityEmbedded(activity);
}

최소 크기 제한

Android 시스템에서는 앱 매니페스트의 <layout> 요소에 지정된 최소 높이 및 너비를 삽입된 활동에 적용합니다. 애플리케이션에서 최소 높이와 너비를 지정하지 않으면 시스템 기본값(sw220dp)이 적용됩니다.

호스트가 삽입된 컨테이너의 크기를 최솟값보다 작게 조정하려고 하면 삽입된 컨테이너가 전체 작업 경계를 차지하도록 확장됩니다.

<activity-alias>

신뢰할 수 있거나 신뢰할 수 없는 활동 삽입이 <activity-alias> 요소와 함께 작동하려면 별칭이 아닌 타겟 활동에 android:knownActivityEmbeddingCerts 또는 android:allowUntrustedActivityEmbedding을 적용해야 합니다. 시스템 서버에서 보안을 확인하는 정책은 별칭이 아닌 타겟에 설정된 플래그를 기반으로 합니다.

호스트 애플리케이션

호스트 애플리케이션은 단일 앱 활동 삽입을 구현하는 것과 동일한 방식으로 교차 앱 활동 삽입을 구현합니다. SplitPairRuleSplitPairFilter 객체 또는 ActivityRuleActivityFilter 객체는 삽입된 활동 및 작업 창 분할을 지정합니다. 분할 규칙은 XML에서 정적으로 정의되거나 런타임에 Jetpack WindowManager API 호출을 사용하여 정의됩니다.

호스트 애플리케이션이 교차 앱 삽입을 선택하지 않은 활동을 삽입하려고 하면 활동이 전체 작업 경계를 차지합니다. 따라서 호스트 애플리케이션은 타겟 활동이 교차 앱 삽입을 허용하는지 여부를 알아야 합니다.

삽입된 활동이 동일한 작업에서 새 활동을 시작하고 이 새 활동이 교차 앱 삽입을 선택하지 않은 경우 활동은 삽입된 컨테이너에서 활동을 오버레이하는 대신 전체 작업 경계를 차지합니다.

호스트 애플리케이션은 활동이 동일한 작업에서 실행되는 경우 제한 없이 자체 활동을 삽입할 수 있습니다.

분할 예

전체 크기 창에서 분할

그림 15. 활동 A가 측면에 활동 B를 시작함

리팩터링할 필요가 없습니다. 정적으로 또는 런타임으로 분할의 구성을 정의한 다음 추가 매개변수 없이 Context#startActivity()를 호출할 수 있습니다.

<SplitPairRule>
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

기본적으로 분할

애플리케이션의 방문 페이지가 대형 화면에서 두 컨테이너로 분할되도록 설계된 경우 두 활동이 모두 생성되고 동시에 표현될 때 사용자 환경이 가장 좋습니다. 하지만 사용자가 기본 컨테이너의 활동과 상호작용(예: 사용자가 탐색 메뉴에서 항목 선택)할 때까지 분할의 보조 컨테이너에 콘텐츠를 사용하지 못할 수도 있습니다. 자리표시자 활동은 분할의 보조 컨테이너에 콘텐츠가 표시될 수 있을 때까지 공백을 채울 수 있습니다(위의 자리표시자 참고).

그림 16. 두 활동을 동시에 열어 생성된 분할. 활동 하나는 자리표시자

자리표시자로 분할을 만들려면 자리표시자를 만들고 기본 활동과 연결합니다.

<SplitPlaceholderRule
    window:placeholderActivityName=".PlaceholderActivity">
    <ActivityFilter
        window:activityName=".MainActivity"/>
</SplitPlaceholderRule>

앱이 인텐트를 수신하면 대상 활동이 활동 분할의 보조 부분으로 표시될 수 있습니다. 예를 들어 목록의 항목에 관한 정보가 포함된 세부정보 화면을 표시하는 요청이 있습니다. 작은 디스플레이에서는 전체 크기 작업 창에, 큰 기기에서는 목록 옆에 세부정보가 표시됩니다.

그림 17. 작은 화면에서는 단독으로, 대형 화면에서는 목록 활동과 함께 표시되는 딥 링크 세부정보 활동

실행 요청은 기본 활동으로 라우팅되어야 하고 대상 세부정보 활동은 분할에서 실행되어야 합니다. 시스템은 사용할 수 있는 디스플레이 너비에 따라 올바른 표시 방법(스택 또는 나란히 표시)을 자동으로 선택합니다.

Kotlin

override fun onCreate(savedInstanceState Bundle?) {
    . . .
    RuleController.getInstance(this)
        .addRule(SplitPairRule.Builder(filterSet).build())
    startActivity(Intent(this, DetailActivity::class.java))
}

Java

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    . . .
    RuleController.getInstance(this)
        .addRule(new SplitPairRule.Builder(filterSet).build());
    startActivity(new Intent(this, DetailActivity.class));
}

딥 링크 대상은 뒤로 탐색 스택에서 사용자가 사용할 수 있는 유일한 활동일 수도 있으므로 세부정보 활동을 닫고 기본 활동만 남기는 것을 방지하는 것이 좋습니다.

목록 활동과 세부정보 활동이 나란히 표시된 대형 디스플레이.
          뒤로 탐색이 세부정보 활동을 닫고 화면에 목록 활동을 남길 수 없음.

세부정보 활동만 있는 작은 디스플레이. 뒤로 탐색이 세부정보 활동을 닫고 목록 활동을 표시할 수 없음

대신 finishPrimaryWithSecondary 속성을 사용하여 두 활동을 동시에 종료할 수 있습니다.

<SplitPairRule
    window:finishPrimaryWithSecondary="always">
    <SplitPairFilter
        window:primaryActivityName=".ListActivity"
        window:secondaryActivityName=".DetailActivity"/>
</SplitPairRule>

아래의 구성 속성을 참고하세요.

분할 컨테이너의 여러 활동

분할 컨테이너에 여러 활동을 스택하면 사용자가 딥 콘텐츠에 액세스할 수 있습니다. 예를 들어 목록-세부정보 분할에서 사용자가 하위 세부정보 섹션으로 이동하되 기본 활동은 제자리에 유지해야 할 수도 있습니다.

그림 18. 작업 창의 보조 창에서 제자리에 열려 있는 활동

Kotlin

class DetailActivity {
    . . .
    fun onOpenSubDetail() {
      startActivity(Intent(this, SubDetailActivity::class.java))
    }
}

Java

public class DetailActivity {
    . . .
    void onOpenSubDetail() {
        startActivity(new Intent(this, SubDetailActivity.class));
    }
}

하위 세부정보 활동이 세부정보 활동 위에 놓여 세부정보 활동이 숨겨집니다.

그러면 사용자는 스택에서 뒤로 이동하여 이전의 세부정보 수준으로 돌아갈 수 있습니다.

그림 19. 스택 맨 위에서 삭제된 활동

여러 활동을 서로 위에 스택하는 것은 이러한 활동이 동일한 보조 컨테이너의 한 활동에서 실행되는 경우 기본 동작입니다. 활성 분할 내의 기본 컨테이너에서 실행된 활동도 활동 스택 맨 위에 있는 보조 컨테이너에 배치됩니다.

새 작업의 활동

분할 작업 창의 활동이 새 작업에서 활동을 시작하는 경우 이 새 작업은 분할을 포함하는 작업에서 분리되어 전체 크기 창으로 표시됩니다. 최근 항목 화면에는 분할에서의 작업과 새 작업, 두 가지 작업이 표시됩니다.

그림 20. 활동 B에서 활동 C를 새 작업으로 시작

활동 대체

활동이 보조 컨테이너 스택에서 대체될 수 있습니다. 예를 들어 기본 활동이 최상위 탐색에 사용되고 보조 활동이 선택된 대상인 경우가 있습니다. 최상위 탐색에서 선택한 각 항목은 보조 컨테이너에서 새 활동을 시작하고 이전에 보조 컨테이너에 존재하던 하나 이상의 활동을 삭제해야 합니다.

그림 21. 기본 창의 최상위 탐색 활동이 보조 창의 대상 활동을 대체함

탐색 선택이 변경될 때 앱이 보조 컨테이너의 활동을 종료하지 않으면 분할이 접힐 때(기기가 접힐 때) 뒤로 탐색이 혼란스러울 수도 있습니다. 예를 들어 기본 창에 메뉴가 있고 보조 창에 화면 A와 B가 스택되어 있는 경우 사용자가 휴대전화를 접으면 B는 A 위에, A는 메뉴 위에 배치됩니다. 사용자가 B에서 뒤로 이동하면 메뉴 대신 화면 A가 표시됩니다.

이 경우 화면 A를 백 스택에서 삭제해야 합니다.

기존 분할에서 측면으로 새 컨테이너에 실행할 때의 기본 동작은 새 보조 컨테이너를 맨 위에 두고 이전 컨테이너를 백 스택에 유지하는 것입니다. clearTop을 사용하여 이전 보조 컨테이너를 지우고 정상적으로 새 활동을 실행하도록 분할을 구성할 수 있습니다.

<SplitPairRule
    window:clearTop="true">
    <SplitPairFilter
        window:primaryActivityName=".Menu"
        window:secondaryActivityName=".ScreenA"/>
    <SplitPairFilter
        window:primaryActivityName=".Menu"
        window:secondaryActivityName=".ScreenB"/>
</SplitPairRule>

Kotlin

class MenuActivity {
    . . .
    fun onMenuItemSelected(selectedMenuItem: Int) {
        startActivity(Intent(this, classForItem(selectedMenuItem)))
    }
}

Java

public class MenuActivity {
    . . .
    void onMenuItemSelected(int selectedMenuItem) {
        startActivity(new Intent(this, classForItem(selectedMenuItem)));
    }
}

또는 동일한 보조 활동을 사용하고 기본(메뉴) 활동에서는 동일한 인스턴스로 확인되지만 상태 또는 UI 업데이트를 보조 컨테이너에 트리거하는 새 인텐트를 전송합니다.

여러 분할

앱은 추가 활동을 측면에 실행하여 여러 수준의 딥 탐색을 제공할 수 있습니다.

보조 컨테이너의 활동이 새 활동을 측면에 실행하면 기존 분할 위에 새 분할이 생성됩니다.

그림 22. 활동 B가 측면에 활동 C를 시작함

백 스택에는 이전에 열린 모든 활동이 포함되어 있으므로 사용자는 C를 종료한 후 A/B 분할로 이동할 수 있습니다.

스택 하나에 있는 활동 A, B, C. 활동은 위에서 아래로 C, B, A의 순서로 스택됨

새 분할을 만들려면 기존의 보조 컨테이너에서 측면으로 새 활동을 실행합니다. A/B 분할과 B/C 분할에 모두 구성을 선언하고 정상적으로 B에서 활동 C를 실행합니다.

<SplitPairRule>
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
    <SplitPairFilter
        window:primaryActivityName=".B"
        window:secondaryActivityName=".C"/>
</SplitPairRule>

Kotlin

class B {
    . . .
    fun onOpenC() {
        startActivity(Intent(this, C::class.java))
    }
}

Java

public class B {
    . . .
    void onOpenC() {
        startActivity(new Intent(this, C.class));
    }
}

분할 상태 변경에 관한 반응

앱의 다양한 활동에는 동일한 기능을 실행하는 UI 요소가 있을 수 있습니다. 예를 들어 계정 설정이 포함된 창을 여는 컨트롤이 있습니다.

그림 23. 기능적으로 동일한 UI 요소를 가진 여러 활동

공통된 UI 요소를 가진 두 활동이 분할되는 경우 중복되어 이 요소를 두 활동 모두에서 표시하면 혼란스러울 수 있습니다.

그림 24. 활동 분할에 중복된 UI 요소

활동이 분할에 포함되는 시점을 알아보려면 SplitController.splitInfoList 흐름을 확인하거나 SplitControllerCallbackAdapter에 리스너를 등록하여 분할 상태의 변경사항을 확인하세요. 그런 다음 UI를 다음과 같이 조정합니다.

Kotlin

val layout = layoutInflater.inflate(R.layout.activity_main, null)
val view = layout.findViewById<View>(R.id.infoButton)
lifecycleScope.launch {
    lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
        splitController.splitInfoList(this@SplitDeviceActivity) // The activity instance.
            .collect { list ->
                view.visibility = if (list.isEmpty()) View.VISIBLE else View.GONE
            }
    }
}

Java

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    . . .
    new SplitControllerCallbackAdapter(SplitController.getInstance(this))
        .addSplitListener(
            this,
            Runnable::run,
            splitInfoList -> {
                View layout = getLayoutInflater().inflate(R.layout.activity_main, null);
                layout.findViewById(R.id.infoButton).setVisibility(
                    splitInfoList.isEmpty() ? View.VISIBLE : View.GONE);
            });
}

코루틴은 모든 수명 주기 상태에서 실행할 수 있지만, 일반적으로 리소스를 보존하기 위해 STARTED 상태에서 실행됩니다 (자세한 내용은 수명 주기 인식 구성요소와 함께 Kotlin 코루틴 사용 참고).

활동이 중지되는 시점을 포함하여 모든 수명 주기 상태에서 콜백을 호출할 수 있습니다. 리스너는 일반적으로 onStart()에 등록되어야 하고 onStop()에서 등록 취소되어야 합니다.

전체 크기 창 모달

일부 활동은 지정된 작업이 실행될 때까지 사용자가 애플리케이션과 상호작용하지 못하도록 차단합니다. 예를 들어 로그인 화면 활동, 정책 확인 화면, 오류 메시지 등이 있습니다. 모달 활동은 분할에 표시되지 않아야 합니다.

확장 구성을 사용하여 활동이 항상 작업 창을 채우도록 강제할 수 있습니다.

<ActivityRule
    window:alwaysExpand="true">
    <ActivityFilter
        window:activityName=".FullWidthActivity"/>
</ActivityRule>

활동 종료

사용자는 디스플레이 가장자리에서 스와이프하여 분할의 한쪽 측면의 활동을 종료할 수 있습니다.

그림 25. 스와이프 동작으로 활동 B 종료
그림 26. 스와이프 동작으로 활동 A 종료

기기가 동작 탐색 대신 뒤로 버튼을 사용하도록 설정된 경우 포커스가 맞춰진 활동(마지막으로 터치되거나 실행된 활동)으로 입력이 전송됩니다.

한 컨테이너의 모든 활동이 종료될 때 반대 컨테이너에 미치는 영향은 분할 구성에 따라 다릅니다.

구성 속성

분할 쌍 규칙 속성을 지정하면 분할의 한쪽에서 모든 활동이 종료될 때 분할의 다른 쪽에 있는 활동에 어떤 영향을 미치는지 구성할 수 있습니다. 속성은 다음과 같습니다.

  • window:finishPrimaryWithSecondary: 보조 컨테이너의 모든 활동이 종료될 때 기본 컨테이너의 활동에 미치는 영향
  • window:finishSecondaryWithPrimary: 기본 컨테이너의 모든 활동이 종료될 때 보조 컨테이너의 활동에 미치는 영향

가능한 속성 값은 다음과 같습니다.

  • always: 연결된 컨테이너의 활동을 항상 종료함
  • never: 연결된 컨테이너의 활동을 종료하지 않음
  • adjacent: 두 컨테이너가 서로 인접해 표시되는 경우 연결된 컨테이너에서 활동을 종료하지만 두 컨테이너가 스택된 경우에는 활동을 종료하지 않음

예를 들면 다음과 같습니다.

<SplitPairRule
    <!-- Do not finish primary container activities when all secondary container activities finish. -->
    window:finishPrimaryWithSecondary="never"
    <!-- Finish secondary container activities when all primary container activities finish. -->
    window:finishSecondaryWithPrimary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

기본 구성

분할된 한 컨테이너의 모든 활동이 종료되면 나머지 컨테이너가 전체 창을 차지합니다.

<SplitPairRule>
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

활동 A와 활동 B가 포함된 분할. A가 종료되어 B가 전체 창을 차지하게 됨.

활동 A와 활동 B가 포함된 분할. B가 종료되어 A가 전체 창을 차지하게 됨

활동 함께 종료

보조 컨테이너의 모든 활동이 종료되면 기본 컨테이너의 활동을 자동으로 종료합니다.

<SplitPairRule
    window:finishPrimaryWithSecondary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

활동 A와 활동 B가 포함된 분할. B의 종료에 따라 A도 종료되어 작업 창이 비게 됨.

활동 A와 활동 B가 포함된 분할. A가 종료되어 B가 작업 창에 단독으로 남음

기본 컨테이너의 모든 활동이 종료되면 보조 컨테이너의 활동을 자동으로 종료합니다.

<SplitPairRule
    window:finishSecondaryWithPrimary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

활동 A와 활동 B가 포함된 분할. A의 종료에 따라 B도 종료되어 작업 창이 비게 됨.

활동 A와 활동 B가 포함된 분할. B가 종료되어 A가 작업 창에 단독으로 남음

기본 컨테이너 또는 보조 컨테이너의 모든 활동이 종료되면 활동을 함께 종료합니다.

<SplitPairRule
    window:finishPrimaryWithSecondary="always"
    window:finishSecondaryWithPrimary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

활동 A와 활동 B가 포함된 분할. A의 종료에 따라 B도 종료되어 작업 창이 비게 됨.

활동 A와 활동 B가 포함된 분할. B의 종료에 따라 A도 종료되어 작업 창이 비게 됨.

컨테이너의 여러 활동 종료

분할 컨테이너에 여러 활동이 스택된 경우 스택 맨 아래에 있는 활동을 종료해도 맨 위에 있는 활동이 자동으로 종료되지는 않습니다.

예를 들어 보조 컨테이너에 두 활동, 즉 활동 B 위에 활동 C가 있으며

B 위에 스택된 활동 C를 포함하는 보조 액티비티 스택은 활동 A를 포함하는 기본 액티비티 스택 위에 스택되어 있습니다.

분할 구성이 활동 A와 활동의 B의 구성에 의해 정의되는 경우

<SplitPairRule>
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

맨 위 활동을 종료하면 분할이 유지됩니다.

기본 컨테이너의 활동 A와 보조 컨테이너의 활동 B, C로 분할되며 C는 B 위에 스택됨. C가 종료되고 A와 B가 활동 분할에 남음

보조 컨테이너의 맨 아래(루트) 활동을 종료해도 그 위에 있는 활동은 삭제되지 않으며 분할도 유지됩니다.

기본 컨테이너의 활동 A와 보조 컨테이너의 활동 B, C로 분할되며 C는 B 위에 스택됨. B가 종료되고 A와 C가 활동 분할에 남음

기본 활동과 함께 보조 활동을 종료하는 경우와 같이 활동을 함께 종료하기 위한 추가 규칙도 실행됩니다.

<SplitPairRule
    window:finishSecondaryWithPrimary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

기본 컨테이너의 활동 A와 보조 컨테이너의 활동 B, C로 분할되며 C는 B 위에 스택됨. A가 종료되고 B와 C도 종료됨

기본과 보조를 함께 종료하도록 분할이 구성된 경우 다음과 같습니다.

<SplitPairRule
    window:finishPrimaryWithSecondary="always"
    window:finishSecondaryWithPrimary="always">
    <SplitPairFilter
        window:primaryActivityName=".A"
        window:secondaryActivityName=".B"/>
</SplitPairRule>

기본 컨테이너의 활동 A와 보조 컨테이너의 활동 B, C로 분할되며 C는 B 위에 스택됨. C가 종료되고 A와 B가 활동 분할에 남음

기본 컨테이너의 활동 A와 보조 컨테이너의 활동 B, C로 분할되며 C는 B 위에 스택됨. B가 종료되고 A와 C가 활동 분할에 남음

기본 컨테이너의 활동 A와 보조 컨테이너의 활동 B, C로 분할되며 C는 B 위에 스택됨. A가 종료되고 B와 C도 종료됨

런타임 시 분할 속성 변경

현재 활성 상태이며 표시되는 분할의 속성은 변경할 수 없습니다. 분할 규칙을 변경하면 추가 활동 실행 및 새 컨테이너에 영향을 주지만 기존의 활성 분할에는 영향을 주지 않습니다.

활성 분할의 속성을 변경하려면 분할의 측면 활동을 종료한 후 새 구성으로 다시 측면으로 실행합니다.

분할에서 전체 크기 창으로 활동 추출

측면 활동 전체 크기 창을 표시하는 새 구성을 만든 다음 동일한 인스턴스로 확인되는 인텐트로 활동을 다시 실행합니다.

런타임 시 분할 지원 확인

활동 삽입은 Android 12L(API 수준 32) 이상에서 지원되지만 이전 플랫폼 버전을 실행하는 일부 기기에서도 사용할 수 있습니다. 런타임 시 이 기능의 사용 가능성을 확인하려면 SplitController.splitSupportStatus 속성 또는 SplitController.getSplitSupportStatus() 메서드를 사용하세요.

Kotlin

if (SplitController.getInstance(this).splitSupportStatus ==
     SplitController.SplitSupportStatus.SPLIT_AVAILABLE) {
     // Device supports split activity features.
}

Java

if (SplitController.getInstance(this).getSplitSupportStatus() ==
     SplitController.SplitSupportStatus.SPLIT_AVAILABLE) {
     // Device supports split activity features.
}

분할이 지원되지 않으면 활동이 비활동 삽입 모델에 따라 액티비티 스택의 맨 위에서 실행됩니다.

시스템 재정의 방지

Android 기기 제조업체(OEM)는 기기 시스템의 기능으로 활동 삽입을 구현할 수 있습니다. 시스템은 여러 활동이 있는 앱에 분할 규칙을 지정하여 앱의 윈도잉 동작을 재정의합니다. 시스템 재정의는 여러 활동이 있는 앱에 시스템 정의 활동 삽입 모드를 강제 적용합니다.

시스템 활동 삽입은 앱을 변경하지 않고도 list-detail과 같은 다중 창 레이아웃을 통해 앱 프레젠테이션을 개선할 수 있습니다. 그러나 시스템의 활동 삽입으로 인해 잘못된 앱 레이아웃, 버그가 발생하거나 시스템 활동 삽입이 앱에서 구현된 활동 삽입과 충돌할 수도 있습니다.

앱은 앱 매니페스트 파일에 속성을 설정하여 시스템 활동 삽입을 방지하거나 허용할 수 있습니다. 예를 들면 다음과 같습니다.

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application>
        <property
            android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE"
            android:value="true|false" />
    </application>
</manifest>

속성 이름은 Jetpack WindowManager의 WindowProperties 객체에 정의됩니다. 앱이 활동 삽입을 구현하거나 시스템에서 활동 삽입 규칙을 앱에 적용하지 못하도록 하려면 이 값을 false로 설정합니다. 이 값을 true로 설정하면 시스템이 시스템에서 정의한 활동 삽입을 앱에 적용할 수 있습니다.

제한, 제약, 주의사항

  • 작업의 루트 활동의 소유자로 식별되는 작업의 호스트 앱만 다른 활동을 구성하고 작업에 삽입할 수 있습니다. 삽입 및 분할을 지원하는 활동이 다른 애플리케이션에 속한 작업에서 실행되는 경우 이러한 활동에는 삽입 및 분할이 작동하지 않습니다.
  • 단일 작업 내에서만 활동을 구성할 수 있습니다. 새 작업에서 활동을 실행하면 항상 기존 분할 외부의 새로운 확장 창에 활동이 배치됩니다.
  • 동일한 프로세스의 활동만 구성하고 분할에 배치할 수 있습니다. SplitInfo 콜백은 동일한 프로세스에 속한 활동만 보고합니다. 다른 프로세스의 활동에 관해 알 수 있는 방법이 없기 때문입니다.
  • 각 쌍 또는 단일 활동 규칙은 규칙 등록 후 발생하는 활동 실행에만 적용됩니다. 기존 분할이나 시각적 속성을 업데이트할 수 있는 방법은 현재 없습니다.
  • 분할 쌍 필터 구성은 활동을 완전히 실행할 때 사용된 인텐트와 일치해야 합니다. 이러한 일치는 애플리케이션 프로세스에서 새로운 활동이 시작될 때 발생합니다. 따라서 암시적 인텐트를 사용할 때 시스템 프로세스의 후반에 확인되는 구성요소 이름을 모를 수도 있습니다. 실행 시점에 구성요소 이름을 알 수 없는 경우 와일드 카드('*/*')를 대신 사용할 수 있으며 인텐트 작업을 기반으로 필터링할 수 있습니다.
  • 분할이 생성된 후 컨테이너 간에 또는 분할 내부나 외부로 활동을 이동할 수 있는 방법은 현재 없습니다. 일치하는 규칙이 있는 새 활동이 실행될 때만 WindowManager 라이브러리에 의해 분할이 생성되며 분할 컨테이너의 마지막 활동이 종료될 때 분할이 소멸됩니다.
  • 구성이 변경될 때 활동을 다시 실행할 수 있으므로 분할을 만들거나 삭제하고 활동 경계가 변경되면 활동은 이전 인스턴스가 완전히 소멸되고 새 인스턴스가 생성되는 과정을 거칠 수 있습니다. 따라서 앱 개발자는 수명 주기 콜백에서 새 활동을 실행하는 등의 작업에 주의해야 합니다.
  • 활동 삽입을 지원하려면 기기에 창 확장 프로그램 인터페이스가 포함되어야 합니다. Android 12L(API 수준 32) 이상을 실행하는 거의 모든 대형 화면 기기에는 이 인터페이스가 포함되어 있습니다. 그러나 여러 활동을 실행할 수 없는 일부 대형 화면 기기에는 창 확장 프로그램 인터페이스가 포함되어 있지 않습니다. 대형 화면 기기에서 멀티 윈도우 모드를 지원하지 않으면 활동 삽입을 지원하지 않을 수도 있습니다.

추가 리소스