Большие развернутые дисплеи и уникальные состояния в сложенном виде открывают новые возможности для пользователей складных устройств. Чтобы ваше приложение учитывало особенности складных устройств, используйте библиотеку 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 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 of the WindowLayoutInfo observable.
disposable?.dispose()
}
}
Особенности складных дисплеев
Класс WindowLayoutInfo из Jetpack WindowManager предоставляет доступ к функциям отображаемого окна в виде списка элементов DisplayFeature .
Объект FoldingFeature — это тип DisplayFeature , предоставляющий информацию о складных дисплеях, включая следующие свойства:
state: Состояние устройства в сложенном виде:FLATилиHALF_OPENEDorientation: Ориентация складки или шарнира,HORIZONTALилиVERTICALocclusionType: Скрывает ли складка или шарнир часть дисплея,NONEилиFULLisSeparating: Создает ли сгиб или шарнир две логические области отображения, 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 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.
}
}
}
}
Java
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
}
Java
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.
}
}
Java
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 для создания видеоплеера, поддерживающего Folding@Home.Оптимизируйте свое приложение камеры на складных устройствах с помощью 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
}
Java
boolean isBookPosture(FoldingFeature foldFeature) {
return (foldFeature != null) &&
(foldFeature.getState() == FoldingFeature.State.HALF_OPENED) &&
(foldFeature.getOrientation() == FoldingFeature.Orientation.VERTICAL);
}
Изменение размера окна
Область отображения приложения может изменяться в результате изменения конфигурации устройства, например, при складывании или раскладывании устройства, его повороте или изменении размера окна в многооконном режиме.
Класс WindowMetricsCalculator из Jetpack WindowManager позволяет получить текущие и максимальные метрики окна. Подобно платформенной WindowMetrics представленной в API уровня 30, класс WindowMetrics из WindowManager предоставляет границы окна, но API обратно совместим с API уровня 14.
См. раздел «Использование классов размеров окон» .
Дополнительные ресурсы
Образцы
- Jetpack WindowManager : пример использования библиотеки Jetpack WindowManager.
- Jetcaster : Реализация позиционирования за столом с помощью Compose.
Кодлабс
- Поддержка складных и двухэкранных устройств с помощью Jetpack WindowManager
- Оптимизируйте приложение камеры на складных устройствах с помощью Jetpack WindowManager.