Встраивание активности

Встраивание действий оптимизирует работу приложений на устройствах с большим экраном, разделяя окно задач приложения между двумя действиями или двумя экземплярами одного и того же действия.

Рисунок 1. Приложение «Настройки» с расположенными рядом действиями.

Если ваше приложение состоит из нескольких действий, встраивание действий позволит вам улучшить пользовательский опыт на планшетах, складных устройствах и устройствах ChromeOS.

Внедрение действий не требует рефакторинга кода. Вы определяете, как ваше приложение будет отображать действия — рядом или в стопке, — создавая XML-файл конфигурации или выполняя вызовы API Jetpack WindowManager .

Поддержка небольших экранов поддерживается автоматически. Когда ваше приложение работает на устройстве с небольшим экраном, действия располагаются друг над другом. На больших экранах действия отображаются рядом. Система определяет представление на основе созданной вами конфигурации — логика ветвления не требуется.

Функция встраивания действий учитывает изменения ориентации устройства и безупречно работает на складных устройствах, складывая и раскладывая действия по мере того, как устройство складывается и раскладывается.

Встраивание активности поддерживается на большинстве устройств с большим экраном под управлением Android 12L (уровень API 32) и выше.

Разделить окно задач

Внедрение активности разделяет окно задач приложения на два контейнера: основной и вспомогательный. Контейнеры содержат активности, запущенные из основной активности или из других активностей, уже находящихся в контейнерах.

Действия размещаются во вторичном контейнере по мере их запуска, а вторичный контейнер размещается поверх основного контейнера на небольших экранах, поэтому размещение действий и обратная навигация соответствуют порядку действий, уже встроенных в ваше приложение.

Встраивание действий позволяет отображать действия различными способами. Ваше приложение может разделить окно задач, запустив два действия одновременно рядом или одно над другим:

Рисунок 2. Два действия рядом и одно над другим.

Действие, занимающее все окно задач, может создать разделение путем запуска нового действия рядом с:

Рисунок 3. Действие A запускает действие B сбоку.

Действия, которые уже находятся в разделении и совместно используют окно задач, могут запускать другие действия следующими способами:

  • Сбоку поверх другого действия:

    Рисунок 4. Действие A запускает действие C сбоку от действия B.
  • В сторону и смещаем разделение вбок, скрывая предыдущую основную деятельность:

    Рисунок 5. Действие B запускает действие C в сторону и смещает разделение вбок.
  • Запустить действие поверх предыдущего, то есть в том же стеке действий:

    Рисунок 6. Действие B запускает действие C без дополнительных флагов намерений.
  • Запустить полное окно активности в той же задаче:

    Рисунок 7. Действие A или действие B запускает действие C, которое заполняет окно задач.

Обратная навигация

Различные типы приложений могут иметь различные правила обратной навигации в состоянии окна разделенной задачи в зависимости от зависимостей между действиями или того, как пользователи запускают событие возврата, например:

  • Совместное выполнение: если действия связаны и одно не должно отображаться без другого, можно настроить обратную навигацию для завершения обоих.
  • Работа в одиночку: если действия полностью независимы, обратная навигация по действию не влияет на состояние другого действия в окне задач.

При использовании навигации с помощью кнопок событие «Назад» отправляется последнему активному действию.

Для навигации с помощью жестов:

  • Android 14 (уровень API 34) и ниже — событие «Назад» отправляется в Activity, где был выполнен жест. При смахивании от левого края экрана событие «Назад» отправляется в Activity в левой части разделённого окна. При смахивании от правого края экрана событие «Назад» отправляется в Activity в правой части.

  • Android 15 (уровень API 35) и выше

    • При работе с несколькими действиями в одном приложении жест завершает верхнее действие независимо от направления свайпа, обеспечивая более унифицированный опыт.

    • В сценариях, включающих два действия из разных приложений (наложение), событие возврата направляется к последнему действию в фокусе, что соответствует поведению навигации с помощью кнопок.

Многопанельный макет

Jetpack WindowManager позволяет создавать активности с многопанельным макетом на устройствах с большим экраном под управлением Android 12L (API уровня 32) и выше, а также на некоторых устройствах с более ранними версиями платформы. Существующие приложения, основанные на нескольких активностях, а не на фрагментах или макетах на основе представлений, таких как SlidingPaneLayout могут улучшить пользовательский интерфейс на больших экранах без рефакторинга исходного кода.

Одним из распространённых примеров является разделение списка и детализации. Для обеспечения высокого качества отображения система запускает действие списка, а затем приложение немедленно запускает действие детализации. Система перехода дожидается отрисовки обоих действий, а затем отображает их вместе. Для пользователя эти два действия запускаются как одно.

Рисунок 8. Два действия запущены одновременно в многопанельном макете.

Разделить атрибуты

Вы можете указать пропорции окна задачи между разделенными контейнерами и то, как контейнеры располагаются относительно друг друга.

Для правил, определенных в XML-файле конфигурации, задайте следующие атрибуты:

  • splitRatio : устанавливает пропорции контейнера. Значение — число с плавающей запятой в открытом интервале (0.0, 1.0).
  • splitLayoutDirection : определяет, как будут располагаться разделённые контейнеры относительно друг друга. Возможные значения:
    • ltr : слева направо
    • rtl : справа налево
    • locale : ltr или rtl определяется на основе настроек locale

Примеры см. в разделе «Конфигурация XML» .

