Большие развёрнутые экраны и уникальные сложенные состояния открывают новые возможности для пользователей складных устройств. Чтобы ваше приложение учитывало сгибание, используйте библиотеку 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
, ваше приложение может поддерживать такие положения, как «настольное», когда телефон располагается на поверхности, шарнир находится в горизонтальном положении, а складной экран наполовину открыт.
Положение «на столе» позволяет пользователям удобно управлять телефоном, не держа его в руках. Положение «на столе» отлично подходит для просмотра медиаконтента, фотосъемки и видеозвонков.

Используйте 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.
}
}
Примеры
Приложение
MediaPlayerActivity
: узнайте, как использовать Media3 Exoplayer и WindowManager для создания видеоплеера с функцией сворачивания.Оптимизируйте приложение камеры на складных устройствах с помощью практического занятия по Jetpack 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);
}
Изменения размера окна
Область отображения приложения может измениться в результате изменения конфигурации устройства, например, при его складывании или раскладывании, повороте или изменении размера окна в многооконном режиме.
Класс WindowMetricsCalculator
в Jetpack WindowManager позволяет получать текущие и максимальные метрики окна. Как и платформенные WindowMetrics
представленные в API уровня 30, WindowManager WindowMetrics
предоставляет границы окна, но API обратно совместимо с API уровня 14.
См. раздел Использование классов размеров окон .
Дополнительные ресурсы
Образцы
- Jetpack WindowManager : пример использования библиотеки Jetpack WindowManager
- Jetcaster : Реализация настольной позы с помощью Compose
Codelabs
- Поддержка складных и двухэкранных устройств с помощью Jetpack WindowManager
- Оптимизируйте приложение камеры на складных устройствах с помощью Jetpack WindowManager