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

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

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

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

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

WindowManager обеспечивает поддержку сбора данных WindowLayoutInfo с использованием потоков Kotlin и обратных вызовов 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. Артефакт включает класс 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.

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

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

Котлин

override fun onCreate(savedInstanceState: Bundle?) {
    // ...
    lifecycleScope.launch(Dispatchers.Main) {
        lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
            // Safely collects from WindowInfoTracker 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<FoldingFeature>()
                        .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 , ваше приложение может поддерживать такие положения, как «настольное», когда телефон располагается на поверхности, шарнир находится в горизонтальном положении, а складной экран наполовину открыт.

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

Рисунок 1. Приложение видеоплеера в настольном положении — видео в вертикальной части экрана; элементы управления воспроизведением в горизонтальной части.

Используйте 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 posture.
   }
}

Ява

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

Примеры

Книжная поза

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

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

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

Котлин

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

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

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

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

См. раздел Использование классов размеров окон .

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

Образцы

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

Codelabs