Для правил, созданных с использованием API WindowManager, создайте объект SplitAttributes с помощью SplitAttributes.Builder и вызовите следующие методы построителя:

  • setSplitType() : устанавливает пропорции разделяемых контейнеров. Допустимые аргументы, включая метод SplitAttributes.SplitType.ratio() , см. в SplitAttributes.SplitType .
  • setLayoutDirection() : задаёт расположение контейнеров. Возможные значения см. в SplitAttributes.LayoutDirection .

Примеры см. в разделе API WindowManager .

Рисунок 9. Два вида деятельности, расположенные слева направо, но с разными коэффициентами разделения.

Раздельная ориентация

Размеры и соотношение сторон дисплея определяют расположение элементов управления в области вставки элементов управления. На больших горизонтальных дисплеях элементы управления отображаются рядом друг с другом; на высоких вертикальных дисплеях или настольных дисплеях на складных устройствах — один над другим.

Ориентацию разделения можно задать с помощью калькулятора SplitAttributes SplitController . Калькулятор вычисляет SplitAttributes для активного SplitRule .

Используйте калькулятор для разделения родительского контейнера в разных направлениях для разных состояний устройства, например:

Котлин

if (WindowSdkExtensions.getInstance().extensionVersion >= 2) {
    SplitController.getInstance(this).setSplitAttributesCalculator { params ->
        val parentConfiguration = params.parentConfiguration
        val builder = SplitAttributes.Builder()
        return@setSplitAttributesCalculator if (parentConfiguration.screenWidthDp >= 840) {
            // Side-by-side dual-pane layout for wide displays.
            builder
                .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
                .build()
        } else if (parentConfiguration.screenHeightDp >= 600) {
            // Horizontal split for tall displays.
            builder
                .setLayoutDirection(SplitAttributes.LayoutDirection.BOTTOM_TO_TOP)
                .build()
        } else {
            // Fallback to expand the secondary container.
            builder
                .setSplitType(SPLIT_TYPE_EXPAND)
                .build()
        }
    }
}

Ява

if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 2) {
    SplitController.getInstance(this).setSplitAttributesCalculator(params -> {
        Configuration parentConfiguration = params.getParentConfiguration();
        SplitAttributes.Builder builder = new SplitAttributes.Builder();
        if (parentConfiguration.screenWidthDp >= 840) {
            // Side-by-side dual-pane layout for wide displays.
            return builder
                .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
                .build();
        } else if (parentConfiguration.screenHeightDp >= 600) {
            // Horizontal split for tall displays.
            return builder
                .setLayoutDirection(SplitAttributes.LayoutDirection.BOTTOM_TO_TOP)
                .build();
        } else {
            // Fallback to expand the secondary container.
            return builder
                .setSplitType(SplitType.SPLIT_TYPE_EXPAND)
                .build();
        }
    });
}

На складных устройствах можно разделить экран по вертикали, если устройство имеет альбомную ориентацию, отобразить одно действие, если устройство имеет портретную ориентацию, и разделить экран по горизонтали, если устройство находится в настольном положении:

Котлин

if (WindowSdkExtensions.getInstance().extensionVersion >= 2) {
    SplitController.getInstance(this).setSplitAttributesCalculator { params ->
        val tag = params.splitRuleTag
        val parentWindowMetrics = params.parentWindowMetrics
        val parentConfiguration = params.parentConfiguration
        val foldingFeatures =
            params.parentWindowLayoutInfo.displayFeatures.filterIsInstance<FoldingFeature>()
        val feature = if (foldingFeatures.size == 1) foldingFeatures[0] else null
        val builder = SplitAttributes.Builder()
        builder.setSplitType(SPLIT_TYPE_HINGE)
        return@setSplitAttributesCalculator if (feature?.isSeparating == true) {
            // Horizontal split for tabletop posture.
            builder
                .setSplitType(SPLIT_TYPE_HINGE)
                .setLayoutDirection(
                    if (feature.orientation == FoldingFeature.Orientation.HORIZONTAL) {
                        SplitAttributes.LayoutDirection.BOTTOM_TO_TOP
                    } else {
                        SplitAttributes.LayoutDirection.LOCALE
                    }
                )
                .build()
        } else if (parentConfiguration.screenWidthDp >= 840) {
            // Side-by-side dual-pane layout for wide displays.
            builder
                .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
                .build()
        } else {
            // No split for tall displays.
            builder
                .setSplitType(SPLIT_TYPE_EXPAND)
                .build()
        }
    }
}

Ява

if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 2) {
    SplitController.getInstance(this).setSplitAttributesCalculator(params -> {
        String tag = params.getSplitRuleTag();
        WindowMetrics parentWindowMetrics = params.getParentWindowMetrics();
        Configuration parentConfiguration = params.getParentConfiguration();
        List<FoldingFeature> foldingFeatures =
            params.getParentWindowLayoutInfo().getDisplayFeatures().stream().filter(
                    item -> item instanceof FoldingFeature)
                .map(item -> (FoldingFeature) item)
                .collect(Collectors.toList());
        FoldingFeature feature = foldingFeatures.size() == 1 ? foldingFeatures.get(0) : null;
        SplitAttributes.Builder builder = new SplitAttributes.Builder();
        builder.setSplitType(SplitType.SPLIT_TYPE_HINGE);
        if (feature != null && feature.isSeparating()) {
            // Horizontal slit for tabletop posture.
            return builder
                .setSplitType(SplitType.SPLIT_TYPE_HINGE)
                .setLayoutDirection(
                    feature.getOrientation() == FoldingFeature.Orientation.HORIZONTAL
                        ? SplitAttributes.LayoutDirection.BOTTOM_TO_TOP
                        : SplitAttributes.LayoutDirection.LOCALE)
                .build();
        }
        else if (parentConfiguration.screenWidthDp >= 840) {
            // Side-by-side dual-pane layout for wide displays.
            return builder
                .setLayoutDirection(SplitAttributes.LayoutDirection.LOCALE)
                .build();
        } else {
            // No split for tall displays.
            return builder
                .setSplitType(SplitType.SPLIT_TYPE_EXPAND)
                .build();
        }
    });
}

