Сделайте ваше приложение осведомленным о сворачивании

Большие развернутые дисплеи и уникальные состояния в сложенном состоянии открывают новые возможности для пользователей на складных устройствах. Чтобы обеспечить поддержку складывания вашего приложения, используйте библиотеку Jetpack WindowManager , которая предоставляет поверхность API для таких функций складных окон устройств, как складки и петли. Когда ваше приложение поддерживает складку, оно может адаптировать свой макет, чтобы избежать размещения важного контента в области сгибов или петель и использовать складки и петли в качестве естественных разделителей.

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

Информация об окне

Интерфейс WindowInfoTracker в Jetpack WindowManager предоставляет информацию о макете окна. Метод windowLayoutInfo() интерфейса возвращает поток данных WindowLayoutInfo , который информирует ваше приложение о состоянии складывания складного устройства. Метод WindowInfoTracker getOrCreate() создает экземпляр WindowInfoTracker .

WindowManager обеспечивает поддержку сбора данных WindowLayoutInfo с помощью Kotlin Flows и обратных вызовов Java.

Котлин потоки

Чтобы запустить и остановить сбор данных WindowLayoutInfo , вы можете использовать перезапускаемую сопрограмму, учитывающую жизненный цикл , в которой блок кода repeatOnLifecycle выполняется, когда жизненный цикл по крайней мере STARTED , и останавливается, когда жизненный цикл STOPPED . Выполнение блока кода автоматически перезапускается, когда жизненный цикл снова STARTED . В следующем примере блок кода собирает и использует данные WindowLayoutInfo :

class DisplayFeaturesActivity : AppCompatActivity() {

    private lateinit var binding: ActivityDisplayFeaturesBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityDisplayFeaturesBinding.inflate(layoutInflater)
        setContentView(binding.root)

        lifecycleScope.launch(Dispatchers.Main) {
            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                WindowInfoTracker.getOrCreate(this@DisplayFeaturesActivity)
                    .windowLayoutInfo(this@DisplayFeaturesActivity)
                    .collect { newLayoutInfo ->
                        // Use newLayoutInfo to update the layout.
                    }
            }
        }
    }
}

Обратные вызовы Java

Уровень совместимости обратного вызова, включенный в зависимость androidx.window:window-java позволяет собирать обновления WindowLayoutInfo без использования Kotlin Flow. Артефакт включает в себя класс WindowInfoTrackerCallbackAdapter , который адаптирует WindowInfoTracker для поддержки регистрации (и отмены регистрации) обратных вызовов для получения обновлений WindowLayoutInfo , например:

public class SplitLayoutActivity extends AppCompatActivity {

    private WindowInfoTrackerCallbackAdapter windowInfoTracker;
    private ActivitySplitLayoutBinding binding;
    private final LayoutStateChangeCallback layoutStateChangeCallback =
            new LayoutStateChangeCallback();

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);

       binding = ActivitySplitLayoutBinding.inflate(getLayoutInflater());
       setContentView(binding.getRoot());

       windowInfoTracker =
                new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this));
   }

   @Override
   protected void onStart() {
       super.onStart();
       windowInfoTracker.addWindowLayoutInfoListener(
                this, Runnable::run, layoutStateChangeCallback);
   }

   @Override
   protected void onStop() {
       super.onStop();
       windowInfoTracker
           .removeWindowLayoutInfoListener(layoutStateChangeCallback);
   }

   class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
       @Override
       public void accept(WindowLayoutInfo newLayoutInfo) {
           SplitLayoutActivity.this.runOnUiThread( () -> {
               // Use newLayoutInfo to update the layout.
           });
       }
   }
}

Поддержка RxJava

Если вы уже используете RxJava (версия 2 или 3 ), вы можете воспользоваться артефактами, которые позволяют использовать Observable или Flowable для сбора обновлений WindowLayoutInfo без использования Kotlin Flow.

Уровень совместимости, предоставляемый зависимостями androidx.window:window-rxjava2 и androidx.window:window-rxjava3 включает методы WindowInfoTracker#windowLayoutInfoFlowable() и WindowInfoTracker#windowLayoutInfoObservable() , которые позволяют вашему приложению получать обновления WindowLayoutInfo , например:

class RxActivity: AppCompatActivity {

    private lateinit var binding: ActivityRxBinding

    private var disposable: Disposable? = null
    private lateinit var observable: Observable<WindowLayoutInfo>

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);

       binding = ActivitySplitLayoutBinding.inflate(getLayoutInflater());
       setContentView(binding.getRoot());

        // Create a new observable
        observable = WindowInfoTracker.getOrCreate(this@RxActivity)
            .windowLayoutInfoObservable(this@RxActivity)
   }

   @Override
   protected void onStart() {
       super.onStart();

        // Subscribe to receive WindowLayoutInfo updates
        disposable?.dispose()
        disposable = observable
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe { newLayoutInfo ->
            // Use newLayoutInfo to update the layout
        }
   }

   @Override
   protected void onStop() {
       super.onStop();

        // Dispose the WindowLayoutInfo observable
        disposable?.dispose()
   }
}

Особенности складных дисплеев

Класс WindowLayoutInfo Jetpack WindowManager делает функции окна отображения доступными в виде списка элементов DisplayFeature .

