Встраивание объектов activity

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • Сбоку над другим действием:

    Рисунок 4. Действие А запускает действие С сбоку над действием Б.
  • Сбоку, и сдвинуть разделение в сторону, скрыв предыдущее основное действие:

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

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

    Рисунок 7. Действие А или Б запускает действие В, которое занимает окно задачи.

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

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

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

Событие "Назад" отправляется в последнее активное окно при использовании навигации с помощью кнопок.

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

  • Android 14 (уровень API 34) и более ранние версии. Событие "Назад" отправляется в действие, в котором был выполнен жест. Когда пользователь проводит пальцем по экрану слева, событие "Назад" отправляется в действие в левой части разделенного окна. Когда пользователь проводит пальцем от правого края экрана, событие "Назад" отправляется в действие на правой панели.

  • Android 15 (уровень API 35) и более поздние версии

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

    • Если на экране одновременно показываются два приложения (одно поверх другого), событие "Назад" направляется в последнее активное приложение, как и при использовании кнопок навигации.

Макет с несколькими панелями

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

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

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

Разделение атрибутов

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

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

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

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

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

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

Примеры можно найти в разделе WindowManager API.

Рисунок 9. Два разделенных экрана с разными пропорциями.

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

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

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

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

Kotlin

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

Java

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

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

Kotlin

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

Java

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, система отображает заполнитель как самое верхнее действие в окне задач, когда размер экрана уменьшается с двухпанельного до однопанельного (пример можно найти в разделе Разделение экрана).

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

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

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

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

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

Несколько действий на вспомогательной панели

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

Разделение действий, содержащее действия А, Б и В, где В накладывается на Б.

В результате порядок задач в стеке будет следующим:

Вторичный стек объектов activity, в котором действие C находится поверх действия B.
          Вторичный стек объектов activity размещается поверх основного стека объектов activity, содержащего действие А.

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

Небольшое окно, в котором показано только действие C.

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

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

Многоуровневые сплиты

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

Окно задач, в котором сначала показаны действия А и Б, а затем Б и В.

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

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

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

Небольшое окно, в котором показано только действие C.

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

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

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

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

Рисунок 13. Действие А с фиксированной вертикальной ориентацией запускает действие Б сбоку.

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

Конфигурация разделения

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

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

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

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

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

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

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

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

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

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

      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>. Добавьте ссылку на реализацию инициализатора RuleControllerSplitInitializer:

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

WindowManager API

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

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

  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

      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() – минимальная ширина экрана (в пикселях, не зависящих от плотности экрана), при которой можно разделить экран.
      • setMinSmallestWidthDp() – задает минимальное значение (в dp), которое должна иметь меньшая из двух сторон экрана, чтобы можно было включить разделение независимо от ориентации устройства.
      • setMaxAspectRatioInPortrait() – задает максимальное соотношение сторон экрана (высота:ширина) в портретной ориентации, при котором отображаются разделения активности. Если соотношение сторон вертикального дисплея превышает максимальное, разделение экрана отключается независимо от ширины дисплея. Примечание. Значение по умолчанию – 1, 4. При таком значении действия занимают все окно задачи в портретной ориентации на большинстве планшетов. Дополнительная информация: SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT и setMaxAspectRatioInLandscape(). Значение по умолчанию для альбомной ориентации – 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);

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

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

      Kotlin

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

      Java

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

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

      Kotlin

      val placeholderActivityFilterSet = setOf(placeholderActivityFilter)

      Java

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

    9. Создайте 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(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(): Задает минимальную ширину экрана (в пикселях, не зависящих от плотности экрана), при которой возможен разделенный экран.
      • setMinSmallestWidthDp(): Устанавливает минимальное значение (в dp), которое должна иметь меньшая из двух сторон экрана, чтобы разрешить разделение независимо от ориентации устройства.
      • setMaxAspectRatioInPortrait(): Устанавливает максимальное соотношение сторон экрана (высота:ширина) в портретной ориентации, при котором отображаются разделения активности. Примечание. Значение по умолчанию – 1, 4. При таком значении окна задач в большинстве планшетов в портретной ориентации заполняются полностью. Дополнительная информация: SPLIT_MAX_ASPECT_RATIO_PORTRAIT_DEFAULT и setMaxAspectRatioInLandscape(). Значение по умолчанию для альбомной ориентации – ALWAYS_ALLOW.
      • setFinishPrimaryWithPlaceholder(): Определяет, как завершение действия-заполнителя влияет на действия в основном контейнере. ALWAYS означает, что система всегда должна завершать действия в основном контейнере, когда завершается плейсхолдер (см. раздел Завершение действий).
      • setSticky(): определяет, будет ли на небольших экранах поверх стека объектов activity отображаться действие-заполнитель после того, как оно впервые появится в разделенном режиме с достаточной минимальной шириной.
    10. Добавьте правило в WindowManager RuleController:

      Kotlin

      ruleController.addRule(splitPlaceholderRule)

      Java

      ruleController.addRule(splitPlaceholderRule);

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

    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) и более поздних версиях приложения могут встраивать действия из других приложений. Встраивание действий из нескольких приложений Android позволяет визуально интегрировать действия из разных приложений или разных UID. Система отображает на экране рядом или друг над другом активность хост-приложения и встроенную активность из другого приложения так же, как при встраивании активности в одном приложении.