Заполнители

Действия-заполнители — это пустые вторичные действия, занимающие область разделения действий. В конечном итоге они должны быть заменены другими действиями, содержащими контент. Например, действие-заполнитель может занимать вторичную сторону разделения действий в макете списка с подробными данными до тех пор, пока не будет выбран элемент списка. В этом случае действие, содержащее подробную информацию о выбранном элементе списка, заменит плейсхолдер.

По умолчанию система отображает плейсхолдеры только при наличии достаточного места для разделения активности. Плейсхолдеры автоматически завершают работу, когда ширина или высота экрана становится слишком маленькой для отображения разделения. При наличии свободного места система перезапускает плейсхолдер с переинициализированным состоянием.

Рисунок 10. Складное устройство складывается и раскладывается. Действие плейсхолдера завершается и воссоздаётся по мере изменения размера дисплея.

Однако атрибут stickyPlaceholder правила SplitPlaceholderRule или метод setSticky() класса SplitPlaceholder.Builder могут переопределить поведение по умолчанию. Если атрибут или метод задаёт значение true , система отображает плейсхолдер как самое верхнее действие в окне задач при уменьшении размера экрана с двухпанельного до однопанельного (см. пример в разделе «Конфигурация Split» ).

Рисунок 11. Складное устройство складывается и раскладывается. Заполнитель активности — липкий.

Изменения размера окна

Если изменения конфигурации устройства уменьшают ширину окна задач таким образом, что оно становится недостаточно большим для многопанельной компоновки (например, когда складное устройство с большим экраном складывается из размера планшета в размер телефона или окно приложения изменяет размер в многооконном режиме), действия, не являющиеся заполнителями, во вторичной панели окна задач накладываются поверх действий в основной панели.

Действия плейсхолдера отображаются только при наличии достаточной ширины экрана для разделения. На экранах с меньшим размером плейсхолдер автоматически исчезает. Когда область отображения снова становится достаточно большой, плейсхолдер создаётся заново. (См. раздел «Плейсхолдеры» ).

Наложение действий возможно, поскольку WindowManager размещает действия во вторичной панели выше действий в первичной панели по оси z.

Несколько действий на вторичной панели

Действие B запускает действие C на месте без дополнительных флагов намерений:

Разделение действий, содержащее действия A, B и C, при этом C расположено поверх B.

в результате получается следующий z-порядок действий в одной и той же задаче:

Вторичный стек действий, содержащий действие C, расположен поверх B. Вторичный стек действий расположен поверх основного стека действий, содержащего действие A.

Таким образом, в меньшем окне задач приложение сжимается до одной активности с C наверху стека:

Маленькое окно, показывающее только активность C.

При возврате в меньшее окно осуществляется переход по действиям, наложенным друг на друга.

Если восстановить конфигурацию окна задач до большего размера, способного вместить несколько панелей, действия снова будут отображаться рядом.

Сложенные сплиты

Действие B запускает действие C в сторону и сдвигает разделение вбок:

Окно задач, в котором показаны действия A и B, затем действия B и C.

Результатом является следующий z-порядок действий в одной и той же задаче:

Действия A, B и C в одном стеке. Действия расположены в следующем порядке сверху вниз: C, B, A.

В меньшем окне задач приложение сжимается до одной активности с C наверху:

Маленькое окно, показывающее только активность C.

Фиксированная портретная ориентация

Параметр манифеста android:screenOrientation позволяет приложениям ограничивать действия портретной или альбомной ориентацией. Чтобы улучшить пользовательский опыт на устройствах с большим экраном, таких как планшеты и складные устройства, производители устройств (OEM) могут игнорировать запросы на ориентацию экрана и отображать приложение в портретной ориентации на альбомных дисплеях или в альбомной ориентации на портретных дисплеях.

Рисунок 12. Действия в формате Letterboxed: фиксированная книжная ориентация на устройстве с альбомной ориентацией (слева), фиксированная альбомная ориентация на устройстве с портретной ориентацией (справа).

Аналогичным образом, при включении встроенных действий OEM-производители могут настраивать устройства для отображения действий в портретной ориентации в формате Letterbox в альбомной ориентации на больших экранах (ширина ≥ 600 dp). Когда действие в портретной ориентации запускает второе действие, устройство может отображать оба действия рядом на двухпанельном дисплее.

Рисунок 13. Действие А в фиксированном портрете запускает действие В сбоку.

Всегда добавляйте свойство android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED в файл манифеста приложения, чтобы сообщить устройствам, что ваше приложение поддерживает встраивание активности (см. раздел «Конфигурация разделения» ). После этого устройства, настроенные OEM-производителем, смогут определить, следует ли отображать активности в режиме «почтовый ящик» с фиксированной портретной ориентацией.

Раздельная конфигурация

Правила разделения настраивают разделение активности. Правила разделения определяются в XML-файле конфигурации или с помощью вызовов API Jetpack WindowManager .