FoldingFeature — это тип DisplayFeature , который предоставляет информацию о складных дисплеях, включая следующее:

  • state : сложенное состояние устройства, FLAT или HALF_OPENED
  • orientation : ориентация сгиба или петли: HORIZONTAL или VERTICAL
  • occlusionType : скрывает ли складка или шарнир часть дисплея, NONE или FULL
  • isSeparating : создает ли сгиб или шарнир две логические области отображения: true или false.

Складное устройство с параметром HALF_OPENED всегда сообщает isSeparating как true, поскольку экран разделен на две области отображения. Кроме того, isSeparating всегда имеет значение true на устройстве с двумя экранами, когда приложение охватывает оба экрана.

Свойство bounds FoldingFeature (наследованное от DisplayFeature ) представляет ограничивающий прямоугольник элемента сгиба, такого как сгиб или шарнир. Границы можно использовать для позиционирования элементов на экране относительно объекта.

Котлин

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    lifecycleScope.launch(Dispatchers.Main) {
        lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
            // Safely collects from windowInfoRepo when the lifecycle is STARTED
            // and stops collection when the lifecycle is STOPPED
            WindowInfoTracker.getOrCreate(this@MainActivity)
                .windowLayoutInfo(this@MainActivity)
                .collect { layoutInfo ->
                    // New posture information
                    val foldingFeature = layoutInfo.displayFeatures
                        .filterIsInstance()
                        .firstOrNull()
                    // Use information from the foldingFeature object
                }

        }
    }
}

Ява

private WindowInfoTrackerCallbackAdapter windowInfoTracker;
private final LayoutStateChangeCallback layoutStateChangeCallback =
                new LayoutStateChangeCallback();

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    ...
    windowInfoTracker =
            new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this));
}

@Override
protected void onStart() {
    super.onStart();
    windowInfoTracker.addWindowLayoutInfoListener(
            this, Runnable::run, layoutStateChangeCallback);
}

@Override
protected void onStop() {
    super.onStop();
    windowInfoTracker.removeWindowLayoutInfoListener(layoutStateChangeCallback);
}

class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
    @Override
    public void accept(WindowLayoutInfo newLayoutInfo) {
        // Use newLayoutInfo to update the Layout
        List<DisplayFeature> displayFeatures = newLayoutInfo.getDisplayFeatures();
        for (DisplayFeature feature : displayFeatures) {
            if (feature instanceof FoldingFeature) {
                // Use information from the feature object
            }
        }
    }
}

Настольный режим

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

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

Приложение видеоплеера в настольном режиме

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

Котлин

fun isTableTopPosture(foldFeature : FoldingFeature?) : Boolean {
    contract { returns(true) implies (foldFeature != null) }
    return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
            foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
}

Ява

boolean isTableTopPosture(FoldingFeature foldFeature) {
    return (foldFeature != null) &&
           (foldFeature.getState() == FoldingFeature.State.HALF_OPENED) &&
           (foldFeature.getOrientation() == FoldingFeature.Orientation.HORIZONTAL);
}

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

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

API предоставляет список поз, поддерживаемых устройством. Если список содержит положение «столешница», вы можете разделить макет и поддержать это положение или запустить различные A/B-тесты для своего пользовательского интерфейса.

Котлин

if (WindowSdkExtensions.getInstance().extensionsVersion >= 6) {
    val postures = WindowInfoTracker.getOrCreate(context).supportedPostures
    if (postures.contains(TABLE_TOP)) {
        // Device supports tabletop mode.
   }
}

Ява

if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 6) {
    List<SupportedPosture> postures = WindowInfoTracker.getOrCreate(context).getSupportedPostures();
    if (postures.contains(SupportedPosture.TABLETOP)) {
        // Device supports tabletop mode.
    }
}

Примеры

  • Приложение MediaPlayerActivity : узнайте, как использовать Media3 Exoplayer и WindowManager для создания складного видеопроигрывателя.

  • Лаборатория кода «Раскройте возможности камеры» : узнайте, как реализовать настольный режим для приложений для фотографий. Покажите видоискатель в верхней половине экрана над сгибом, а элементы управления — в нижней половине под сгибом.

Режим книги

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

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

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

Котлин

fun isBookPosture(foldFeature : FoldingFeature?) : Boolean {
    contract { returns(true) implies (foldFeature != null) }
    return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
            foldFeature.orientation == FoldingFeature.Orientation.VERTICAL
}

Ява

boolean isBookPosture(FoldingFeature foldFeature) {
    return (foldFeature != null) &&
           (foldFeature.getState() == FoldingFeature.State.HALF_OPENED) &&
           (foldFeature.getOrientation() == FoldingFeature.Orientation.VERTICAL);
}

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

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

Класс Jetpack WindowManager WindowMetricsCalculator позволяет получать текущие и максимальные метрики окна. Как и платформа WindowMetrics представленная на уровне API 30, WindowManager WindowMetrics обеспечивает границы окна, но API обратно совместим вплоть до уровня API 14.

См. Классы размеров окон .

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

Образцы

  • Jetpack WindowManager : пример использования библиотеки Jetpack WindowManager.
  • Jetcaster : реализация положения стола с помощью Compose

Кодлабы

{% дословно %} {% дословно %} {% дословно %} {% дословно %}