Например, приложение "Настройки" может встроить функцию выбора обоев из приложения 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, чтобы проверить, встраивает ли хост ваше действие. Пример:

Kotlin

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

Java

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

Ограничение по минимальному размеру

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

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

<activity-alias>

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

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

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

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

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

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

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

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

Рисунок 15. Действие А запускает действие Б сбоку.

Рефакторинг не требуется. Вы можете задать конфигурацию для разделения статически или во время выполнения, а затем вызвать 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?) {
    super.onCreate(savedInstanceState)
    RuleController.getInstance(this)
        .addRule(SplitPairRule.Builder(filterSet).build())
    startActivity(Intent(this, DetailActivity::class.java))
}

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

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

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

Небольшой медийный баннер с подробной информацией об активности. Обратная навигация не позволяет закрыть экран с подробной информацией и вернуться к списку.

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

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

Подробнее об атрибутах конфигурации…

Несколько действий в разделенных контейнерах

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

Рисунок 18. История действий открыта на месте на дополнительной панели окна задачи.

Kotlin

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

Java

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

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

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

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

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

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

Если в окне разделенной задачи запускается новая задача, она будет отделена от исходной и отобразится в полноэкранном режиме. На экране "Недавние" показываются две задачи: задача в разделенном режиме и новая задача.

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

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

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

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

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

В таких случаях экран А должен быть удален из стека возврата.

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

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

Kotlin

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

Java

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

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

Несколько разделений

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

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

Рисунок 22. Действие Б запускает действие В сбоку.

Стек возврата содержит все ранее открытые действия, поэтому пользователи могут перейти к разделению A/B после завершения C.

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

Чтобы создать новый разделенный экран, запустите новое приложение сбоку от существующего дополнительного контейнера. Объявите конфигурации для разделения A/B и B/C и запустите Activity C обычным способом из Activity B:

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

Kotlin

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

Java

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

Как реагировать на изменения состояния разделения экрана

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

Рисунок 23. Разные действия с функционально идентичными элементами интерфейса.

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

Рисунок 24. Повторяющиеся элементы интерфейса в разделенном представлении действий.

Чтобы узнать, когда действия выполняются в разделенном режиме, проверьте поток SplitController.splitInfoList или зарегистрируйте прослушиватель с помощью SplitControllerCallbackAdapter для отслеживания изменений в разделенном режиме. Затем настройте интерфейс:

Kotlin

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

Java

@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);
            });
}

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

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

Полноэкранное модальное окно

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

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

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

Завершать занятия

Чтобы завершить действие в одной из частей экрана, проведите пальцем от края дисплея:

Рисунок 25. Жест пролистывания завершает действие Б.
Рисунок 26. Жест пролистывания завершает действие А.

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

Влияние завершения всех действий в контейнере на противоположный контейнер зависит от конфигурации разделения.

Атрибуты конфигурации

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

  • 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 займет все окно.

Сегмент, содержащий действия А и Б. Когда B закончится, A займет все окно.

совместно выполнять задания;

Автоматически завершать действия в основном контейнере, когда завершаются все действия во вспомогательном контейнере:

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

Сегмент, содержащий действия А и Б. Задача Б завершена, а вместе с ней и задача А. Окно задач пустое.

Сегмент, содержащий действия А и Б. Задача A завершена, и в окне задач осталась только задача B.

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

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

Сегмент, содержащий действия А и Б. Задача А завершена, а вместе с ней и задача Б. Окно задач пустое.

Сегмент, содержащий действия А и Б. Задача Б завершена, и в окне задач осталась только задача А.

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

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

Сегмент, содержащий действия А и Б. Задача А завершена, а вместе с ней и задача Б. Окно задач пустое.

Сегмент, содержащий действия А и Б. Задача Б завершена, а вместе с ней и задача А. Окно задач пустое.

Как выполнять несколько действий в контейнерах

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

Например, если в дополнительном контейнере два элемента, C и B, причем C находится поверх B:

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

а конфигурация разделения определяется конфигурацией действий А и Б:

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