В любом случае ваше приложение должно получить доступ к библиотеке WindowManager и сообщить системе, что приложение реализовало внедрение активности.

Сделайте следующее:

  1. Добавьте последнюю зависимость библиотеки WindowManager в файл build.gradle на уровне модуля вашего приложения, например:

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

    Библиотека WindowManager предоставляет все компоненты, необходимые для внедрения активности.

  2. Сообщите системе, что в вашем приложении реализовано внедрение активности.

    Добавьте свойство android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED к элементу <application> файла манифеста приложения и задайте значение 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-файл конфигурации и делает правила доступными для системы. Initializer библиотеки Jetpack Startup делает XML-файл доступным для RuleController при запуске приложения, чтобы правила вступали в силу при запуске любых действий.

    Чтобы создать инициализатор, выполните следующие действия:

    1. Добавьте последнюю зависимость библиотеки Jetpack Startup в файл build.gradle на уровне модуля, например:

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

    2. Создайте класс, реализующий интерфейс Initializer .

      Инициализатор делает правила разделения доступными для RuleController передавая идентификатор XML-файла конфигурации ( main_split_config.xml ) методу RuleController.parseRules() .

      Котлин

      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()
          }
      }

      Ява

      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 обнаруживает и инициализирует SplitInitializer до вызова метода onCreate() приложения. В результате правила разделения вступают в силу с момента запуска основной активности приложения.

API оконного менеджера

Вы можете реализовать внедрение действий программно, используя несколько вызовов API. Вызовы следует выполнять в методе onCreate() подкласса Application , чтобы гарантировать применение правил до запуска любых действий.