Если вы завершите верхнее занятие, разделение сохранится.

Разделение с действием А в основном контейнере и действиями Б и В во второстепенном, причем В расположено над Б. C завершает работу, и A и B остаются в разделенном режиме.

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

Разделение с действием А в основном контейнере и действиями Б и В во второстепенном, причем В расположено над Б. B завершает работу, и A и C остаются в разделенном режиме.

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

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

Разделение с действием А в основном контейнере и действиями Б и В во вторичном контейнере, причем В наложено на Б. А завершается, а также завершаются В и С.

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

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

Разделение с действием А в основном контейнере и действиями Б и В во второстепенном, причем В расположено над Б. C завершает работу, и A и B остаются в разделенном режиме.

Разделение с действием А в основном контейнере и действиями Б и В во второстепенном, причем В расположено над Б. B завершает работу, и A и C остаются в разделенном режиме.

Разделение с действием А в основном контейнере и действиями Б и В во второстепенном, причем В расположено над Б. Задача А завершается, а вместе с ней – задачи Б и В.

Как изменить свойства разделения во время выполнения

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

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

Динамические свойства разделения

Android 15 (уровень API 35) и более поздние версии, поддерживаемые Jetpack WindowManager 1.4 и более поздними версиями, предлагают динамические функции, позволяющие настраивать разделение встроенных действий, в том числе:

  • Разделение на панели. Интерактивный перетаскиваемый разделитель позволяет пользователям изменять размер панелей в разделенной презентации.
  • Закрепление стека объектов activity. Пользователи могут закрепить контент в одном контейнере и изолировать навигацию в нем от навигации в другом контейнере.
  • Затемнение экрана при показе диалогового окна. При показе диалогового окна приложения могут указывать, нужно ли затемнять все окно задачи или только контейнер, в котором открылось диалоговое окно.

Развертывание панели

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

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

  1. Как создать экземпляр DividerAttributes

  2. Настройте атрибуты разделителя:

    • color: цвет разделителя перетаскиваемой панели.

    • widthDp: ширина разделителя перетаскиваемой панели. Задайте значение WIDTH_SYSTEM_DEFAULT, чтобы система определяла ширину разделителя.

    • Диапазон перетаскивания. Минимальный процент экрана, который может занимать каждая панель. Значение может быть от 0,33 до 0,66. Задайте значение DRAG_RANGE_SYSTEM_DEFAULT, чтобы система определяла диапазон перетаскивания.

    Kotlin

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

    Java

    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

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

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

  1. Добавьте кнопку в файл макета объекта activity, который вы хотите закрепить, например в объект activity с подробными сведениями в макете "Список и подробные сведения":

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

    Kotlin

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

    Java

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

Диалоговое окно с затемнением экрана

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

В WindowManager версии 1.4 и более поздних версий при открытии диалогового окна по умолчанию затемняется все окно приложения (см. EmbeddingConfiguration.DimAreaBehavior.ON_TASK).

Чтобы затемнить только контейнер действия, которое открыло диалоговое окно, используйте EmbeddingConfiguration.DimAreaBehavior.ON_ACTIVITY_STACK.

Как извлечь действие из разделенного экрана в полноэкранный режим

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

Как проверить поддержку разделения во время выполнения

Встраивание действий поддерживается в 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) могут встроить функцию в системное ПО устройства. Система задает правила разделения для приложений с несколькими действиями, переопределяя поведение окон приложений. Системная переопределяющая функция принудительно переводит многоэкранные приложения в режим встраивания, заданный системой.

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

Чтобы запретить или разрешить встраивание системных действий, задайте значение PROPERTY_ACTIVITY_EMBEDDING_ALLOW_SYSTEM_OVERRIDE в файле манифеста приложения, например:

<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 при запуске новых действий с соответствующими правилами. Разделение экрана прекращается, когда завершается последнее действие в контейнере разделения.
  • Объекты activity можно перезапускать при изменении конфигурации, поэтому при создании или удалении разделения экрана и изменении границ объекта activity может быть полностью уничтожен предыдущий экземпляр и создан новый. Поэтому разработчикам приложений следует осторожно использовать такие функции, как запуск новых действий из обратных вызовов жизненного цикла.
  • Устройства должны включать интерфейс расширений окон, чтобы поддерживать встраивание действий. Интерфейс есть почти на всех устройствах с большим экраном, на которых установлена ОС Android 12L (уровень API 32) или более поздней версии. Однако некоторые устройства с большим экраном, которые не могут запускать несколько действий, не включают интерфейс расширений окон. Если устройство с большим экраном не поддерживает многооконный режим, оно может не поддерживать и встраивание действий.

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