Чтобы программно создать разделение активности, выполните следующие действия:

  1. Создайте правило разделения:

    1. Создайте SplitPairFilter , который определяет действия, которые разделяют это разделение:

      Котлин

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

      Ява

      SplitPairFilter splitPairFilter = new SplitPairFilter(
         new ComponentName(this, ListActivity.class),
         new ComponentName(this, DetailActivity.class),
         null
      );

    2. Добавить фильтр в набор фильтров:

      Котлин

      val filterSet = setOf(splitPairFilter)

      Ява

      Set<SplitPairFilter> filterSet = new HashSet<>();
      filterSet.add(splitPairFilter);
      ```

    3. Создайте атрибуты макета для разделения:

      Котлин

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

      Ява

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

      SplitAttributes.Builder создает объект, содержащий атрибуты макета:

      • setSplitType() : определяет, как доступная область отображения распределяется между каждым контейнером активности. Тип разделения определяет долю доступной области отображения, выделяемую основному контейнеру; вторичный контейнер занимает оставшуюся часть доступной области отображения.
      • setLayoutDirection() : определяет, как контейнеры активности располагаются относительно друг друга, начиная с основного контейнера.
    4. Создайте правило SplitPairRule :

      Котлин

      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()

      Ява

      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_DEFAULT и setMaxAspectRatioInLandscape() . Значение по умолчанию для альбомной ориентации — ALWAYS_ALLOW .
      • setFinishPrimaryWithSecondary() : устанавливает, как завершение всех действий во вторичном контейнере влияет на действия в первичном контейнере. NEVER указывает, что система не должна завершать первичные действия после завершения всех действий во вторичном контейнере (см. раздел Завершение действий ).
      • setFinishSecondaryWithPrimary() : устанавливает, как завершение всех действий в первичном контейнере влияет на действия во вторичном контейнере. ALWAYS указывает, что система должна всегда завершать действия во вторичном контейнере после завершения всех действий в первичном контейнере (см. раздел Завершение действий ).
      • setClearTop() : определяет, завершаются ли все действия во вторичном контейнере при запуске нового действия в контейнере. Значение false указывает, что новые действия накладываются поверх действий, уже находящихся во вторичном контейнере.
    5. Получите единичный экземпляр WindowManager RuleController и добавьте правило:

      Котлин

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

      Ява

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

    6. Создайте заполнитель для вторичного контейнера, если контент недоступен:

    7. Создайте ActivityFilter , который определяет активность, с которой заполнитель разделяет разделение окна задачи:

      Котлин

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

      Ява

      ActivityFilter placeholderActivityFilter = new ActivityFilter(
          new ComponentName(this, ListActivity.class),
          null
      );

    8. Добавить фильтр в набор фильтров:

      Котлин

      val placeholderActivityFilterSet = setOf(placeholderActivityFilter)

      Ява

      Set<ActivityFilter> placeholderActivityFilterSet = new HashSet<>();
      placeholderActivityFilterSet.add(placeholderActivityFilter);

    9. Создайте правило SplitPlaceholderRule :

      Котлин

      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()

      Ява

      SplitPlaceholderRule splitPlaceholderRule = new SplitPlaceholderRule.Builder(
            placeholderActivityFilterSet,
            new Intent(this, 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_DEFAULT и setMaxAspectRatioInLandscape() . Значение по умолчанию для альбомной ориентации — ALWAYS_ALLOW .
      • setFinishPrimaryWithPlaceholder() : устанавливает, как завершение действия заполнителя влияет на действия в основном контейнере. ALWAYS указывает, что система всегда должна завершать действия в основном контейнере после завершения действия заполнителя (см. раздел Завершение действий ).
      • setSticky() : определяет, будет ли отображаться действие заполнителя поверх стека действий на небольших дисплеях после того, как заполнитель впервые появится в разделе с достаточной минимальной шириной.
    10. Добавьте правило в WindowManager RuleController :

      Котлин

      ruleController.addRule(splitPlaceholderRule)

      Ява

      ruleController.addRule(splitPlaceholderRule);

  2. Укажите виды деятельности, которые никогда не должны быть частью разделения:

    1. Создайте ActivityFilter , который определяет активность, которая всегда должна занимать всю область отображения задачи:

      Котлин

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

      Ява

      ActivityFilter expandedActivityFilter = new ActivityFilter(
          new ComponentName(this, ExpandedActivity.class),
          null
      );

    2. Добавить фильтр в набор фильтров:

      Котлин

      val expandedActivityFilterSet = setOf(expandedActivityFilter)

      Ява

      Set<ActivityFilter> expandedActivityFilterSet = new HashSet<>();
      expandedActivityFilterSet.add(expandedActivityFilter);

    3. Создайте ActivityRule :

      Котлин

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

      Ява

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

      ActivityRule.Builder создает и настраивает правило:

      • expandedActivityFilterSet : Содержит фильтры активности, которые определяют, когда применять правило, путем идентификации действий, которые необходимо исключить из разделений.
      • setAlwaysExpand() : указывает, должно ли действие заполнять все окно задачи.
    4. Добавьте правило в WindowManager RuleController :

      Котлин

      ruleController.addRule(activityRule)

      Ява

      ruleController.addRule(activityRule);

Встраивание между приложениями

В Android 13 (уровень API 33) и выше приложения могут встраивать активности из других приложений. Кросс-приложение (или кросс- UID ) встраивает активности, обеспечивая визуальную интеграцию активностей из нескольких приложений Android. Система отображает активность основного приложения и встроенную активность другого приложения на экране рядом или сверху и снизу, как при встраивании активности одного приложения.

Например, приложение «Настройки» может встроить функцию выбора обоев из приложения WallpaperPicker:

Рисунок 14. Приложение «Настройки» (меню слева) со встроенным селектором обоев (справа).

Модель доверия

Хост-процессы, внедряющие действия из других приложений, могут переопределять отображение встроенных действий, включая размер, положение, обрезку и прозрачность. Вредоносные хосты могут использовать эту возможность для введения пользователей в заблуждение и проведения атак типа «кликджекинг» или других атак, направленных на изменение пользовательского интерфейса.

Чтобы предотвратить злоупотребление встраиванием активности между приложениями, Android требует от приложений согласия на встраивание своих действий. Приложения могут назначать хосты доверенными или недоверенными.

Доверенные хосты

Чтобы разрешить другим приложениям встраивать и полностью контролировать представление действий из вашего приложения, укажите сертификат SHA-256 хост-приложения в атрибуте android:knownActivityEmbeddingCerts элементов <activity> или <application> файла манифеста вашего приложения.

Задайте значение 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>

Владельцы приложений могут получить дайджест сертификата SHA, выполнив задачу Gradle signingReport . Дайджест сертификата представляет собой отпечаток SHA-256 без разделяющих двоеточий. Подробнее см. в разделах Запуск отчёта о подписи и Аутентификация клиента .

Недоверенные хосты

Чтобы разрешить любому приложению встраивать действия вашего приложения и управлять их представлением, укажите атрибут android:allowUntrustedActivityEmbedding в элементах <activity> или <application> в манифесте приложения, например:

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

Значение атрибута по умолчанию — false, что предотвращает внедрение активности между приложениями.

Пользовательская аутентификация

Чтобы снизить риски внедрения недоверенной активности, создайте собственный механизм аутентификации, который проверяет подлинность хоста. Если вам известны сертификаты хоста, используйте библиотеку androidx.security.app.authenticator для аутентификации. Если хост проходит аутентификацию после внедрения вашей активности, вы можете отобразить фактическое содержимое. В противном случае вы можете сообщить пользователю, что действие не разрешено, и заблокировать содержимое.

Используйте метод ActivityEmbeddingController#isActivityEmbedded() из библиотеки Jetpack WindowManager, чтобы проверить, встраивает ли хост вашу активность, например:

Котлин

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

Ява

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

Минимальное ограничение размера

Система Android применяет к встроенным действиям минимальную высоту и ширину, указанные в элементе манифеста приложения <layout> . Если в приложении минимальная высота и ширина не указаны, применяются системные значения по умолчанию ( sw220dp ).

Если хост попытается изменить размер встроенного контейнера до размера меньше минимального, встроенный контейнер расширится, заняв все границы задачи.

<activity-alias>

Для работы с элементом <activity-alias> для внедрения доверенных и недоверенных активностей необходимо применить android:knownActivityEmbeddingCerts или android:allowUntrustedActivityEmbedding к целевой активности, а не к псевдониму. Политика, проверяющая безопасность на системном сервере, основана на флагах, установленных для целевой активности, а не на псевдониме.

Хост-приложение

Хостовые приложения реализуют внедрение активности между приложениями так же, как и внедрение активности в рамках одного приложения. Объекты SplitPairRule и SplitPairFilter или ActivityRule и ActivityFilter определяют встроенные активности и разделение окон задач. Правила разделения определяются статически в XML или во время выполнения с помощью вызовов API Jetpack WindowManager.

Если хост-приложение пытается внедрить активность, которая не поддерживает встраивание между приложениями, эта активность занимает все границы задачи. В результате хост-приложениям необходимо знать, допускают ли целевые активности встраивание между приложениями.

Если встроенная активность запускает новую активность в той же задаче и новая активность не выбрала встраивание между приложениями, активность занимает все границы задачи вместо того, чтобы накладываться на активность во встроенном контейнере.

Хост-приложение может встраивать собственные действия без ограничений, если эти действия запускаются в одной и той же задаче.

Примеры разделения

Разделить из полного окна

Рисунок 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. Активность подробной ссылки отображается отдельно на маленьком экране, но вместе с активностью списка на большом экране.

Запрос на запуск должен быть направлен на основную активность, а целевая детальная активность должна быть запущена в отдельном режиме. Система автоматически выбирает правильный режим представления — сложенный или параллельный — в зависимости от доступной ширины экрана.

Котлин

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

Ява

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(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. Действие открыто на дополнительной панели окна задач.

Котлин

class DetailActivity : AppCompatActivity() {
    fun onOpenSubdetail() {
        startActivity(Intent(this, SubdetailActivity::class.java))
    }
}

Ява

public class DetailActivity  extends AppCompatActivity {
    void onOpenSubdetail() {
        startActivity(new Intent(this, SubdetailActivity.class));
    }
}

Подробная деятельность размещается поверх подробной деятельности, скрывая ее:

Затем пользователь может вернуться к предыдущему уровню детализации, перейдя назад по стеку:

Рисунок 19. Активность удалена из верхней части стека.

Размещение действий друг над другом — это поведение по умолчанию при запуске действий из действия в том же вторичном контейнере. Действия, запущенные из основного контейнера в рамках активного разделения, также попадают во вторичный контейнер, расположенный наверху стека действий.

Действия в новой задаче

Когда действия в окне разделённой задачи запускают действия в новой задаче, новая задача отображается отдельно от задачи, включающей разделённую задачу, и в полноэкранном режиме. На экране «Недавние» отображаются две задачи: задача в разделённой задаче и новая задача.

Рисунок 20. Начало действия C в новой задаче из действия B.

Замена активности

Действия можно заменить в стеке вторичного контейнера, например, когда основное действие используется для навигации верхнего уровня, а вторичное действие — выбранный пункт назначения. Каждый выбор в навигационной панели верхнего уровня должен начинать новое действие во вторичном контейнере и удалять одно или несколько действий, которые там были ранее.

Рисунок 21. Навигационная активность верхнего уровня на основной панели заменяет целевые действия на дополнительной панели.

If the app doesn't finish the activity in the secondary container when the navigation selection changes, back navigation might be confusing when the split is collapsed (when the device is folded). For example, if you have a menu in the primary pane and screens A and B stacked in the secondary pane, when the user folds the phone, B is on top of A, and A is on top of the menu. When the user navigates back from B, A appears instead of the menu.

Screen A must be removed from the back stack in such cases.

The default behavior when launching to the side in a new container over an existing split is to put the new secondary containers on top and retain the old ones in the back stack. You can configure the splits to clear the previous secondary containers with clearTop and launch new activities normally.

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

Котлин

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

Ява

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

Alternatively, use the same secondary activity, and from the primary (menu) activity send new intents that resolve to the same instance but trigger a state or UI update in the secondary container.

Multiple splits

Apps can provide multi-level deep navigation by launching additional activities to the side.

When an activity in a secondary container launches a new activity to the side, a new split is created over top of the existing split.

Figure 22. Activity B starts activity C to the side.

The back stack contains all activities that were previously opened, so users can navigate to the A/B split after finishing C.

Activities A, B, and C in a stack. The activities are stacked in
          the following order from top to bottom: C, B, A.

To create a new split, launch the new activity to the side from the existing secondary container. Declare the configurations for both the A/B and B/C splits and launch activity C normally from B:

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

Котлин

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

Ява

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

React to split state changes

Different activities in an app can have UI elements that perform the same function; for example, a control that opens a window containing account settings.

Figure 23. Different activities with functionally identical UI elements.

If two activities that have a UI element in common are in a split, it's redundant and perhaps confusing to show the element in both activities.

Figure 24. Duplicate UI elements in activity split.

To know when activities are in a split, check the SplitController.splitInfoList flow or register a listener with SplitControllerCallbackAdapter for changes in the split state. Then, adjust the UI accordingly:

Котлин

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

Ява

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(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);
            });
}

Coroutines can be launched in any lifecycle state, but are typically launched in the STARTED state to conserve resources (see Use Kotlin coroutines with lifecycle-aware components for more information).

Callbacks can be made in any lifecycle state, including when an activity is stopped. Listeners should usually be registered in onStart() and unregistered in onStop() .

Full-window modal

Some activities block users from interacting with the application until a specified action is performed; for example, a login screen activity, policy acknowledgement screen, or error message. Modal activities should be prevented from appearing in a split.

An activity can be forced to always fill the task window by using the expand configuration:

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

Finish activities

Users can finish activities on either side of the split by swiping from the edge of the display:

Figure 25. Swipe gesture finishing activity B.
Figure 26. Swipe gesture finishing activity A.

If the device is set up to use the back button instead of gesture navigation, the input is sent to the focused activity—the activity that was touched or launched last.

The effect that finishing all activities in a container has on the opposing container depends on the split configuration.

Configuration attributes

You can specify split pair rule attributes to configure how finishing all activities on one side of the split affects the activities on the other side of the split. The attributes are:

  • window:finishPrimaryWithSecondary — How finishing all activities in the secondary container affects the activities in the primary container
  • window:finishSecondaryWithPrimary — How finishing all activities in the primary container affects the activities in the secondary container

Possible values of the attributes include:

  • always — Always finish the activities in the associated container
  • never — Never finish the activities in the associated container
  • adjacent — Finish the activities in the associated container when the two containers are displayed adjacent to each other, but not when the two containers are stacked

Например:

<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>

Конфигурация по умолчанию

When all activities in one container of a split finish, the remaining container occupies the entire window:

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

Split containing activities A and B. A is finished, leaving B to
          occupy the entire window.

Split containing activities A and B. B is finished, leaving A to
          occupy the entire window.

Finish activities together

Finish the activities in the primary container automatically when all activities in the secondary container finish:

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

Split containing activities A and B. B is finished, which also
          finishes A, leaving the task window empty.

Split containing activities A and B. A is finished, leaving B alone
          in the task window.

Finish the activities in the secondary container automatically when all activities in the primary container finish:

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

Split containing activities A and B. A is finished, which also
          finishes B, leaving the task window empty.

Split containing activities A and B. B is finished, leaving A alone
          in the task window.

Finish activities together when all activities in either the primary or secondary container finish:

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

Split containing activities A and B. A is finished, which also
          finishes B, leaving the task window empty.

Split containing activities A and B. B is finished, which also
          finishes A, leaving the task window empty.

Finish multiple activities in containers

If multiple activities are stacked in a split container, finishing an activity on the bottom of the stack does not automatically finish activities on top.

For example, if two activities are in the secondary container, C on top of B:

Secondary activity stack containing activity C stacked on top of B
          is stacked on top of the prmary activity stack containing activity
          A.

and the configuration of the split is defined by the configuration of activities A and B:

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

finishing the top activity retains the split.

Split with activity A in primary container and activities B and C in
          secondary, C stacked on top of B. C finishes, leaving A and B in the
          activity split.

Finishing the bottom (root) activity of the secondary container does not remove the activities on top of it; and so, also retains the split.

Split with activity A in primary container and activities B and C in
          secondary, C stacked on top of B. B finishes, leaving A and C in the
          activity split.

Any additional rules for finishing activities together, such as finishing the secondary activity with the primary, are also executed:

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

Split with activity A in primary container and activities B and C in
          secondary container, C stacked on top of B. A finishes, also
          finishing B and C.

And when the split is configured to finish primary and secondary together:

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

Split with activity A in primary container and activities B and C in
          secondary, C stacked on top of B. C finishes, leaving A and B in the
          activity split.

Split with activity A in primary container and activities B and C in
          secondary, C stacked on top of B. B finishes, leaving A and C in the
          activity split.

Split with activity A in primary container and activities B and C in
          secondary, C stacked on top of B. A finishes, also finishing B and
          C.

Change split properties at runtime

The properties of an active and visible split cannot be changed. Changing the split rules affects additional activity launches and new containers, but not existing and active splits.

To change the properties of active splits, finish the side activity or activities in the split and launch to the side again with a new configuration.

Dynamic split properties

Android 15 (API level 35) and higher supported by Jetpack WindowManager 1.4 and higher offer dynamic features that enable configurability of activity embedding splits, including:

  • Pane expansion: An interactive, draggable divider enables users to resize the panes in a split presentation.
  • Activity stack pinning: Users can pin the content in one container and isolate navigation in the container from navigation in the other container.
  • Dialog full-screen dim: When displaying a dialog, apps can specify whether to dim the entire task window or just the container that opened the dialog.

Pane expansion

Pane expansion enables users to adjust the amount of screen space allocated to the two activities in a dual‑pane layout.

To customize the appearance of the window divider and set the divider's draggable range, do the following:

  1. Create an instance of DividerAttributes

  2. Customize the divider attributes:

    • color : The color of the draggable pane separator.

    • widthDp : The width of the draggable pane separator. Set to WIDTH_SYSTEM_DEFAULT to let the system determine the divider width.

    • Drag range: The minimum percentage of the screen either pane can occupy. Can range from 0.33 to 0.66. Set to DRAG_RANGE_SYSTEM_DEFAULT to let the system determine the drag range.

    Котлин

    val splitAttributesBuilder: SplitAttributes.Builder = SplitAttributes.Builder()
        .setSplitType(SplitAttributes.SplitType.ratio(0.33f))
        .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
    
    if (WindowSdkExtensions.getInstance().extensionVersion >= 6) {
        splitAttributesBuilder.setDividerAttributes(
            DividerAttributes.DraggableDividerAttributes.Builder()
                .setColor(getColor(R.color.divider_color))
                .setWidthDp(4)
                .setDragRange(DividerAttributes.DragRange.DRAG_RANGE_SYSTEM_DEFAULT)
                .build()
        )
    }
    val splitAttributes: SplitAttributes = splitAttributesBuilder.build()

    Ява

    SplitAttributes.Builder splitAttributesBuilder = new SplitAttributes.Builder()
        .setSplitType(SplitAttributes.SplitType.ratio(0.33f))
        .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT);
    
    if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 6) {
        splitAttributesBuilder.setDividerAttributes(
          new DividerAttributes.DraggableDividerAttributes.Builder()
            .setColor(ContextCompat.getColor(this, R.color.divider_color))
            .setWidthDp(4)
            .setDragRange(DividerAttributes.DragRange.DRAG_RANGE_SYSTEM_DEFAULT)
            .build()
        );
    }
    SplitAttributes _splitAttributes = splitAttributesBuilder.build();

Activity stack pinning

Activity stack pinning enables users to pin one of the split windows so the activity stays as is while users navigate within the other window. Activity stack pinning provides an enhanced multitasking experience.

To enable activity stack pinning in your app, do the following:

  1. Add a button to the layout file of the activity you want to pin, for example, the detail activity of an list‑detail layout:

    <androidx.constraintlayout.widget.ConstraintLayout
     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:id="@+id/detailActivity"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:background="@color/white"
     tools:context=".DetailActivity">
    
    <TextView
       android:id="@+id/textViewItemDetail"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:textSize="36sp"
       android:textColor="@color/obsidian"
       app:layout_constraintBottom_toTopOf="@id/pinButton"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toTopOf="parent" />
    
    <androidx.appcompat.widget.AppCompatButton
       android:id="@+id/pinButton"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:text="@string/pin_this_activity"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="@id/textViewItemDetail"/>
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    
  2. In the onCreate() method of the activity, set an onclick listener on the button:

    Котлин

    val pinButton: Button = findViewById(R.id.pinButton)
    pinButton.setOnClickListener {
        val splitAttributes: SplitAttributes = SplitAttributes.Builder()
            .setSplitType(SplitAttributes.SplitType.ratio(0.66f))
            .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
            .build()
    
        val pinSplitRule = SplitPinRule.Builder()
            .setSticky(true)
            .setDefaultSplitAttributes(splitAttributes)
            .build()
    
        SplitController.getInstance(applicationContext)
            .pinTopActivityStack(taskId, pinSplitRule)
    }

    Ява

    Button pinButton = findViewById(R.id.pinButton);
    pinButton.setOnClickListener( (view) -> {
        SplitAttributes splitAttributes = new SplitAttributes.Builder()
            .setSplitType(SplitAttributes.SplitType.ratio(0.66f))
            .setLayoutDirection(SplitAttributes.LayoutDirection.LEFT_TO_RIGHT)
            .build();
    
        SplitPinRule pinSplitRule = new SplitPinRule.Builder()
            .setSticky(true)
            .setDefaultSplitAttributes(splitAttributes)
            .build();
    
        SplitController.getInstance(getApplicationContext())
            .pinTopActivityStack(getTaskId(), pinSplitRule);
    });

Dialog full-screen dim

Activities typically dim their displays to draw attention to a dialog. In activity embedding, both panes of the dual‑pane display should dim, not just the pane containing the activity that opened the dialog, for a unified UI experience.

With WindowManager 1.4 and higher, the entire app window dims by default when a dialog opens (see EmbeddingConfiguration.DimAreaBehavior.ON_TASK ).

To dim only the container of the activity that opened the dialog, use EmbeddingConfiguration.DimAreaBehavior.ON_ACTIVITY_STACK .

Extract an activity from a split to full window

Create a new configuration that displays the side activity full window, and then relaunch the activity with an intent that resolves to the same instance.

Check for split support at runtime

Activity embedding is supported on Android 12L (API level 32) and higher, but is also available on some devices running earlier platform versions. To check at runtime for the availability of the feature, use the SplitController.splitSupportStatus property or SplitController.getSplitSupportStatus() method:

Котлин

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

Ява

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

If splits are not supported, activities are launched on top of the activity stack (following the non-activity embedding model).

Prevent system override

The manufacturers of Android devices (original equipment manufacturers, or OEMs), can implement activity embedding as a function of the device system. The system specifies split rules for multi-activity apps, overriding the windowing behavior of the apps. The system override forces multi-activity apps into a system-defined activity embedding mode.

System activity embedding can enhance app presentation through multi-pane layouts, such as list-detail , without any changes to the app. However, the system's activity embedding might also cause incorrect app layouts, bugs, or conflicts with activity embedding implemented by the app.

Your app can prevent or permit system activity embedding by setting PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE in the app manifest file, for example:

<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>

The property name is defined in the Jetpack WindowManager WindowProperties object. Set the value to false if your app implements activity embedding, or if you want to otherwise prevent the system from applying its activity embedding rules to your app. Set the value to true to permit the system to apply system-defined activity embedding to your app.

Limitations, restrictions, and caveats

  • Only the host app of the task, which is identified as the owner of the root activity in the task, can organize and embed other activities in the task. If activities that support embedding and splits run in a task that belongs to a different application, then embedding and splits will not work for those activities.
  • Activities can only be organized within a single task. Launching an activity in a new task always puts it in a new expanded window outside of any existing splits.
  • Only activities in the same process can be organized and put in a split. The SplitInfo callback only reports activities that belong to the same process, since there is no way of knowing about activities in different processes.
  • Each pair or singular activity rule applies only to activity launches that happen after the rule has been registered. There is currently no way to update existing splits or their visual properties.
  • The split pair filter configuration must match the intents used when launching activities completely. The matching occurs at the point when a new activity is started from the application process, so it might not know about component names that are resolved later in the system process when using implicit intents. If a component name is not known at the time of launch, a wildcard can be used instead ("*/*") and filtering can be performed based on intent action.
  • There is currently no way to move activities between containers or in and out of splits after they were created. Splits are only created by the WindowManager library when new activities with matching rules are launched, and splits are destroyed when the last activity in a split container is finished.
  • Activities can be relaunched when the configuration changes, so when a split is created or removed and activity bounds change, the activity can go through complete destruction of the previous instance and creation of the new one. As a result, app developers should be careful with things like launching new activities from lifecycle callbacks.
  • Devices must include the window extensions interface to support activity embedding. Nearly all large screen devices running Android 12L (API level 32) or higher include the interface. However, some large screen devices that are not capable of running multiple activities don't include the window extensions interface. If a large screen device doesn't support multi-window mode, it might not support activity embedding.

Дополнительные ресурсы